你瞭解Java記憶體模型麼(Java7、8、9記憶體模型的區別)

和程式碼去流浪發表於2020-12-24

Java記憶體模型是每個java程式設計師必須掌握理解的,這是Java的核心基礎,對我們編寫程式碼特別是併發程式設計時有很大幫助。由於Java程式是交由JVM執行的,所以我們在談Java記憶體區域劃分的時候事實上是指JVM記憶體區域劃分。

首先,我們回顧一下Java程式執行流程:

如上圖所示,首先Java原始碼檔案(.java字尾)會被Java編譯器編譯為位元組碼檔案(.class字尾),然後由JVM中的類載入器載入各個類的位元組碼檔案,載入完畢之後,交由JVM執行引擎執行。在整個程式執行過程中,JVM會用一段空間來儲存程式執行期間需要用到的資料和相關資訊,這段空間一般被稱作為Runtime Data Area(執行時資料區),也就是我們常說的JVM記憶體。因此,在Java中我們常常說到的記憶體管理就是針對這段空間進行管理(如何分配和回收記憶體空間)。

那麼本篇文章主要是要分析Runtime Data Area(執行時資料區)的結構。

1. 執行時資料區分為幾個部分?

根據 JVM 規範,JVM 記憶體共分為虛擬機器棧、堆、方法區、程式計數器、本地方法棧五個部分。

1.1 方法區

方法區是java虛擬機器規範去中定義的一種概念上的區域,具有什麼功能,但並沒有規定這個區域到底應該位於何處,因此對於實現者來說,如何來實際方法區是有著很大自由度的。

永生代是hotspot中的一個概念,其他jvm實現未必有,例如jrockit就沒這東西。java8之前,hotspot使用在記憶體中劃分出一塊區域來儲存類的元資訊、類變數以及內部字串(interned string)等內容,稱之為永生代,把它作為方法區來使用。

[JEP122][2]提議取消永生代,方法區作為概念上的區域仍然存在。原先永生代中類的元資訊會被放入本地記憶體(後設資料區,metaspace),將類的靜態變數和內部字串放入到java堆中。

為了龍清楚方法區那麼需要解釋兩個名詞:永久代和元空間

PermGen(永久代)

絕大部分Java程式設計師應該都見過“java.lang.OutOfMemoryError: PremGen space”異常。這裡的“PermGen space”其實指的就是方法區。不過方法區和“PermGen space”又有著本質的區別。前者是JVM的規範,而後者則是JVM規範的一種實現,並且只有HotSpot才有“PermGen space”,而對於其他型別的虛擬機器,如JRockit(Oracle)、J9(IBM)並沒有“PermGen space”。由於方法區主要儲存類的相關資訊,所以對於動態生成類的情況比較容易出現永久代的記憶體溢位。並且JDK 1.8中引數PermSize和MaxPermSize已經失效。

元空間

其實,移除永久代的工作從JDK 1.7就開始了。JDK 1.7中,儲存在永久代的部分資料就已經轉移到Java Heap或者Native Heap。但永久代仍存在於JDK 1.7中,並沒有完全移除,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了Java heap;類的靜態變數(class statics)轉移到了Java heap。

JDK1.8對JVM架構的改造將類後設資料放到本地記憶體中,另外,將常量池和靜態變數放到Java堆裡。HotSpot VM將會為類的後設資料明確分配和釋放本地記憶體。在這種架構下,類元資訊就突破了原來-XX:MaxPermSize的限制,現在可以使用更多的本地記憶體。這樣就從一定程度上解決了原來在執行時生成大量類造成經常Full GC問題,如執行時使用反射、代理等。所以升級以後Java堆空間可能會增加。
元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間的最大區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制,但可以通過以下引數指定元空間的大小:
-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行型別解除安裝,同時GC會對改值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。
-XX:MaxMetaspaceSize,最大空間,預設是沒有限制的。
除了上面的兩個指定大小的選項外,還有兩個與GC相關的屬性:-XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少為分配空間所導致的垃圾收集。
-XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少為釋放空間所導致的垃圾收集。


所以對於方法區,Java8之後的變化:

  • 移除了永久代(PermGen),替換為元空間(Metaspace);
  • 永久代中的 class metadata 轉移到了 native memory(本地記憶體,而不是虛擬機器);
  • 永久代中的 interned Strings 和 class static variables 轉移到了 Java heap;
  • 永久代引數 (PermSize MaxPermSize) -> 元空間引數(MetaspaceSize MaxMetaspaceSize)



1.2 虛擬機器棧(執行緒棧)與 堆(Heap)
為更好的理解Java執行緒棧和堆,我們簡單的認為Java記憶體模型把Java虛擬機器內部劃分為執行緒棧和堆。這張圖演示了Java記憶體模型的邏輯檢視。


每一個執行在Java虛擬機器裡的執行緒都擁有自己的執行緒棧。這個執行緒棧包含了這個執行緒呼叫的方法當前執行點相關的資訊。一個執行緒僅能訪問自己的執行緒棧。一個執行緒建立的本地變數對其它執行緒不可見,僅自己可見。即使兩個執行緒執行同樣的程式碼,這兩個執行緒任然在在自己的執行緒棧中的程式碼來建立本地變數。因此,每個執行緒擁有每個本地變數的獨有版本。
所有原始型別的本地變數都存放線上程棧上,因此對其它執行緒不可見。一個執行緒可能向另一個執行緒傳遞一個原始型別變數的拷貝,但是它不能共享這個原始型別變數自身。
堆上包含在Java程式中建立的所有物件,無論是哪一個物件建立的。這包括原始型別的物件版本。如果一個物件被建立然後賦值給一個區域性變數,或者用來作為另一個物件的成員變數,這個物件任然是存放在堆上。
下面這張圖演示了呼叫棧和本地變數存放線上程棧上,物件存放在堆上。


一個本地變數可能是原始型別,在這種情況下,它總是“呆在”執行緒棧上。
一個本地變數也可能是指向一個物件的一個引用。在這種情況下,引用(這個本地變數)存放線上程棧上,但是物件本身存放在堆上。
一個物件可能包含方法,這些方法可能包含本地變數。這些本地變數任然存放線上程棧上,即使這些方法所屬的物件存放在堆上。
一個物件的成員變數可能隨著這個物件自身存放在堆上。不管這個成員變數是原始型別還是引用型別。
靜態成員變數跟隨著類定義一起也存放在堆上。
存放在堆上的物件可以被所有持有對這個物件引用的執行緒訪問。當一個執行緒可以訪問一個物件時,它也可以訪問這個物件的成員變數。如果兩個執行緒同時呼叫同一個物件上的同一個方法,它們將會都訪問這個物件的成員變數,但是每一個執行緒都擁有這個本地變數的私有拷貝。
下圖演示了上面提到的點:


兩個執行緒擁有一些列的本地變數。其中一個本地變數(Local Variable 2)執行堆上的一個共享物件(Object 3)。這兩個執行緒分別擁有同一個物件的不同引用。這些引用都是本地變數,因此存放在各自執行緒的執行緒棧上。這兩個不同的引用指向堆上同一個物件。
注意,這個共享物件(Object 3)持有Object2和Object4一個引用作為其成員變數(如圖中Object3指向Object2和Object4的箭頭)。通過在Object3中這些成員變數引用,這兩個執行緒就可以訪問Object2和Object4。

1.3 程式計數器
程式計數器是一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器。分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。
由於Java 虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)只會執行一條執行緒中的指令。因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間的計數器互不影響,獨立儲存,我們稱這類記憶體區域為“執行緒私有”的記憶體。
如果執行緒正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Natvie 方法,這個計數器值則為空(Undefined)。
此記憶體區域是唯一一個在Java 虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。

1.4 本地方法棧

本地方法棧(Native MethodStacks)與虛擬機器棧所發揮的作用是非常相似的,其區別不過是虛擬機器棧為虛擬機器執行Java 方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器使用到的Native 方法服務。虛擬機器規範中對本地方法棧中的方法使用的語言、使用方式與資料結構並沒有強制規定,因此具體的虛擬機器可以自由實現它。甚至有的虛擬機器(譬如Sun HotSpot 虛擬機器)直接就把本地方法棧和虛擬機器棧合二為一。

與虛擬機器棧一樣,本地方法棧區域也會丟擲StackOverflowError和OutOfMemoryError異常。

相關文章