Java虛擬機內(nèi)存模型

作者:xcbeyond
瘋狂源自夢想,技術(shù)成就輝煌!微信公眾號:《程序猿技術(shù)大咖》號主,專注后端開發(fā)多年,擁有豐富的研發(fā)經(jīng)驗,樂于技術(shù)輸出、分享,現(xiàn)階段從事微服務(wù)架構(gòu)項目的研發(fā)工作,涉及架構(gòu)設(shè)計、技術(shù)選型、業(yè)務(wù)研發(fā)等工作。對于Java、微服務(wù)、數(shù)據(jù)庫、Docker有深入了解,并有大量的調(diào)優(yōu)經(jīng)驗。

一、前言
Java虛擬機,簡稱JVM(Java Virtual Machine),是Java語言中最為核心的一個東西,Java程序運行離不開它,因為它的存在,使得Java擁有“一次編譯,多次運行”的特點。任何平臺只要裝有針對于該平臺的Java虛擬機,字節(jié)碼文件(.class)就可以在該平臺上運行。

JVM是Java中最難以理解、而且非常重要的知識點,也常常用來衡量一個人Java基本功是否牢靠,更是在面試中被問及最多、最頻繁的知識點之一。本文將從Java虛擬機內(nèi)存模型開始入手,一步步來了解它。

Java虛擬機內(nèi)存模型是Java程序運行的基礎(chǔ),為了使Java應(yīng)用程序正常運行,JVM將其內(nèi)存數(shù)據(jù)分為程序計數(shù)器、虛擬機棧、本地方法棧、堆和方法區(qū),如下圖所示:

(在JDK1.8開始,已經(jīng)去掉了方法區(qū)的概念,用元空間(Metaspace)進行了代替.)

程序計數(shù)器用于存放下一條運行的指令;虛擬機棧和本地方法棧用于存放函數(shù)方法調(diào)用堆棧信息;Java堆用于存放Java程序運行時所需的對象等數(shù)據(jù);方法區(qū)用于存放程序的元數(shù)據(jù)信息。

其中,一部分是線程私有的,而另一部分卻是線程共享的。

線程私有:程序計數(shù)器、虛擬機棧、本地方法棧
線程共享:堆、方法區(qū)
二、程序計數(shù)器
程序計數(shù)器是一塊很小的內(nèi)存空間,用于存放下一條運行的指令,它是線程私有的,可以認作為當(dāng)前線程的行號指示器。

由于Java是支持線程的語言,當(dāng)線程數(shù)量超過CPU數(shù)量時,線程之間根據(jù)時間片輪詢搶奪CPU資源。對于單核CPU而言,每一時刻,只能有一個線程在運行,而其他線程必須被切換出去。為此,每一個線程都必須用一個獨立的程序計數(shù)器,來記錄下一條要運行的指令。各個線程之間的計數(shù)器互不影響,獨立工作。

如果當(dāng)線程正在執(zhí)行一個Java方法,則程序計數(shù)器記錄正在執(zhí)行的Java字節(jié)碼地址,如果當(dāng)前線程正在執(zhí)行一個Native方法,則程序計數(shù)器為空。

三、虛擬機棧(棧)
棧保存的是方法的局部變量、部分結(jié)果,并參與方法的調(diào)用和返回,即:棧幀數(shù)據(jù)。

1.棧幀
每個方法被執(zhí)行的時候都會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接方法、返回地址等信息。每一個方法被調(diào)用的過程就對應(yīng)一個棧幀在虛擬機棧中從入棧(方法調(diào)用)到出棧(方法返回)的過程。

棧幀結(jié)構(gòu)如下圖所示:
 

如果方法調(diào)用時,方法的參數(shù)和局部變量相對較多,那么棧幀中局部變量表就會比較大,棧幀就很很大,因此,單個方法調(diào)用所需的??臻g大小也會很大。(在程序開發(fā)時,盡量避免這種情況,尤其是遞歸方法中要避免遞歸調(diào)用的深度)

以下代碼片段中,通過逐步設(shè)置遞歸方法調(diào)用的深度,將會拋出棧溢出異常(StackOverflowError)。






public class StackTest {
    // 遞歸次數(shù)
    private final int count = 100000;

    /**
     * 遞歸方法
     * @param num
     */
    public void recursionMethod(int num) {
        num++;
        if (num < count) {
            recursionMethod(num);
        }
    }

    @Test
    public void stackDepthTest() {
        recursionMethod(0);
    }
}

2.棧溢出、內(nèi)存溢出
Java虛擬機規(guī)范中允許棧的大小是動態(tài)的或者是固定的,定義了兩種異常與??臻g相關(guān):StackOverflowError和OutOfMemoryError。如果線程在計算過程中,請求的棧深度大于最大可用的棧深度,則會拋出StackOverflowError異常,如果棧能夠動態(tài)擴展,而在擴展過程中,沒有足夠的內(nèi)存空間來支持棧的擴展,則會拋出OutOfMemoryError異常。

其中,可以使用JVM參數(shù)-Xss來調(diào)整設(shè)置棧的大小,從而決定了方法調(diào)用可以達到的深度。

針對上述代碼StackTest中,在遞歸次數(shù)為100000時,將-Xss參數(shù)調(diào)整為-Xss512M后,未拋出異常。
 

3.jclasslib工具
篇外話,但覺得還是有必要提出來,在研究JVM時,總是會去研究一些字節(jié)碼指令、Class類文件結(jié)構(gòu)、大小等數(shù)據(jù),而jclasslib工具恰恰滿足這些,有了它更有助于我們對Java、JVM有更深入的了解。

大家可根據(jù)自己的喜好,選擇安裝,有單機軟件版、IDE插件可供使用,在此,我選擇的是在idea中安裝了jclasslib插件,方便使用。此工具將伴隨著你在JVM的世界里翱翔,一探JVM究竟。

以上述代碼為例進行說明,如下圖所示,在idea中通過jclasslib插件查看StackTest.class文件,展開方法recursionMethod后,查看Code屬性的Misc頁簽中,當(dāng)前方法的最大局部變量表的容量為2。因為在該方法中只有一個int類型的參數(shù),所以共占2字。
 

關(guān)于jclasslib工具的更多使用技巧,在不斷的使用中去摸索吧。

四、本地方法棧
本地方法棧和虛擬機棧的功能很相似,虛擬機棧用于管理Java方法的調(diào)用,而本地方法棧用于管理本地方法的調(diào)用。

本地方法并不是用Java實現(xiàn)的,而是使用C實現(xiàn)的。本地方法棧保存的是native方法的信息,當(dāng)一個JVM創(chuàng)建的線程調(diào)用native方法后,JVM不再為其在虛擬機棧中創(chuàng)建棧幀,JVM只是簡單地動態(tài)鏈接并直接調(diào)用native方法。

在Hot Spot虛擬機中,是不區(qū)分本地方法棧和虛擬機棧的。因此,本地方法棧一樣也會拋出異常StackOverflowError和OutOfMemoryError。

五、堆
堆可以說是Java運行時內(nèi)存中最為重要的部分,幾乎所有的對象和數(shù)組都是在堆中分配空間的。堆分為新生代和老年代兩部分,新生代用于存放剛剛產(chǎn)生的對象和年輕的對象,如果對象一直沒有被收回,生存得足夠長,老年對象就會被移入老年代。

新生代又可以進一步細分為eden、survivor space0(s0或者from space)和survivor space1(s1或者to space)。eden稱之為伊甸園,即對象的出生地,大部分對象剛剛創(chuàng)建時,通常會存放在這里。s0和s1為survivor空間,直譯為幸存者,就是指存放其中的對象至少經(jīng)歷了一次垃圾回收,并得以幸存。如果在幸存區(qū)的對象到了指定年齡仍未被回收,則有機會進入老年代。

換言之,堆空間簡單分為新生代和老年代,新生代用于存放剛產(chǎn)生的新對象,老年代則存放年長的對象(存放時間較長,經(jīng)過垃圾回收次數(shù)較多的對象)。

堆空間結(jié)構(gòu)如下圖所示:
 

六、方法區(qū)
方法區(qū)同堆一樣,是所有線程共享的內(nèi)存區(qū)域,為了區(qū)分堆,又被稱為非堆。主要保存的信息是類的元數(shù)據(jù),即類的類型信息、常量池、域信息、方法信息,如static修飾的變量加載類的時候就被加載到方法區(qū)中。

類型信息包括類的完整名稱、父類的完整名稱、類型修飾符(public/protected/private)和類型的直接接口類表;常量池包括這個類方法、域等信息所引用的常量信息;域信息包括域名稱、域類型和域修飾符;方法信息包括方法名稱、返回類型、方法參數(shù)、方法修飾符、方法字節(jié)碼、操作數(shù)棧和方法幀棧的局部變量區(qū)大小以及異常表??傊?,方法區(qū)內(nèi)保存的信息,大部分都來自于class文件。

在Hot Spot虛擬機中,方法區(qū)也成為永久區(qū),是一塊獨立于Java堆的內(nèi)存空間。雖然叫做永久區(qū),但是永久區(qū)中的對象,同樣也是可以被GC回收的。只是對于GC的表現(xiàn)和Java堆空間略不相同。對永久區(qū)GC的回收,通常主要從兩個方面分析:一是GC對永久區(qū)常量池的回收,二是永久區(qū)對類元數(shù)據(jù)的回收。

方法區(qū)也成為永久區(qū),主要存放常量和類的定義信息。

(在JDK1.8的HotSpot虛擬機中,已經(jīng)去掉了方法區(qū)的概念,用 Metaspace代替,并且將其移到了本地內(nèi)存來規(guī)劃了。)