溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何理解java虛擬機(jī)的基本結(jié)構(gòu)

發(fā)布時(shí)間:2021-09-27 09:43:22 來源:億速云 閱讀:106 作者:柒染 欄目:編程語言

今天就跟大家聊聊有關(guān)如何理解java虛擬機(jī)的基本結(jié)構(gòu),可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

1. java 虛擬機(jī)的架構(gòu)

如何理解java虛擬機(jī)的基本結(jié)構(gòu)

  • 類加載子系統(tǒng):負(fù)責(zé)從文件系統(tǒng)或者網(wǎng)絡(luò)中加載class信息,加載的類信息存放于一塊稱為方法區(qū)的內(nèi)存空間中。除了類的信息,方法區(qū)中可能還會(huì)存放運(yùn)行時(shí)常量池信息,包括字符串字面量和數(shù)字常量(這部分常量信息是class文件中常量池部分的內(nèi)存映射)。

  • java堆:java堆在虛擬機(jī)啟動(dòng)的時(shí)候建立,它是java程序最主要的內(nèi)存工作區(qū)域。幾乎所有的java對(duì)象實(shí)例都存放于java堆中。堆空間是所有線程共享的,這是一塊與java應(yīng)用密切相關(guān)的內(nèi)存區(qū)域。

  • 直接內(nèi)存:java的NIO庫允許程序使用直接內(nèi)存。直接內(nèi)存是在java堆外的、直接向系統(tǒng)申請(qǐng)的內(nèi)存區(qū)域。通常,訪問直接內(nèi)存的速度會(huì)優(yōu)于java堆。因此,出于性能考慮,讀寫頻繁的場(chǎng)合可能會(huì)考慮使用直接內(nèi)存。由于直接內(nèi)存在java堆外,因此,它的大小不會(huì)直接受限于Xmx指定的最大堆大小,但是系統(tǒng)內(nèi)存是有限的,java堆和直接內(nèi)存的總和依然受限于操作系統(tǒng)的最大內(nèi)存。

  • 垃圾回收系統(tǒng):垃圾回收系統(tǒng)是java虛擬機(jī)有重要組成部分,垃圾回收器可以對(duì)方法區(qū)、java堆和直接內(nèi)存進(jìn)行回收。其中,java堆是垃圾收集器的工作重點(diǎn)。和 C/C++ 不同,java中所有的對(duì)象空間釋放都是隱式的。也就是說,java中沒有類似 free() 或者 delete() 這樣的函數(shù)釋放指定的內(nèi)存區(qū)域。對(duì)于不再使用的垃圾對(duì)象,垃圾回收系統(tǒng)會(huì)在后臺(tái)默默工作,默默查找、標(biāo)識(shí)并釋放垃圾對(duì)象,完成包括java堆、方法區(qū)和直接內(nèi)存中的全自動(dòng)化管理。

  • 每一個(gè)java虛擬機(jī)線程都有一個(gè)私有的java棧,一個(gè)線程的java棧在線程創(chuàng)建的時(shí)候被創(chuàng)建,java棧中保存著幀信息,java棧中保存著局部變量、方法參數(shù),同時(shí)和java方法的調(diào)用、返回密切相關(guān)。

  • 本地方法棧和java棧非常類似,最大的不同在于java棧用于方法的調(diào)用,而本地方法棧則用于本地方法的調(diào)用,作為對(duì)java虛擬機(jī)的重要擴(kuò)展,java虛擬機(jī)允許java直接調(diào)用本地方法(通常使用C編寫)

  • PC(Program Counter)寄存器也是每一個(gè)線程私有的空間,java虛擬機(jī)會(huì)為每一個(gè)java線程創(chuàng)建PC寄存器。在任意時(shí)刻,一個(gè)java線程總是在執(zhí)行一個(gè)方法,這個(gè)正在被執(zhí)行的方法稱為當(dāng)前方法。如果當(dāng)前方法不是本地方法,PC寄存器就會(huì)指向當(dāng)前正在被執(zhí)行的指令。如果當(dāng)前方法是本地方法,那么PC寄存器的值就是undefined

  • 執(zhí)行引擎是java虛擬機(jī)的最核心組件之一,它負(fù)責(zé)執(zhí)行虛擬機(jī)的字節(jié)碼,現(xiàn)代虛擬機(jī)為了提高執(zhí)行效率,會(huì)使用即時(shí)編譯技術(shù)將方法編譯成機(jī)器碼后再執(zhí)行。

2. java堆

如何理解java虛擬機(jī)的基本結(jié)構(gòu)

根據(jù)java回收機(jī)制的不同,java堆有可能擁有不同的結(jié)構(gòu)。最為常見的一種構(gòu)成是將整個(gè)java堆分為新生代和老年代。其中新生代存放新生對(duì)象或者年齡不大的對(duì)象,老年代則存放老年對(duì)象。新生代有可能分為eden區(qū)、s0區(qū)、s1區(qū),s0區(qū)和s1區(qū)也被稱為from和to區(qū),他們是兩塊大小相同、可以互換角色的內(nèi)存空間。

3. java棧

java棧是一塊線程私有的內(nèi)存空間。如果說,java堆和程序數(shù)據(jù)密切相關(guān),那么java棧就是和線程執(zhí)行密切相關(guān)。線程執(zhí)行的基本行為是函數(shù)調(diào)用,每次函數(shù)調(diào)用的數(shù)據(jù)都是通過java棧傳遞的。

在java棧中保存的主要內(nèi)容為棧幀。每一次函數(shù)調(diào)用,都會(huì)有一個(gè)對(duì)應(yīng)的棧幀被壓入java棧,每一個(gè)函數(shù)調(diào)用結(jié)束,都會(huì)有一個(gè)棧幀被彈出java棧。如下圖:

如何理解java虛擬機(jī)的基本結(jié)構(gòu)

函數(shù)1對(duì)應(yīng)棧幀1,函數(shù)2對(duì)應(yīng)棧幀2,依次類推。當(dāng)前正在執(zhí)行的函數(shù)所對(duì)應(yīng)的幀就是當(dāng)前幀(位于棧頂),它保存著當(dāng)前函數(shù)的局部變量、中間計(jì)算結(jié)果等數(shù)據(jù)。

當(dāng)函數(shù)返回時(shí),棧幀從java棧中被彈出,java方法區(qū)有兩種返回函數(shù)的方式,一種是正常的函數(shù)返回,使用return指令,另一種是拋出異常。不管使用哪種方式,都會(huì)導(dǎo)致棧幀被彈出。

java虛擬機(jī)提供了參數(shù)-Xss來指定線程的最大棧空間,這個(gè)參數(shù)也直接決定了函數(shù)調(diào)用的最大深度:

private static int count = 0;
public static void recursion() {
    count++;
    recursion();
}

public static void main(String[] args) {
    try{
        recursion();
    } catch (Throwable e) {
        System.out.println("deep of calling =" + count);
        e.printStackTrace();
    }
}

使用-Xss256K參數(shù),結(jié)果如下:

如何理解java虛擬機(jī)的基本結(jié)構(gòu)

如何理解java虛擬機(jī)的基本結(jié)構(gòu)

可以看到,在進(jìn)行大約2900次調(diào)用后,發(fā)生了棧溢出錯(cuò)誤,通過增大-Xss的值,可以獲得更深的調(diào)用層次,嘗試使用參數(shù)-Xss512K,可以看到調(diào)用次數(shù)明顯增加:

如何理解java虛擬機(jī)的基本結(jié)構(gòu)

在一個(gè)棧幀中,至少包含局部變量表、操作數(shù)棧和幀數(shù)據(jù)區(qū)幾個(gè)部分。

1 局部變量表

局部變量表用于保存函數(shù)的參數(shù)以及局部變量。局部亦是表中的變量只在當(dāng)前函數(shù)調(diào)用中有效,當(dāng)函數(shù)調(diào)用結(jié)束后,函數(shù)棧幀銷毀,局部變量表也會(huì)隨之銷毀。

由于局部變量表在棧幀之中,因此,如果函數(shù)的參數(shù)和局部變量較多,會(huì)使局部變量表膨脹,從而每一次函數(shù)調(diào)用就會(huì)占用更多的??臻g,最終導(dǎo)致函數(shù)的嵌套調(diào)用次數(shù)減少。

下面這段代碼,第一個(gè)recursion() 函數(shù)有3個(gè)參數(shù)和10個(gè)局部變量,因此,其局部變量表含有13個(gè)變量,而第2個(gè)recursion()函數(shù)不含有任何參數(shù)和局部變量。當(dāng)這兩個(gè)函數(shù)被嵌套調(diào)用時(shí),第2個(gè)rescursion()函數(shù)可以擁有更深的調(diào)用層次。

 private static int count = 0;

    public static void recursion(long a, long b, long c) {
        long e = 1, f = 2, g = 3, h = 4, i = 5, k = 6, q = 7, x = 8, y = 9, z = 10;
        recursion(a, b, c);
    }
    public static void recursion() {
        count++;
        recursion();
    }

    public static void main(String[] args) {
        try{
            recursion();
        } catch (Throwable e) {
            System.out.println("deep of calling = " + count);
            e.printStackTrace();
        }
    }

使用-Xss256k 執(zhí)行上述代碼中的第1個(gè)rescursion() 函數(shù),結(jié)果如下:

如何理解java虛擬機(jī)的基本結(jié)構(gòu)

使用-Xss256k 執(zhí)行上述代碼中的第2個(gè)rescursion() 函數(shù),結(jié)果如下:

如何理解java虛擬機(jī)的基本結(jié)構(gòu)

可以看到,在相同的棧容量下,局部變量少的函數(shù)可以支持更深層次的函數(shù)調(diào)用。

棧楨中的局部變量表中的槽位是可以重用的,如果局部變量的作用域范圍超過了其作用域,那么在其作用域之后聲明的新的局部變量就很有可能會(huì)復(fù)用局部變量a的槽位,從而達(dá)到節(jié)省資源的目的。局部變量表中的變量也是重要的垃圾回收根節(jié)點(diǎn),被局部變量表中直接或間接引用的對(duì)象都是不會(huì)回收的。

如以下代碼:

public void localVarGc1() {
        byte[] a = new byte[6 * 1024 * 1024];
        System.gc();
    }

    public void localVarGc2() {
        byte[] a = new byte[6 * 1024 * 1024];
        a = null;
        System.gc();
    }

    public void localVarGc3() {
        {
            byte[] a = new byte[6 * 1024 * 1024];
        }
        System.gc();
    }

    public void localVarGc4() {
        {
            byte[] a = new byte[6 * 1024 * 1024];
        }
        int c = 10;
        System.gc();
    }

    public void localVarGc5() {
        localVarGc1();
        System.gc();
    }

    public static void main(String[] args) {
        Demo05 d = new Demo05();
        d.localVarGc1();
        //d.localVarGc2();
        //d.localVarGc3();
        //d.localVarGc4();
        //d.localVarGc5();
    }

上述代碼中,每一個(gè)localVarGcN()函數(shù)都分配了一塊6MB的堆內(nèi)存,并使用局部變量引用這塊空間??梢允褂脜?shù)-XX:+PrintGC 分別執(zhí)行上述函數(shù),在輸出的日志中,可以看到垃圾回收前后堆的大小,進(jìn)而推斷byte數(shù)組是否被回收。

  • localVarGc1()中,在申請(qǐng)空間后,立即進(jìn)行垃圾回收,很多明顯,由于byte數(shù)組被變量a引用,因此無法回收這塊空間。執(zhí)行結(jié)果如下:

[GC (System.gc())  8765K->6664K(251392K), 0.0041586 secs]
[Full GC (System.gc())  6664K->6515K(251392K), 0.0039022 secs]
  • localVarGc2()中,在垃圾回收前,先將變量a置為null,使用byte數(shù)組失去強(qiáng)引用,故垃圾回收可以順利回收byte數(shù)組。執(zhí)行結(jié)果如下:

[GC (System.gc())  8765K->568K(251392K), 0.0012696 secs]
[Full GC (System.gc())  568K->395K(251392K), 0.0039405 secs]
  • 對(duì)于localVarGc3(),在垃圾回收前,先使用局部變量a失效,雖然變量a已經(jīng)離開了作用域,但是變量a依然存在于局部變量表中,并且也指向這塊byte數(shù)組,故byte數(shù)組依然無法被回收。執(zhí)行結(jié)果如下:

[GC (System.gc())  8765K->6696K(251392K), 0.0039619 secs]
[Full GC (System.gc())  6696K->6515K(251392K), 0.0039020 secs]
  • 對(duì)于localVarGc4(),在垃圾回收前,不僅使用變量a失效,更是聲明了變量c,使變量c復(fù)用了變量a的字,由于變量a此時(shí)被銷毀,故垃圾回收器可以順利回收byte數(shù)組。執(zhí)行結(jié)果如下:

[GC (System.gc())  8765K->536K(251392K), 0.0010555 secs]
[Full GC (System.gc())  536K->370K(251392K), 0.0033685 secs]
  • 對(duì)于localVarGc5(),它首先調(diào)用了localVarGC1(),很明顯,在localVarGc1()中并沒有釋放byte數(shù)組,但在localVarGc1()返回后,它的棧楨被銷毀,自然也包含了棧幀中的所有局部變量,故byte數(shù)組失去引用,在localVarGc5()的垃圾回收中被回收。執(zhí)行結(jié)果如下:

[GC (System.gc())  8765K->6744K(251392K), 0.0034826 secs]
[Full GC (System.gc())  6744K->6539K(251392K), 0.0045563 secs]
[GC (System.gc())  6539K->6539K(251392K), 0.0007713 secs]
[Full GC (System.gc())  6539K->395K(251392K), 0.0032212 secs]
2. 操作數(shù)棧

操作數(shù)棧主要用于保存計(jì)算過程的中間結(jié)果,同事作為計(jì)算過程中變量臨時(shí)的存儲(chǔ)空間。操作數(shù)棧也是一個(gè)先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu),只支持入棧和出棧兩種操作。

3. 幀數(shù)據(jù)區(qū)

幀數(shù)據(jù)區(qū)時(shí)候?yàn)榱酥С殖A砍亟馕?、正常方法返回和異常處理等。大部分Java字節(jié)碼指令需要進(jìn)行常量池訪問,在幀數(shù)據(jù)區(qū)中保存著訪問常量池的指針,方便程序訪問常量池。

提示:由于每次函數(shù)調(diào)用都會(huì)產(chǎn)生對(duì)應(yīng)的棧幀,從而占用一定的??臻g,因此,如果??臻g不足,那么函數(shù)調(diào)用自然無法繼續(xù)進(jìn)行下去。當(dāng)請(qǐng)求的棧深度大于最大可用棧深度時(shí),系統(tǒng)會(huì)拋出StackOverflowError棧溢出錯(cuò)誤。 舉個(gè)例子:

4. 棧上分配

棧上分配是Java虛擬機(jī)提供的一項(xiàng)優(yōu)化技術(shù),它的基本思想是:對(duì)于那些線程私有的對(duì)象(這里指不可能被其他線程訪問的對(duì)象),可以將它們打散分配在棧上,而不是分配在堆上。分配在棧上的好處是可以在函數(shù)調(diào)用結(jié)束后自行銷毀,而不需要垃圾回收器的介入,從而提高系統(tǒng)的性能。

棧上分配的以及技術(shù)基礎(chǔ)是進(jìn)行逃逸分析。逃逸分析的目的是判斷對(duì)象的作用域是否有可能逃逸出函數(shù)體。

下面這個(gè)簡(jiǎn)單示例顯示了對(duì)非逃逸對(duì)象的棧上分配:

public static class User {
    public int  id;
    public String name = "";
}

public static void alloc() {
    User u = new User();
    u.id = 5;
    u.name = "geym0909";
}

public static void main(String[] args) {
    long b = System.currentTimeMillis();
    for(int i = 0; i < 10_0000_0000; i++) {
        alloc();
    }
    long e = System.currentTimeMillis();
    System.out.println(e - b);
}

上述代碼在主函數(shù)中進(jìn)行了1億次alloc()調(diào)用來創(chuàng)建對(duì)象,由于User對(duì)象實(shí)例需要占用約16字節(jié)的空間,因此累計(jì)分配空間將近1.5GB。如果堆空間小于這個(gè)值,就必然會(huì)發(fā)生GC。使用如下參數(shù)運(yùn)行上述代碼:

-server -Xmx10m -Xms10m -XX:+PrintGC -XX:+DoEscapeAnalysis -XX:-UseTLAB -XX:+EliminateAllocations
  • 這里使用參數(shù)-server執(zhí)行程序,因?yàn)樵赟erver模式下,才可以啟用逃逸分析。

  • 參數(shù)-XX:+DoEscapeAnalysis啟用逃逸分析。

  • -Xms10m、-Xmx10m指定了最大與最小堆空間都是10m

  • -XX:+PrintGC將打印GC日志

  • -XX:+EliminateAllocations 開啟了標(biāo)量替換(默認(rèn)打開),允許將對(duì)象打散分配在棧上,比如對(duì)象擁有id與name兩個(gè)字段,那么這兩個(gè)字段將會(huì)被視為兩個(gè)獨(dú)立的局部變量進(jìn)行分配。

  • -XX:-UseTLAB關(guān)閉TLAB

程序執(zhí)行后,結(jié)果如下:

如何理解java虛擬機(jī)的基本結(jié)構(gòu)

注:在本人機(jī)器上,使用如下參數(shù)(即不指定任何棧上分配相關(guān)的參數(shù)),結(jié)果依然無大量gc日志:

-server -Xmx10m -Xms10m -XX:+PrintGC

如何理解java虛擬機(jī)的基本結(jié)構(gòu)

再關(guān)閉逃逸分析,則結(jié)果如下:

-server -Xmx10m -Xms10m -XX:+PrintGC -XX:-DoEscapeAnalysis

如何理解java虛擬機(jī)的基本結(jié)構(gòu)

可見,在本人機(jī)器上逃逸分析、棧上分配是默認(rèn)開啟的。

對(duì)于大量的零散小對(duì)象,棧上分配提供了一種很好的對(duì)象分配優(yōu)化策略,棧上分配速度快,并且可以有效避免垃圾回收帶來的負(fù)面影響,但由于和堆空間相比,??臻g較小,因此,大對(duì)象無法也不適用在棧上分配。

5. 方法區(qū)

和堆一樣,方法區(qū)是一塊所有線程共享的內(nèi)存區(qū)域,它用于保存系統(tǒng)的類信息,比如類的字段、方法、常量池等。方法區(qū)的大小決定了系統(tǒng)可以保存多少個(gè)類,如果系統(tǒng)定義了太多的類,導(dǎo)致方法區(qū)的溢出,虛擬機(jī)同樣會(huì)拋出內(nèi)存溢出錯(cuò)誤。

在JDK1.6、JDK1.7中,方法區(qū)可以理解為永久區(qū)(Perm)。永久區(qū)可以使用參數(shù) -XX:PermSize-XX:MaxPermSize 指定,默認(rèn)情況下,-XX:MaxPermSize 為64M。一個(gè)大的永久區(qū)可以保存更多的類信息。如果系統(tǒng)使用了一些動(dòng)態(tài)代理,那么有可能會(huì)在運(yùn)行時(shí)生成大量的類,如果這樣,就需要設(shè)置一個(gè)合理的永久區(qū)大小,確保不發(fā)生永久區(qū)內(nèi)存溢出。

在JDK1.8中,永久區(qū)已經(jīng)被徹底移除,取而代之的是元數(shù)據(jù)區(qū),元數(shù)據(jù)區(qū)大小可以使用參數(shù) -XX:MaxMetaspaceSize 指定(一個(gè)大的元數(shù)據(jù)區(qū)可以使系統(tǒng)支持更多的類),這是一塊堆外的直接內(nèi)存。與永久區(qū)不同,如果不指定大小,默認(rèn)情況下,虛擬機(jī)會(huì)耗盡所有的可用系統(tǒng)內(nèi)存。

如果元數(shù)據(jù)區(qū)發(fā)生異常,虛擬機(jī)一樣會(huì)拋出異常。

4. java虛擬機(jī)參數(shù)總結(jié)

  • -server:使用server模式啟動(dòng)jvm,對(duì)應(yīng)也有-client,使用client模式啟動(dòng)jvm。對(duì)于server模式,jvm啟動(dòng)較慢,因?yàn)閖vm會(huì)收集系統(tǒng)信息并進(jìn)行優(yōu)化在提高程序的運(yùn)行效率;對(duì)于client模式,jvm啟動(dòng)較快,但由于沒有收集運(yùn)行時(shí)的信息導(dǎo)致優(yōu)化不足,后期運(yùn)行效率可能會(huì)降低。

  • 參數(shù)-XX:+DoEscapeAnalysis啟用逃逸分析。

  • -Xms10m、-Xmx10m指定了最大與最小堆空間都是10m

  • -Xss256k:指定棧大小為256k

  • -XX:+PrintGC將打印GC日志

  • -XX:+EliminateAllocations 開啟了標(biāo)量替換(默認(rèn)打開),允許將對(duì)象打散分配在棧上,比如對(duì)象擁有id與name兩個(gè)字段,那么這兩個(gè)字段將會(huì)被視為兩個(gè)獨(dú)立的局部變量進(jìn)行分配。

  • -XX:-UseTLAB關(guān)閉TLAB

  • -XX:PermSize-XX:MaxPermSize:在JDK1.6、JDK1.7中,方法區(qū)可以理解為永久區(qū)(Perm)。永久區(qū)可以使用參數(shù) -XX:PermSize-XX:MaxPermSize 指定。默認(rèn)情況下,-XX:MaxPermSize 為64M。

  • -XX:MaxMetaspaceSize:在JDK1.8中,永久區(qū)已經(jīng)被徹底移除,取而代之的是元數(shù)據(jù)區(qū),元數(shù)據(jù)區(qū)大小可以使用參數(shù) -XX:MaxMetaspaceSize 指定。這是一塊堆外的直接內(nèi)存。與永久區(qū)不同,如果不指定大小,默認(rèn)情況下,虛擬機(jī)會(huì)耗盡所有的可用系統(tǒng)內(nèi)存。


看完上述內(nèi)容,你們對(duì)如何理解java虛擬機(jī)的基本結(jié)構(gòu)有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI