從Java虛擬機規(guī)范看HotSpot虛擬機的內(nèi)存結(jié)構(gòu)和變遷
點擊上方藍(lán)色字體,選擇“標(biāo)星公眾號”
優(yōu)質(zhì)文章,第一時間送達(dá)
作者 | Yuchao_Huang
來源 | urlify.cn/aM7Jbu
引言
網(wǎng)上有大量討論JVM的內(nèi)存模型的文章,但很多內(nèi)容都是到處摘抄而來,導(dǎo)致許多概念模糊不清。
比如對于“JVM內(nèi)存模型”和“Java內(nèi)存模型(JMM)”沒有區(qū)分,實際上,Java內(nèi)存模型(JMM)是一種規(guī)范,和具體的Java虛擬機的內(nèi)存結(jié)構(gòu)不是一個概念,不應(yīng)該把諸如“年輕代“、”老年代”這類關(guān)于虛擬機具體實現(xiàn)的討論歸為Java內(nèi)存模型。
而在具體討論JVM的內(nèi)存結(jié)構(gòu)時,還應(yīng)該指出,我們通常討論的都是HotSpot虛擬機中的實現(xiàn),這些模型并不是所有虛擬機通用的,比如“Perm Gen(永久代)”就是HotSpot中的概念,JRockit中并沒有永久代。
此外,不應(yīng)該把“永久代”和“方法區(qū)”混為一談,永久代(Perm Gen)只是HotSpot對于Java虛擬機規(guī)范中方法區(qū)(Method Area)的一種實現(xiàn),后來被改成了元空間(MetaSpace),文中會具體介紹這些變化。
本文希望從Java虛擬機規(guī)范出發(fā),盡可能通過查閱官方文檔,以及閱讀HotSpot VM中的部分核心源代碼的方式,重新梳理Java虛擬機的內(nèi)存結(jié)構(gòu),重點討論:
HotSpot虛擬機中,Heap(堆),Method Area(方法區(qū))和Run-Time Constant Pool(運行時常量池)的關(guān)系
Method Area的在JDK1.6,JDK1.7和JDK1.8中的變遷(Perm Gen的消失和MetaSpace的出現(xiàn))
字符串常量池的轉(zhuǎn)移以及運行時常量池和intern方法的變化等。
而Jvm中的The pc Register、Java Virtual Machine Stacks和Native Method Stacks這些部分,則不在本文的討論范圍之內(nèi)。
Java虛擬機規(guī)范中的內(nèi)存模型
Java虛擬機規(guī)范上指定了Java虛擬機的運行時數(shù)據(jù)區(qū)包括The pc Register、Java Virtual Machine Stacks、 Heap、 Method Area、Run-Time Constant Pool和Native Method Stacks這些部分,其中PC寄存器,Java虛擬機棧和本地方法棧會為每個線程所創(chuàng)建,屬于線程私有,而堆,方法區(qū)和運行時常量池是所有線程共享的。
PC寄存器,Java虛擬機棧和本地方法棧的作用與傳統(tǒng)的操作系統(tǒng)類似,這里不多贅述,我們主要關(guān)注Heap(堆),Method Area(方法區(qū))和Run-Time Constant Pool(運行時常量池)的規(guī)范。
Heap(堆)
首先查看Java虛擬機中對Heap的定義:
The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.
上面的定義指出,Heap是Java虛擬機中為所有Java虛擬機線程所共享的內(nèi)存區(qū)域,它是一塊為所有對象和數(shù)組分配內(nèi)存的運行時數(shù)據(jù)區(qū)。
規(guī)范中還有下面的一段描述:
The heap is created on virtual machine start-up. Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector); objects are never explicitly deallocated. The Java Virtual Machine assumes no particular type of automatic storage management system, and the storage management technique may be chosen according to the implementor's system requirements. The heap may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger heap becomes unnecessary. The memory for the heap does not need to be contiguous.
文字有點長,不過我們可以總結(jié)出幾個有關(guān)Heap的要點:
Heap是在Java虛擬機啟動時創(chuàng)建的
Heap中的對象占用的空間由自動存儲管理系統(tǒng)所回收(其實就是GC),對象不能被顯式回收
自動存儲管理系統(tǒng)(垃圾收集器)沒有統(tǒng)一的實現(xiàn),由虛擬機的實現(xiàn)者來選擇
Heap的空間大小可以是固定的,也可以進(jìn)行擴充和收縮,Heap不需要連續(xù)的內(nèi)存空間
看完Java虛擬機規(guī)范中對Heap的描述,我們最需要記住的一點是:Heap是一塊為所有對象和數(shù)組分配內(nèi)存的運行時數(shù)據(jù)區(qū)
Method Area(方法區(qū))
首先看Java虛擬機規(guī)范中對Method Area的定義:
The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization
上面一段文字的含義是,Method Area(方法區(qū))是Java虛擬機中為所有Java虛擬機線程所共享的內(nèi)存區(qū)域,它類似于傳統(tǒng)語言中存儲編譯后代碼的區(qū)域,或者可以說它類似于操作系統(tǒng)進(jìn)程中的'text'段(代碼段,在操作系統(tǒng)中內(nèi)存會分為數(shù)據(jù)段,代碼段,堆,棧和BBS段)。
Method Area用于保存每個類的結(jié)構(gòu)信息,如運行時常量池、字段和方法數(shù)據(jù)、以及方法和構(gòu)造器的代碼,包括用于類,對象和接口初始化的特殊方法。
下面還有一段描述,看起來跟對Heap的描述很像:
The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.
上面的文字有以下要點:
Method Area是在Java虛擬機啟動時創(chuàng)建的
Method Area在邏輯上是Heap的一部分,但可以選擇不對它進(jìn)行垃圾收集
Java虛擬機規(guī)范不強制規(guī)定Method Area的存儲位置和管理已編譯代碼的策略
Heap的空間大小可以是固定的,也可以進(jìn)行擴充和收縮,Heap不需要連續(xù)的內(nèi)存空間
從Java虛擬機規(guī)范的描述可以看出,規(guī)范對Method Area的定義是比較寬泛,只是定義了一塊內(nèi)存區(qū)域,用于存儲類的結(jié)構(gòu)信息。它沒有嚴(yán)格定義Method Area在內(nèi)存中的位置,也沒有規(guī)定對它進(jìn)行垃圾回收等管理策略。
因此,我們不應(yīng)該認(rèn)為Method Area和Heap是完全割裂的兩塊內(nèi)存區(qū)域,它甚至可以是Heap的一部分。
Run-Time Constant Pool(運行時常量池)
老樣子,先看定義:
A run-time constant pool is a per-class or per-interface run-time representation of the
constant_pooltable in aclassfile (§4.4). It contains several kinds of constants, ranging from numeric literals known at compile-time to method and field references that must be resolved at run-time. The run-time constant pool serves a function similar to that of a symbol table for a conventional programming language, although it contains a wider range of data than a typical symbol table.
上面的第一句很重要:運行時常量池是每個類/接口的字節(jié)碼文件中constant_pool table的運行時實現(xiàn)。意思就是,每個類/接口都會擁有一個和字節(jié)碼中的常量池對應(yīng)的運行時常量池。
它包含了各種常量,包括編譯時已知的數(shù)值字面量、運行時解析的方法和字段引用。
Run-Time Constant Pool的功能和傳統(tǒng)編程語言的符號表類似,不過它包含的符號類型更廣。
第二段照舊有一段描述:
Each run-time constant pool is allocated from the Java Virtual Machine's method area (§2.5.4). The run-time constant pool for a class or interface is constructed when the class or interface is created (§5.3) by the Java Virtual Machine.
這里有個重要信息:每個運行時常量池都是從Java虛擬機的Method Area中分配的,它和class/interface一起被Java虛擬機所創(chuàng)建。
這說明了Run-Time Constant Pool是Method Area的一部分,這和Method Area中的描述是相符的。
小結(jié)HotSpot VM的內(nèi)存結(jié)構(gòu)
簡單總結(jié)一下上面虛擬機規(guī)范的內(nèi)容:
Heap用于為所有對象和數(shù)組分配內(nèi)存Method Area用于保存類/接口的結(jié)構(gòu)信息Run-Time Constant Pool用于保存各種常量
我畫了一張簡單的示意圖來展示它們之間的關(guān)系:

這個圖中,我把Heap和Method Area分成了兩個互相隔離的區(qū)域,Java虛擬機規(guī)范并沒有要求這么做,不過HotSpot虛擬機的早期實現(xiàn)和圖中是類似的。
Method Area和Heap分開也是比較合理的,因為兩者保存的數(shù)據(jù)類型不一樣,數(shù)據(jù)的生命周期也不相同,分開存儲更有利于管理和回收。
HotSpot VM的內(nèi)存模型變遷
在這一部分,我將通過三張內(nèi)存結(jié)構(gòu)圖來描述HotSpot虛擬機的內(nèi)存模型在JDK1.6,JDK1.7和JDK1.8中的變遷,請忽略各個區(qū)域的大小比例,重點關(guān)注各個區(qū)域的轉(zhuǎn)移。
JDK1.6 Perm Gen作為Method Area的實現(xiàn)
在JDK1.6中,永久代(Perm Gen)作為Method Area的實現(xiàn),這里保存著類的靜態(tài)變量(Class statics),字符串常量池(String Table),符號引用(Symbols)和字面量(Interned Strings)。
這個時期的永久代和堆是相鄰的,使用連續(xù)的物理內(nèi)存,但是內(nèi)存空間是隔離的。
永久代的垃圾收集是和老年代捆綁在一起的,因此無論誰滿了,都會觸發(fā)永久代和老年代的垃圾收集。
永久代的內(nèi)存受到Java虛擬機的管理。

JDK1.7 數(shù)據(jù)向Java Heap和Native Heap遷移
在JDK1.7中,Perm Gen的數(shù)據(jù)開始向Java Heap和Native Heap轉(zhuǎn)移:
字符串常量池(String Table)轉(zhuǎn)移到了Java Heap
字面量(Interned Strings)轉(zhuǎn)移到了Java Heap
類的靜態(tài)變量(Class Statics)轉(zhuǎn)移到了Java Heap
符號引用(Symbols)轉(zhuǎn)移到了Native Heap
Java Heap和Native Heap有什么區(qū)別?
Native Heap是操作系統(tǒng)層面的堆區(qū),是JVM進(jìn)程運行時動態(tài)向操作系統(tǒng)申請的內(nèi)存空間。JVM會在Native Heap中劃出一塊區(qū)域作為Java Heap(也有JVM Heap的說法,本文使用Oracle官網(wǎng)的名詞)。Java Heap就是Java虛擬機規(guī)范里面的Heap。
具體可以參考這篇回答:native memory和native heap及GC heap有什么關(guān)系?
為什么要轉(zhuǎn)移永久代的數(shù)據(jù)?
因為通常使用PermSize和MaxPermSize設(shè)置永久代的大小就決定了永久代的上限,容易遇到OOM,比如使用動態(tài)代理時,需要大量加載類文件,這時候很容易就發(fā)生java.lang.OutOfMemoryError: PermGen Space的異常。
為了減少永久代的壓力,因此JDK1.7開始把數(shù)據(jù)向堆和本地內(nèi)存遷移。

JDK1.8 MetaSpace成為Method Area的實現(xiàn)
到了JDK1.8,HotSpot直接使用MetaSpace取代了Perm Gen。
自此,HotSpot虛擬機中不再有Perm Gen(永久代),只有MetaSpace(元空間)。
下面直接貼一段Oracle的官方資料中對MetaSpace的描述:
JDK 8 does not have Permanent Generation
Class metadata is stored in a new space called Metaspace
Not contiguous with the Java Heap
Metaspace is allocated out of native memory
Maximum space available to the Metaspace is the available system memory
This can though be limited by MaxMetaspaceSize JVM option
可以看到,元空間對比老年代有很多優(yōu)點,它不再和Java Heap使用相鄰的物理內(nèi)存,直接從本地內(nèi)存分配空間,元空間大小的上限受限于系統(tǒng)的內(nèi)存大小,因此發(fā)生OOM的概率可以大大降低。當(dāng)然,我們還是可以使用MaxMetaspaceSize選項來限制MetaSpace的大小。

字符串常量池和intern()方法
在HotSpot內(nèi)存模型的變遷過程中,還有一個地方值得特別關(guān)心,那就是String Table(字符串常量池)。
String Table在JDK1.6中位于Perm Gen,但是在JDK1.7中被轉(zhuǎn)移到了Java Heap中,這次轉(zhuǎn)移伴隨著String.intern()方法的性質(zhì)發(fā)生了一些微小的改變。
在1.6中,intern的處理是先判斷字符串常量是否在字符串常量池中,如果存在直接返回該對象的引用。如果沒有找到,則將該字符串常量加入到字符串常量區(qū),也就是在永久代中創(chuàng)建該字符串對象,再把引用保存到字符串常量池中。
在1.7中,intern的處理是先判斷字符串常量是否在字符串常量池中,如果存在直接返回該對象的引用,如果沒有找到,說明該字符串常量在堆中,則處理是把堆區(qū)該對象的引用加入到字符串常量池中,以后別人拿到的是該字符串常量的引用,實際存在堆中。
這里只是簡單提一下結(jié)論,具體的細(xì)節(jié)會寫一篇文章來介紹一下,敬請期待。
結(jié)語
至此,本文對Java虛擬機規(guī)范中關(guān)于JVM內(nèi)存區(qū)域的描述做了簡單的解讀,并以HotSpot虛擬機為例說明了具體實現(xiàn)和規(guī)范之間的聯(lián)系。
Java虛擬機規(guī)范是一份與實現(xiàn)無關(guān)的文檔,它在描述時沒有規(guī)定具體的實現(xiàn)細(xì)節(jié),顯得"模棱兩可",但所有的Java虛擬機實現(xiàn)都應(yīng)該遵循這個規(guī)范。
其中還有關(guān)于類文件格式,字節(jié)碼指令等相關(guān)內(nèi)容的描述,感興趣的讀者可以自行前往閱讀。
另外,Oracle官網(wǎng)的教程PPT也是一份不錯的資料:HotSpot JVM Memory Management
關(guān)于字符串常量池的細(xì)節(jié),放在下一篇文章來討論。
粉絲福利:Java從入門到入土學(xué)習(xí)路線圖
??????

??長按上方微信二維碼 2 秒
感謝點贊支持下哈 
