引言
網上有大量討論JVM的內存模型的文章,但很多內容都是到處摘抄而來,導致許多概念模糊不清。
比如對於“JVM內存模型”和“Java內存模型(JMM)”沒有區分,實際上,Java內存模型(JMM)是一種規范,和具體的Java虛擬機的內存結構不是一個概念,不應該把諸如“年輕代“、”老年代”這類關於虛擬機具體實現的討論歸為Java內存模型。
而在具體討論JVM的內存結構時,還應該指出,我們通常討論的都是HotSpot虛擬機中的實現,這些模型並不是所有虛擬機通用的,比如“Perm Gen(永久代)”就是HotSpot中的概念,JRockit中並沒有永久代。
此外,不應該把“永久代”和“方法區”混為一談,永久代(Perm Gen)只是HotSpot對於Java虛擬機規范中方法區(Method Area)的一種實現,后來被改成了元空間(MetaSpace),文中會具體介紹這些變化。
本文希望從Java虛擬機規范出發,盡可能通過查閱官方文檔,以及閱讀HotSpot VM中的部分核心源代碼的方式,重新梳理Java虛擬機的內存結構,重點討論:
- HotSpot虛擬機中,Heap(堆),Method Area(方法區)和Run-Time Constant Pool(運行時常量池)的關系
- Method Area的在JDK1.6,JDK1.7和JDK1.8中的變遷(Perm Gen的消失和MetaSpace的出現)
- 字符串常量池的轉移以及運行時常量池和intern方法的變化等。
而Jvm中的The pc Register
、Java Virtual Machine Stacks
和Native Method Stacks
這些部分,則不在本文的討論范圍之內。
Java虛擬機規范中的內存模型
Java虛擬機規范上指定了Java虛擬機的運行時數據區包括The pc
Register、Java Virtual Machine Stacks、 Heap、 Method Area、Run-Time Constant Pool和Native Method Stacks這些部分,其中PC寄存器,Java虛擬機棧和本地方法棧會為每個線程所創建,屬於線程私有,而堆,方法區和運行時常量池是所有線程共享的。
PC寄存器,Java虛擬機棧和本地方法棧的作用與傳統的操作系統類似,這里不多贅述,我們主要關注Heap(堆),Method Area(方法區)和Run-Time Constant Pool(運行時常量池)的規范。
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虛擬機線程所共享的內存區域,它是一塊為所有對象和數組分配內存的運行時數據區。
規范中還有下面的一段描述:
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.
文字有點長,不過我們可以總結出幾個有關Heap的要點:
- Heap是在Java虛擬機啟動時創建的
- Heap中的對象占用的空間由自動存儲管理系統所回收(其實就是GC),對象不能被顯式回收
- 自動存儲管理系統(垃圾收集器)沒有統一的實現,由虛擬機的實現者來選擇
- Heap的空間大小可以是固定的,也可以進行擴充和收縮,Heap不需要連續的內存空間
看完Java虛擬機規范中對Heap的描述,我們最需要記住的一點是:Heap是一塊為所有對象和數組分配內存的運行時數據區
Method Area(方法區)
首先看Java虛擬機規范中對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(方法區)是Java虛擬機中為所有Java虛擬機線程所共享的內存區域,它類似於傳統語言中存儲編譯后代碼的區域,或者可以說它類似於操作系統進程中的'text'段(代碼段,在操作系統中內存會分為數據段,代碼段,堆,棧和BBS段)。
Method Area用於保存每個類的結構信息,如運行時常量池、字段和方法數據、以及方法和構造器的代碼,包括用於類,對象和接口初始化的特殊方法。
下面還有一段描述,看起來跟對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虛擬機啟動時創建的
- Method Area在邏輯上是Heap的一部分,但可以選擇不對它進行垃圾收集
- Java虛擬機規范不強制規定Method Area的存儲位置和管理已編譯代碼的策略
- Heap的空間大小可以是固定的,也可以進行擴充和收縮,Heap不需要連續的內存空間
從Java虛擬機規范的描述可以看出,規范對Method Area的定義是比較寬泛,只是定義了一塊內存區域,用於存儲類的結構信息。它沒有嚴格定義Method Area在內存中的位置,也沒有規定對它進行垃圾回收等管理策略。
因此,我們不應該認為Method Area和Heap是完全割裂的兩塊內存區域,它甚至可以是Heap的一部分。
Run-Time Constant Pool(運行時常量池)
老樣子,先看定義:
A run-time constant pool is a per-class or per-interface run-time representation of the
constant_pool
table in aclass
file (§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.
上面的第一句很重要:運行時常量池是每個類/接口的字節碼文件中constant_pool table
的運行時實現。意思就是,每個類/接口都會擁有一個和字節碼中的常量池對應的運行時常量池。
它包含了各種常量,包括編譯時已知的數值字面量、運行時解析的方法和字段引用。
Run-Time Constant Pool的功能和傳統編程語言的符號表類似,不過它包含的符號類型更廣。
第二段照舊有一段描述:
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虛擬機所創建。
這說明了Run-Time Constant Pool是Method Area的一部分,這和Method Area中的描述是相符的。
小結HotSpot VM的內存結構
簡單總結一下上面虛擬機規范的內容:
Heap
用於為所有對象和數組分配內存Method Area
用於保存類/接口的結構信息Run-Time Constant Pool
用於保存各種常量
我畫了一張簡單的示意圖來展示它們之間的關系:
這個圖中,我把Heap和Method Area分成了兩個互相隔離的區域,Java虛擬機規范並沒有要求這么做,不過HotSpot虛擬機的早期實現和圖中是類似的。
Method Area和Heap分開也是比較合理的,因為兩者保存的數據類型不一樣,數據的生命周期也不相同,分開存儲更有利於管理和回收。
HotSpot VM的內存模型變遷
在這一部分,我將通過三張內存結構圖來描述HotSpot虛擬機的內存模型在JDK1.6,JDK1.7和JDK1.8中的變遷,請忽略各個區域的大小比例,重點關注各個區域的轉移。
JDK1.6 Perm Gen作為Method Area的實現
在JDK1.6中,永久代(Perm Gen)作為Method Area的實現,這里保存着類的靜態變量(Class statics),字符串常量池(String Table),符號引用(Symbols)和字面量(Interned Strings)。
這個時期的永久代和堆是相鄰的,使用連續的物理內存,但是內存空間是隔離的。
永久代的垃圾收集是和老年代捆綁在一起的,因此無論誰滿了,都會觸發永久代和老年代的垃圾收集。
永久代的內存受到Java虛擬機的管理。
JDK1.7 數據向Java Heap和Native Heap遷移
在JDK1.7中,Perm Gen的數據開始向Java Heap和Native Heap轉移:
- 字符串常量池(String Table)轉移到了Java Heap
- 字面量(Interned Strings)轉移到了Java Heap
- 類的靜態變量(Class Statics)轉移到了Java Heap
- 符號引用(Symbols)轉移到了Native Heap
Java Heap和Native Heap有什么區別?
Native Heap是操作系統層面的堆區,是JVM進程運行時動態向操作系統申請的內存空間。JVM會在Native Heap中划出一塊區域作為Java Heap(也有JVM Heap的說法,本文使用Oracle官網的名詞)。Java Heap就是Java虛擬機規范里面的Heap。
具體可以參考這篇回答:native memory和native heap及GC heap有什么關系?
為什么要轉移永久代的數據?
因為通常使用PermSize和MaxPermSize設置永久代的大小就決定了永久代的上限,容易遇到OOM,比如使用動態代理時,需要大量加載類文件,這時候很容易就發生java.lang.OutOfMemoryError: PermGen Space
的異常。
為了減少永久代的壓力,因此JDK1.7開始把數據向堆和本地內存遷移。
JDK1.8 MetaSpace成為Method Area的實現
到了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
可以看到,元空間對比老年代有很多優點,它不再和Java Heap使用相鄰的物理內存,直接從本地內存分配空間,元空間大小的上限受限於系統的內存大小,因此發生OOM的概率可以大大降低。當然,我們還是可以使用MaxMetaspaceSize
選項來限制MetaSpace的大小。
字符串常量池和intern()方法
在HotSpot內存模型的變遷過程中,還有一個地方值得特別關心,那就是String Table(字符串常量池)。
String Table在JDK1.6中位於Perm Gen,但是在JDK1.7中被轉移到了Java Heap中,這次轉移伴隨着String.intern()
方法的性質發生了一些微小的改變。
- 在1.6中,intern的處理是先判斷字符串常量是否在字符串常量池中,如果存在直接返回該對象的引用。如果沒有找到,則將該字符串常量加入到字符串常量區,也就是在永久代中創建該字符串對象,再把引用保存到字符串常量池中。
- 在1.7中,intern的處理是先判斷字符串常量是否在字符串常量池中,如果存在直接返回該對象的引用,如果沒有找到,說明該字符串常量在堆中,則處理是把堆區該對象的引用加入到字符串常量池中,以后別人拿到的是該字符串常量的引用,實際存在堆中。
這里只是簡單提一下結論,具體的細節會寫一篇文章來介紹一下,敬請期待。
結語
至此,本文對Java虛擬機規范中關於JVM內存區域的描述做了簡單的解讀,並以HotSpot虛擬機為例說明了具體實現和規范之間的聯系。
Java虛擬機規范是一份與實現無關的文檔,它在描述時沒有規定具體的實現細節,顯得"模棱兩可",但所有的Java虛擬機實現都應該遵循這個規范。
其中還有關於類文件格式,字節碼指令等相關內容的描述,感興趣的讀者可以自行前往閱讀。
另外,Oracle官網的教程PPT也是一份不錯的資料:HotSpot JVM Memory Management
關於字符串常量池的細節,放在下一篇文章來討論。