JVM 內(nèi)存模型

.java 源文件 -> javac 工具編譯 -> .class 文件 -> JVM 解析 -> 010101 機(jī)器碼 -跑在不同的操作系統(tǒng)上。
基于上面的流程可以看出,java 是一個(gè)跨平臺(tái)語言。
本節(jié)來分析 Java 對(duì)象如何進(jìn)行分配和回收。
JVM 運(yùn)行時(shí)數(shù)據(jù)區(qū)主要由線程私有區(qū)域和線程共享區(qū)域組成。
- 線程私有區(qū)域:
- 虛擬機(jī)棧
- 本地方法棧
- 程序計(jì)數(shù)器
2.線程共享區(qū)域:
- 堆
- 方法區(qū)
下面繪制一個(gè)草圖來描述 JVM 運(yùn)行數(shù)據(jù)區(qū)的組成:

線程私有區(qū)域
線程私有區(qū)域組成為:
- 程序計(jì)數(shù)器
- 虛擬機(jī)棧
- 本地方法棧
程序計(jì)數(shù)器
什么是程序計(jì)數(shù)器呢?
因?yàn)?Java 本身就是一個(gè)多線程的,每一個(gè)線程都有一個(gè)程序計(jì)數(shù)器, CPU 在對(duì)線程上下文切換時(shí),會(huì)使用程序計(jì)數(shù)器記錄下當(dāng)前線程正在執(zhí)行的字節(jié)碼指令的地址(行號(hào)),這樣線程再次回來工作時(shí),就知道執(zhí)行到哪個(gè)位置了。
為了更加深入的理解程序計(jì)數(shù)器,下面來看這樣一段代碼:

通過 javap -verbose JMMDemo.class 得到對(duì)應(yīng)字節(jié)碼:

上圖紅色標(biāo)出的 Code 對(duì)應(yīng)的這些數(shù)就是程序計(jì)數(shù)器了。
虛擬機(jī)棧
在了解虛擬機(jī)棧之前,先來看看棧這個(gè)概念
棧是一種數(shù)據(jù)結(jié)構(gòu),入口只有一個(gè)。
棧的特點(diǎn):FILO,也就是先進(jìn)后出。
面試題:為什么虛擬機(jī)需要使用棧?
非常符合JAVA中方法間的調(diào)用。
例如以下方法的調(diào)用過程,就是方法的入棧和出棧過程。
private void methodA(){
methodB();
println("methodA");
}
private void methodB() {
methodC();
println("methodB");
}
private void methodC() {
println("methodC");
}
虛擬棧也是屬于線程私有部分,在線程內(nèi)部中一般會(huì)調(diào)用很多方法,而每一個(gè)方法使用一個(gè)棧幀來描述。
下面用一個(gè)草圖來描述一下棧幀與虛擬機(jī)棧的關(guān)系:
虛擬機(jī)棧是由多個(gè)棧幀組成,每調(diào)用一個(gè)方法就相當(dāng)于有一個(gè)棧幀入棧到虛擬機(jī)棧中。

棧幀的組成
在前面描述過,在線程中,一個(gè)方法被調(diào)用就會(huì)一個(gè)棧幀被壓入虛擬機(jī)棧中。棧幀就是用來描述這個(gè)方法,一個(gè)棧幀是由局部變量表,操作數(shù)棧,返回值地址,動(dòng)態(tài)鏈接組成。
下面還是回到上面示例,結(jié)合草圖,看它們之間的關(guān)系:

局部變量表:
存放方法內(nèi)部變量表
32位地址,尋址空間為 4G 。如果需要存放64位的數(shù)據(jù),需要使用高位和地位表示。
下面是 method() 生成的局部變量表:

- this //表示當(dāng)前對(duì)象
- o //new Object()
- count //int count = 0;
操作數(shù)棧:
對(duì)局部變量表中的變量進(jìn)行出棧入棧的操作。
返回值地址:
一個(gè)方法被執(zhí)行之后,有一個(gè)返回值,返回給對(duì)應(yīng)的調(diào)用處。
動(dòng)態(tài)鏈接:
主要對(duì)應(yīng)的多態(tài),只有代碼執(zhí)行時(shí)才知道具體的實(shí)現(xiàn)類是那個(gè)對(duì)象。
StackOverflowError
這個(gè)異常想必很多人都遇過,字面意思就是棧溢出。我們通過上面的分析我們知道,虛擬機(jī)棧如果不斷出現(xiàn)棧幀入棧,當(dāng)虛擬機(jī)??臻g達(dá)到上限,那么就會(huì)出現(xiàn) StackOverflowError。
下面來模擬這個(gè)錯(cuò)誤的產(chǎn)生:
public class StackOverflowError {
private static int count = 0;
public static void main(String[] args) {
try {
recursion();
} catch (Throwable e) {
System.out.println("deep of calling = " + count);
e.printStackTrace();
}
}
public static void recursion() {
count++;
recursion();
}
}

虛擬機(jī)棧空間大小可以通過
-Xss來設(shè)置,例如-Xss164K,缺省情況下是 1m 。
如果是死循環(huán)出現(xiàn)這樣的錯(cuò)誤StackOverflowError,那么通過 -Xss 參數(shù)的設(shè)置也是沒有用。
本地方法棧
虛擬機(jī)棧對(duì)應(yīng)的方法是 Java 方法,而本地方法棧對(duì)應(yīng)的是 native 方法。其他方面應(yīng)該和虛擬機(jī)棧差不多。
虛擬機(jī)規(guī)范無強(qiáng)制規(guī)定,各版本虛擬機(jī)自由實(shí)現(xiàn),HotSpot直接把本地方法棧和虛擬機(jī)棧合二為一,當(dāng)一個(gè) JVM 創(chuàng)建的線程調(diào)用 native 方法后,JVM 不再為其在虛擬機(jī)棧中創(chuàng)建棧幀,JVM 只是簡(jiǎn)單地動(dòng)態(tài)鏈接并直接調(diào)用native方法
逃逸分析優(yōu)化
逃逸分析的基本行為就是分析對(duì)象動(dòng)態(tài)作用域:當(dāng)一個(gè)對(duì)象在方法中被定義后,它可能被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其他地方中,稱為方法逃逸。
public static StringBuffer craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
public static String createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
第一段代碼中的sb就逃逸了,而第二段代碼中的sb就沒有逃逸。
默認(rèn)情況下,java 虛擬機(jī)是開啟逃逸分析的選項(xiàng) -XX:+DoEscapeAnalysis
public class EscapeDemo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
generate();
}
System.out.println((System.currentTimeMillis() - start));
}
private static void generate() {
User user = new User();//開始-XX:+DoEscapeAnalysis之后,該對(duì)象是在棧上分配
user.age = 10;
user.name = "hello";
}
private static class User{
public String name;
public int age;
}
}
開啟逃逸分析選項(xiàng)的輸出結(jié)果:
創(chuàng)建1億個(gè)對(duì)象的時(shí)間:6
從輸出結(jié)果來看創(chuàng)建1億個(gè) User 對(duì)象的時(shí)間是非常短的,因?yàn)镴ava 虛擬機(jī)默認(rèn)開始逃逸分析的選項(xiàng)。
現(xiàn)在將逃逸分析選項(xiàng)關(guān)閉,并在控制臺(tái)輸出打印結(jié)果,同時(shí)開啟 gc 日志。
-XX:-DoEscapeAnalysis -XX:+PrintGC
[GC (Allocation Failure) 65536K->680K(251392K), 0.0007842 secs]
[GC (Allocation Failure) 66216K->696K(251392K), 0.0007729 secs]
[GC (Allocation Failure) 66232K->648K(251392K), 0.0005273 secs]
[GC (Allocation Failure) 66184K->608K(316928K), 0.0006086 secs]
[GC (Allocation Failure) 131680K->688K(316928K), 0.0021467 secs]
[GC (Allocation Failure) 131760K->624K(438272K), 0.0007702 secs]
[GC (Allocation Failure) 262768K->529K(438272K), 0.0011675 secs]
[GC (Allocation Failure) 262673K->529K(700416K), 0.0004699 secs]
[GC (Allocation Failure) 524817K->529K(700416K), 0.0008970 secs]
[GC (Allocation Failure) 524817K->529K(1015296K), 0.0003706 secs]
創(chuàng)建1億個(gè)對(duì)象的時(shí)間:717
兩者在創(chuàng)建1億個(gè)對(duì)象的時(shí)間對(duì)比是幾百倍的差距。
線程私有部分的回收問題
線程私有部分的內(nèi)存空間是隨線程產(chǎn)生而產(chǎn)生,隨線程死亡而自動(dòng)釋放的。所以不需要像堆空間那樣過多的考慮內(nèi)存釋放問題。
參考
https://blog.csdn.net/w372426096/article/details/80938788
本文是筆者學(xué)習(xí)之后的總結(jié),方便日后查看學(xué)習(xí),有任何不對(duì)的地方請(qǐng)指正。
記錄于2019年4月15日