JVM內(nèi)存結(jié)構(gòu):程序計數(shù)器、虛擬機棧、本地方法棧
一、JVM 入門介紹
JVM 定義
Java Virtual Machine,JAVA程序的運行環(huán)境(JAVA二進制字節(jié)碼的運行環(huán)境)
JVM 優(yōu)勢
- 一次編寫,到處運行
- 自動內(nèi)存管理,垃圾回收機制
- 數(shù)組下標越界檢查 常見的JVM
注:我們筆記所使用的的是HotSpot 版本
JVM JRE JDK的比較
JVM JRE JDK的區(qū)別:
學(xué)習(xí)步驟
學(xué)習(xí)順序如下圖:(由簡到難)
二、內(nèi)存結(jié)構(gòu)
整體架構(gòu)
1、程序計數(shù)器(寄存器)
Program Counter Register
1.1 作用
程序計數(shù)器用于保存JVM中下一條所要執(zhí)行的指令的地址
0:getstatic #20 // PrintStream out = System.out; 1:astore_1 // -- 2:aload_1 // out.println(1); 3:iconst_1 // -- 4:invokevirtual #26 // -- 5:aload_1 // out.println(2); 6:iconst_2 // -- 7:invokevirtual #26 // -- 8:aload_1 // out.println(3); 9:iconst_3 // -- 10:invokevirtual #26 // -- 11:aload_1 // out.println(4); 12:iconst_4 // -- 13:invokevirtual #26 // -- 14:aload_1 // out.println(5); 15:iconst_5 // -- 16:invokevirtual #26 // -- return
Java指令執(zhí)行流程:
- 每一條二進制字節(jié)碼(JVM指令) 通過 解釋器 轉(zhuǎn)換成 機器碼 然后 就可以被 CPU 執(zhí)行了!
- 當(dāng) 解釋器 將一條jvm 指令轉(zhuǎn)換成 機器碼后 其會 向程序計數(shù)器 遞交 下一條 jvm 指令的執(zhí)行地址!
- 程序計數(shù)器在硬件層面 其實是通過 寄存器 實現(xiàn)的!
- 所以程序計數(shù)器的作用就是:用于保存JVM中下一條所要執(zhí)行的指令的地址!
1.2 特點
- 線程私有
- CPU會為每個線程分配時間片,當(dāng)當(dāng) 前線程的時間片使用完以后,CPU就會去執(zhí)行另一個線程中的代碼
- 程序計數(shù)器是每個線程所私有的,當(dāng)另一個線程的時間片用完,又返回來執(zhí)行當(dāng)前線程的代碼時,通過程序計數(shù)器可以知道應(yīng)該執(zhí)行哪一句指令
- 不會存在內(nèi)存溢出
2、虛擬機棧
Java Virtual Machine Stacks
2.1 定義
- 每個線程運行需要的內(nèi)存空間,這一空間被稱為虛擬機棧(Frames)
- 每個棧由多個棧幀(Frame) 組成,對應(yīng)著每個方法運行時所占用的內(nèi)存
- 每個線程只能有一個活動棧幀,對應(yīng)著當(dāng)前正在執(zhí)行的方法,當(dāng)方法執(zhí)行時壓入棧,方法執(zhí)行完畢后 彈出棧
2.2 演示
代碼
/** * @Auther: csp1999 * @Date: 2020/11/10/11:36 * @Description: 演示棧幀 */ public class Demo01 { public static void main(String[] args) { methodA(); } private static void methodA() { methodB(1, 2); } private static int methodB(int a, int b) { int c = a + b; return c; } }
我們打斷點來Debug 一下看一下方法執(zhí)行的流程:
接這往下走,使方法B執(zhí)行完畢:
然后方法A執(zhí)行完畢,其對應(yīng)的棧幀出棧,main方法對應(yīng)的棧幀為活動棧幀;最后main執(zhí)行完畢 棧幀出棧,虛擬機棧為空,代碼運行結(jié)束!
2.3 面試問題辨析
- 1.垃圾回收是否涉及棧內(nèi)存?
- 不需要。因為虛擬機棧中是由一個個棧幀組成的,在方法執(zhí)行完畢后,對應(yīng)的棧幀就會被彈出棧。所以無需通過垃圾回收機制去回收內(nèi)存。
- 2.棧內(nèi)存的分配越大越好嗎?
- 不是。因為物理內(nèi)存是一定的,棧內(nèi)存越大,可以支持更多的遞歸調(diào)用,但是可執(zhí)行的線程數(shù)就會越少。
- 舉例:如果物理內(nèi)存是500M(假設(shè)),如果一個線程所能分配的棧內(nèi)存為2M的話,那么可以有250個線程。而如果一個線程分配棧內(nèi)存占5M的話,那么最多只能有100 個線程同時執(zhí)行!
3.方法內(nèi)的局部變量是否是線程安全的?
從圖中得出:局部變量如果是靜態(tài)的可以被多個線程共享,那么就存在線程安全問題。如果是非靜態(tài)的只存在于某個方法作用范圍內(nèi),被線程私有,那么就是線程安全的!
看一個案例:
/** * 局部變量的線程安全問題 */ public class Demo02 { public static void main(String[] args) {// main 函數(shù)主線程 StringBuilder sb = new StringBuilder(); sb.append(4); sb.append(5); sb.append(6); new Thread(() -> {// Thread新創(chuàng)建的線程 m2(sb); }).start(); } public static void m1() { // sb 作為方法m1()內(nèi)部的局部變量,是線程私有的 ---> 線程安全 StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static void m2(StringBuilder sb) { // sb 作為方法m2()外部的傳遞來的參數(shù),sb 不在方法m2()的作用范圍內(nèi) // 不是線程私有的 ---> 非線程安全 sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static StringBuilder m3() { // sb 作為方法m3()內(nèi)部的局部變量,是線程私有的 StringBuilder sb = new StringBuilder();// sb 為引用類型的變量 sb.append(1); sb.append(2); sb.append(3); return sb;// 然而方法m3()將sb返回,sb逃離了方法m3()的作用范圍,且sb是引用類型的變量 // 其他線程也可以拿到該變量的 ---> 非線程安全 // 如果sb是非引用類型,即基本類型(int/char/float...)變量的話,逃離m3()作用范圍后,則不會存在線程安全 } }
該面試題答案:
如果方法內(nèi)局部變量沒有逃離方法的作用范圍,則是線程安全的
如果局部變量引用了對象,并逃離了方法的作用范圍,則需要考慮線程安全問題
2.4 內(nèi)存溢出
Java.lang.stackOverflowError 棧內(nèi)存溢出
發(fā)生原因
- 1.虛擬機棧中,棧幀過多(無限遞歸),這種情況比較常見!
- 2.每個棧幀所占用內(nèi)存過大(某個/某幾個棧幀內(nèi)存直接超過虛擬機棧最大內(nèi)存),這種情況比較少見!
舉2個案例:
案例1:
/** * 演示棧內(nèi)存溢出 java.lang.StackOverflowError * -Xss256k 可以通過棧內(nèi)存參數(shù) 設(shè)置棧內(nèi)存大小 */ public class Demo03 { private static int count; public static void main(String[] args) { try { method1(); } catch (Throwable e) { e.printStackTrace(); System.out.println(count); } } private static void method1() { count++;// 統(tǒng)計棧幀個數(shù) method1();// 方法無限遞歸,不斷產(chǎn)生棧幀 到虛擬機棧 } } 最后輸出結(jié)果: java.lang.StackOverflowError at com.haust.jvm_study.demo.Demo03.method1(Demo03.java:21) ... ... 39317// 棧幀個數(shù),不同的虛擬機大小能存放的棧幀數(shù)量不一樣
我們可以通過修改參數(shù)來指定虛擬機棧內(nèi)存大小
當(dāng)我們將虛擬機棧內(nèi)存縮小到指定的256k的時候再運行Demo03后,會得到其棧內(nèi)最大棧幀數(shù)為:3816 遠小于原來的39317!
案例2:
/** * 兩個類之間的循環(huán)引用問題,導(dǎo)致的棧溢出 * * 解決方案:打斷循環(huán),即在員工emp 中忽略其dept屬性,放置遞歸互相調(diào)用 */ public class Demo04 { public static void main(String[] args) throws JsonProcessingException { Dept d = new Dept(); d.setName("Market"); Emp e1 = new Emp(); e1.setName("csp"); e1.setDept(d); Emp e2 = new Emp(); e2.setName("hzw"); e2.setDept(d); d.setEmps(Arrays.asList(e1, e2)); // 輸出結(jié)果:{"name":"Market","emps":[{"name":"csp"},{"name":"hzw"}]} ObjectMapper mapper = new ObjectMapper();// 要導(dǎo)入jackson包 System.out.println(mapper.writeValueAsString(d)); } } /** * 員工 */ class Emp { private String name; @JsonIgnore// 忽略該屬性:為啥呢?我們來分析一下! /** * 如果我們不忽略掉員工對象中的部門屬性 * System.out.println(mapper.writeValueAsString(d)); * 會出現(xiàn)下面的結(jié)果: * { * "name":"Market","emps": * [c * {"name":"csp",dept:{name:'xxx',emps:'...'}}, * ... * ] * } * 也就是說,輸出結(jié)果中,部門對象dept的json串中包含員工對象emp, * 而員工對象emp 中又包含dept,這樣互相包含就無線遞歸下去,json串越來越長... * 直到棧溢出! */ private Dept dept; public String getName() { return name; } public void setName(String name) { this.name = name; } public Dept getDept() { return dept; } public void setDept(Dept dept) { this.dept = dept; } } /** * 部門 */ class Dept { private String name; private List<Emp> emps; public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Emp> getEmps() { return emps; } public void setEmps(List<Emp> emps) { this.emps = emps; } }
2.5 線程運行診斷
案例1:CPU占用過高
- Linux環(huán)境下運行某些程序的時候,可能導(dǎo)致CPU的占用過高,這時需要定位占用CPU過高的線程
- top命令,查看是哪個進程占用CPU過高
- ps H -eo pid, tid(線程id), %cpu | grep 剛才通過top查到的進程號 通過ps命令進一步查看具體是哪個線程占用CPU過高!
- jstack 進程id 通過查看進程中的線程的nid,剛才通過ps命令看到的tid來對比定位,注意jstack查找出的線程id是16進制的,需要轉(zhuǎn)換
- 可以通過線程id,找到有問題的線程,進一步定位到問題代碼的源碼行數(shù)!
我們可以看到上圖中的thread1 線程一直在運行(runnable)中,說明就是它占用了較高的CPU內(nèi)存;
3、本地方法棧
一些帶有native 關(guān)鍵字的方法就是需要JAVA去調(diào)用本地的C或者C++方法,因為JAVA有時候沒法直接和操作系統(tǒng)底層交互,所以需要用到本地方法!
如圖:
- 本地接口的作用是融合不同的編程語言為Java所用,它的初衷是融合C/C++程序,Java誕生的時候是C/C++橫行的時候,要想立足,必須由調(diào)用C/C++程序,于是就在內(nèi)存中專門開辟了一塊區(qū)域處理標記為native的代碼,它的具體做法是Native Method Stack中登記native方法,在Execution Engine執(zhí)行時加載native libraies
- 目前該方法的使用的越來越少了,除非是與硬件有關(guān)的應(yīng)用,比如通過Java程序驅(qū)動打印機或者Java系統(tǒng)管理生產(chǎn)設(shè)備,在企業(yè)級應(yīng)用中已經(jīng)比較少見。因為現(xiàn)在的異構(gòu)領(lǐng)域間的通信很發(fā)達,比如可以使用Socket通信,也可以使用Web Service等等,不多做介紹
- 本地方法棧(Native Method Stack):(它的具體做法是Native Method Stack中登記native方法,在Execution Engine 執(zhí)行時加載本地方法庫)
- native方法的舉例: Object類中的clone wait notify hashCode 等 Unsafe類都是native方法
4、總結(jié)
這篇文章的內(nèi)容就到這了,希望大家多多關(guān)注腳本之家的其他內(nèi)容!
相關(guān)文章
Flink流處理引擎零基礎(chǔ)速通之?dāng)?shù)據(jù)的抽取篇
今天不分享基礎(chǔ)概念知識了,來分享一個馬上工作需要的場景,要做數(shù)據(jù)的抽取,不用kettle,想用flink。實際就是flink的sql、table層級的api2022-05-05Java結(jié)合redistemplate使用分布式鎖案例講解
在Java中使用RedisTemplate結(jié)合Redis來實現(xiàn)分布式鎖是一種常見的做法,特別適用于微服務(wù)架構(gòu)或多實例部署的應(yīng)用程序中,以確保數(shù)據(jù)的一致性和避免競態(tài)條件,下面給大家分享使用Spring Boot和RedisTemplate實現(xiàn)分布式鎖的案例,感興趣的朋友一起看看吧2024-08-08java ThreadPool線程池的使用,線程池工具類用法說明
這篇文章主要介紹了java ThreadPool線程池的使用,線程池工具類用法說明,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10idea普通javaweb項目如何部署到tomcat(讀取web.xml文件)
這篇文章主要介紹了idea普通javaweb項目如何部署到tomcat(讀取web.xml文件),具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08springboot實現(xiàn)maven多模塊和打包部署
本文主要介紹了springboot實現(xiàn)maven多模塊和打包部署,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04JVM內(nèi)存結(jié)構(gòu)相關(guān)知識解析
這篇文章主要介紹了JVM內(nèi)存結(jié)構(gòu)相關(guān)知識解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-11-11Java反射中java.beans包學(xué)習(xí)總結(jié)
本篇文章通過學(xué)習(xí)Java反射中java.beans包,吧知識點做了總結(jié),并把相關(guān)內(nèi)容做了關(guān)聯(lián),對此有需要的朋友可以學(xué)習(xí)參考下。2018-02-02java 利用反射獲取內(nèi)部類靜態(tài)成員變量的值操作
這篇文章主要介紹了java 利用反射獲取內(nèi)部類靜態(tài)成員變量的值操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12