Java 虛擬機(jī)內(nèi)存模型—線程私有

JVM 內(nèi)存模型

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ū)域組成。

  1. 線程私有區(qū)域:
  • 虛擬機(jī)棧
  • 本地方法棧
  • 程序計(jì)數(shù)器

2.線程共享區(qū)域:

  • 方法區(qū)

下面繪制一個(gè)草圖來描述 JVM 運(yùn)行數(shù)據(jù)區(qū)的組成:

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é)碼:

程序計(jì)數(shù)器

上圖紅色標(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)系:

虛擬機(jī)棧-棧幀

局部變量表:

存放方法內(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();
    }
}
StackOverflowError

虛擬機(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日

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容