天天看點

從Java虛拟機規範看HotSpot虛拟機的記憶體結構和變遷

引言

網上有大量讨論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的存儲位置和管理已編譯代碼的政策

從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 a class 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用于儲存各種常量

我畫了一張簡單的示意圖來展示它們之間的關系:

從Java虛拟機規範看HotSpot虛拟機的記憶體結構和變遷

這個圖中,我把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虛拟機的管理。

從Java虛拟機規範看HotSpot虛拟機的記憶體結構和變遷

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開始把資料向堆和本地記憶體遷移。

從Java虛拟機規範看HotSpot虛拟機的記憶體結構和變遷

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的大小。

從Java虛拟機規範看HotSpot虛拟機的記憶體結構和變遷

字元串常量池和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虛拟機實作都應該遵循這個規範。

參考資料

  • The Java® Virtual Machine Specification-Java SE 14 Edition
  • HotSpot JVM Memory Management
  • Dissecting Memory Problems
  • HotSpot Runtime Overview
  • Java SE 7 Features and Enhancements