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

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

一、前言
Java虛擬機(jī),簡(jiǎn)稱JVM(Java Virtual Machine),是Java語(yǔ)言中最為核心的一個(gè)東西,Java程序運(yùn)行離不開它,因?yàn)樗拇嬖冢沟肑ava擁有“一次編譯,多次運(yùn)行”的特點(diǎn)。任何平臺(tái)只要裝有針對(duì)于該平臺(tái)的Java虛擬機(jī),字節(jié)碼文件(.class)就可以在該平臺(tái)上運(yùn)行。

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

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

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

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

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

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

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

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

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

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

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

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

以下代碼片段中,通過逐步設(shè)置遞歸方法調(diào)用的深度,將會(huì)拋出棧溢出異常(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虛擬機(jī)規(guī)范中允許棧的大小是動(dòng)態(tài)的或者是固定的,定義了兩種異常與??臻g相關(guān):StackOverflowError和OutOfMemoryError。如果線程在計(jì)算過程中,請(qǐng)求的棧深度大于最大可用的棧深度,則會(huì)拋出StackOverflowError異常,如果棧能夠動(dòng)態(tài)擴(kuò)展,而在擴(kuò)展過程中,沒有足夠的內(nèi)存空間來(lái)支持棧的擴(kuò)展,則會(huì)拋出OutOfMemoryError異常。

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

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

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

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

以上述代碼為例進(jìn)行說明,如下圖所示,在idea中通過jclasslib插件查看StackTest.class文件,展開方法recursionMethod后,查看Code屬性的Misc頁(yè)簽中,當(dāng)前方法的最大局部變量表的容量為2。因?yàn)樵谠摲椒ㄖ兄挥幸粋€(gè)int類型的參數(shù),所以共占2字。
 

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

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

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

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

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

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

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

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

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

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

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

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

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