JVM學習(一)——記憶體結構

Hiway發表於2019-01-10

其他更多java基礎文章:
java基礎學習(目錄)


學習資料
Java虛擬機器規範
方法區的Class資訊,又稱為永久代,是否屬於Java堆?
JVM 內部原理(一)— 概述

執行時資料區域(Run-Time Data Areas)

Java虛擬機器定義了在程式執行期間使用的各種執行時資料區域。其中一些資料區域是在Java虛擬機器啟動時建立的,僅在Java虛擬機器退出時銷燬。其他資料區域是每個執行緒。執行緒資料區域是線上程退出時建立和銷燬執行緒時建立的。
JVM所管理的幾個執行時資料區域:方法區、虛擬機器棧、本地方法棧、堆、程式計數器,其中方法區和堆是由執行緒共享的資料區,其他幾個是執行緒隔離的資料區。程式計數器,虛擬機器棧,本地方法棧,隨執行緒而生,執行緒亡而亡

執行緒獨享

  • 程式計數器
  • 虛擬機器棧
  • 本地方法棧

執行緒共享

  • 方法區

JVM學習(一)——記憶體結構

程式計數器/PC計數器(The pc Register)

程式計數器是一塊較小的記憶體,他可以看做是當前執行緒所執行的行號指示器。每個Java虛擬機器執行緒都有自己的程式計數器,通常程式計數器會在執行指令結束後增加,因此它需要保持下一將要執行指令的地址。 位元組碼直譯器工作的時候就是通過改變這個計數器的值來選取下一條需要執行的位元組碼的指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。如果執行緒正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Native方法,這個計數器則為空。此記憶體區域是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemotyError情況的區域

虛擬機器棧(Java Virtual Machine Stacks)

每個執行緒都有自己的棧(stack),棧內以幀(frame)的形式保持著執行緒內執行的每個方法。棧是一個後進先出(LIFO)的資料結構,所以當前執行的方法在棧頂部。每次方法呼叫時,都會建立新的幀並且壓入棧的頂部。當方法正常返回或丟擲未捕獲的異常時,幀或從棧頂移除。除了壓入和移除幀物件的操作,棧沒有其他直接的操作,因此幀物件分配在堆中,記憶體並不要求連續。對於執行引擎來說,活動執行緒中,只有棧頂的棧幀是有效的,稱為當前棧幀,這個棧幀所關聯的方法稱為當前方法。執行引擎所執行的所有位元組碼指令都只針對當前棧幀進行操作。
在Java 虛擬機器規範中,對虛擬機器棧規定了兩種異常狀況:如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError 異常;如果虛擬機器棧可以動態擴充套件(當前大部分的Java虛擬機器都可動態擴充套件,只不過Java虛擬機器規範中也允許固定長度的虛擬機器棧),當擴充套件時無法申請到足夠的記憶體時會丟擲OutOfMemoryError 異常。

JVM學習(一)——記憶體結構

幀(Frame)

每次方法呼叫時,新的幀都會建立並被壓入棧頂。當方法正常返回或丟擲未捕獲異常時,幀會從做退棧操作。詳細的異常處理參加後面 異常表(Exception Table)部分。

每個幀都包括

  • 區域性變數(Local variable)
  • 返回地址(Return value)
  • 運算元棧(Operand stack)
  • 動態連結(Dynamic Linking)
區域性變數表(Local variable)

區域性變數表是一組變數值儲存空間,用於存放方法引數和方法內部定義的區域性變數。在Java程式被編譯成Class檔案時,就在方法的Code屬性的max_locals資料項中確定了該方法所需要分配的最大區域性變數表的容量。
Java虛擬機器使用區域性變數在方法呼叫上傳遞引數。在類方法呼叫中,任何引數都在從區域性變數0開始的連續區域性變數中傳遞。在例項方法呼叫中,區域性變數0始終用於傳遞對呼叫例項方法的物件的引用(Java程式語言裡的this)。隨後,任何引數都在從區域性變數1開始的連續區域性變數中傳遞。

區域性變數可以是:

  • boolean運算元棧
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

所有的型別都在本地變數陣列中佔一個槽,而 longdouble 會佔兩個連續的槽,因為它們有雙倍寬度(64-bit 而不是 32-bit)。reference型別虛擬機器規範沒有明確說明它的長度,但一般來說,虛擬機器實現至少都應當能從此引用中直接或者間接地查詢到物件在Java堆中的起始地址索引和方法區中的物件型別資料。returnAddress型別是為位元組碼指令jsr、jsr_w和ret服務的,它指向了一條位元組碼指令的地址。

運算元棧(Operand stack)

每個幀包含一個後進先出(LIFO)堆疊,稱為其運算元堆疊。幀的運算元堆疊的最大深度在編譯時確定,並與幀相關的方法的程式碼一起提供。
虛擬機器把運算元棧作為它的工作區——大多數指令都要從這裡彈出資料,執行運算,然後把結果壓回運算元棧。比如,iadd指令就要從運算元棧中彈出兩個整數,執行加法運算,其結果又壓回到運算元棧中,看看下面的示例,它演示了虛擬機器是如何把兩個int型別的區域性變數相加,再把結果儲存到第三個區域性變數的:

begin  
iload_0    // push the int in local variable 0 ontothe stack  
iload_1    //push the int in local variable 1 onto the stack  
iadd       // pop two ints, add them, push result  
istore_2   // pop int, store into local variable 2  
end  
複製程式碼

在這個位元組碼序列裡,前兩個指令iload_0iload_1將儲存在區域性變數中索引為0和1的整數壓入運算元棧中,其後iadd指令從運算元棧中彈出那兩個整數相加,再將結果壓入運算元棧。第四條指令istore_2則從運算元棧中彈出結果,並把它儲存到區域性變數區索引為2的位置。下圖詳細表述了這個過程中區域性變數和運算元棧的狀態變化,圖中沒有使用的區域性變數區和運算元棧區域以空白表示。

JVM學習(一)——記憶體結構

返回地址

方法的返回分為兩種情況,一種是正常退出,退出後會根據方法的定義來決定是否要傳返回值給上層的呼叫者,一種是異常導致的方法結束,這種情況是不會傳返回值給上層的呼叫方法。

不過無論是那種方式的方法結束,在退出當前方法時都會跳轉到當前方法被呼叫的位置,如果方法是正常退出的,則呼叫者的PC計數器的值就可以作為返回地址,如果是因為異常退出的,則是需要通過異常處理表來確定。

方法的的一次呼叫就對應著棧幀在虛擬機器棧中的一次入棧出棧操作,因此方法退出時可能做的事情包括:恢復上層方法的區域性變數表以及運算元棧,如果有返回值的話,就把返回值壓入到呼叫者棧幀的運算元棧中,還會把PC計數器的值調整為方法呼叫入口的下一條指令。

動態連結(Dynamic Linking)

虛擬機器執行的時候,執行時常量池會儲存大量的符號引用,這些符號引用可以看成是每個方法的間接引用。如果代表棧幀A的方法想呼叫代表棧幀B的方法,那麼這個虛擬機器的方法呼叫指令就會以B方法的符號引用作為引數,但是因為符號引用並不是直接指向代表B方法的記憶體位置,所以在呼叫之前還必須要將符號引用轉換為直接引用,然後通過直接引用才可以訪問到真正的方法。

如果符號引用是在類載入階段或者第一次使用的時候轉化為直接應用,那麼這種轉換成為靜態解析,如果是在執行期間轉換為直接引用,那麼這種轉換就成為動態連線。

本地方法棧(Native Method Stack)

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

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

堆(Heap)

堆是執行時分配類例項和陣列記憶體的地方。陣列和物件是不能存在棧裡的,因為棧幀(frame)不是被設計用作此目的,一旦棧幀建立了,它的大小不可更改。幀只用來儲存指向對中物件或陣列的引用。與幀內本地變數陣列裡基本變數和引用不同,物件總是儲存在堆內的,所以在方法結束前,它們不會被移除。而且,物件只能被垃圾回收器移除。

為了支援垃圾回收的機制,堆通常被分為三部分:

  • 新生代(Young Generation)
    • 通常分為 新生者(Eden)和 倖存者(Survivor)
  • 老年代(Old Generation/Tenured Generation)
  • 永久代(Permanent Generation)(JDK8中已移除

記憶體管理(Memory Management)

物件和陣列不會被顯式的移除,而是會被 GC 自動回收。通常的順序是這樣:

  1. 新的物件和陣列被建立在新生代區
  2. 小的 GC 會發生在新生代,存活的物件會從 新生區(Eden)移到 倖存區(Survivor)
  3. 大的 GC ,通常會導致應用程式執行緒暫停,物件移動會發生在不同代之間。仍然存活的物件會從新生代被移動到老年代。
  4. 永久代的收集時刻都會在對老年代收集時發生。任何一代記憶體使用滿了,會在兩代同時發生收集。

關於新生代、老年代、GC的詳細講解請關注JVM學習後續文章

方法區(Method Area)

本文重點介紹方法區。因為jdk6,7,8中分別對方法區的實現永久代做了修改。

方法區介紹

JVM虛擬機器規範中:

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. 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. A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.

在 Java 虛擬機器中,方法區( Method Area) 是可供各條執行緒共享的執行時記憶體區域。方法區與傳統語言中的編譯程式碼儲存區( Storage Area Of Compiled Code)或者作業系統程式的正文段( Text egment)的作用非常類似,它儲存了每一個類的結構資訊,例如執行時常量池( Runtime Constant Pool)、欄位和方法資料、建構函式和普通方法的位元組碼內容、還包括一些在類、例項、介面初始化時用到的特殊方法

方法區在虛擬機器啟動的時候被建立,雖然方法區是堆的邏輯組成部分,但是簡單的虛擬機器實現可以選擇在這個區域不實現垃圾收集。這個版本的 Java 虛擬機器規範也不限定實現方法區的記憶體位置和編譯程式碼的管理策略。方法區的容量可以是固定大小的,也可以隨著程式執行的需求動態擴充套件,並在不需要過多空間時自動收縮。方法區在實際記憶體空間中可以是不連續的。

Java 虛擬機器實現應當提供給程式設計師或者終端使用者調節方法區初始容量的手段,對於可以動態擴充套件和收縮方法區來說,則應當提供調節其最大、最小容量的手段

如果方法區的記憶體空間不能滿足記憶體分配請求,那 Java 虛擬機器將丟擲一個OutOfMemoryError異常。

總之,就是用來儲存類的結構資訊。它有一個別名叫做Non-Heap(非堆)。

永久代

永久代是HotSpot中方法區的實現。
平時,說到永久代(PermGen space)的時候往往將其和方法區不加區別。這麼理解在一定角度也說的過去。 因為,JVM虛擬機器規範只是規定了有方法區這麼個概念和它的作用,並沒有規定如何去實現它。那麼,在不同的 JVM 上方法區的實現肯定是不同的了。 同時,大多數用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集擴充套件至方法區,或者說使用永久代來實現方法區。

雖然可以牽強的解釋這種將方法區和永久帶等同對待觀點。但最終方法區和永久帶還是不同的。一個是標準一個是實現。

JDK1.7之前的永久代

java7之前,方法區位於永久代(PermGen),永久代和堆相互隔離,永久代的大小在啟動JVM時可以設定一個固定值,不可變。這裡有個在面試中經常問的問題,就是String.intern()方法,詳情可以閱讀之前我寫的一篇文章java基礎:String — 字串常量池與intern(二)

JDK1.7的永久代

Highlights of Technology Changes in Java SE 7中,我們可以看到

In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.

在JDK 7中,interned strings不再分配在Java堆的永久代中,而是和應用程式建立的其他物件一樣一起分配在Java堆的主要部分(稱為年輕代和年老代)。此更改將導致主Java堆中的資料更多,而永久代中的資料更少,因此可能需要調整堆大小。由於這種變化,大多數應用程式在堆使用方面只會看到相對較小的差異,但是載入許多類或大量使用String.intern()方法的大型應用程式會看到更顯著的差異。

以及方法區的Class資訊,又稱為永久代,是否屬於Java堆? 中,R大的講解:

Oracle JDK7 / OpenJDK 7的HotSpot VM是把Symbol的儲存從PermGen移動到了native memory,並且把靜態變數從instanceKlass末尾(位於PermGen內)移動到了java.lang.Class物件的末尾(位於普通Java heap內)。
“常量池”如果說的是runtime constant pool,這個還是在PermGen裡;
“常量池”如果說的是SymbolTable / StringTable,這倆table自身原本就一直在native memory裡,是它們所引用的東西在哪裡更有意思。
上面說了,7是把SymbolTable引用的Symbol移動到了native memory,而StringTable引用的java.lang.String例項則從PermGen移動到了普通Java heap。

R大說的runtime constant pool即為執行常量池,SymbolTable 即為識別符號表,StringTable即字串常量池。從而我們可以得知,即 java7中,儲存在永久代的部分資料就已經轉移到Java Heap或者Native memory。但永久代仍存在於JDK 1.7中,並沒有完全移除。

jdk1.8的元空間及永久代

JEP 122: Remove the Permanent Generation

Move part of the contents of the permanent generation in Hotspot to the Java heap and the remainder to native memory.
Hotspot's representation of Java classes (referred to here as class meta-data) is currently stored in a portion of the Java heap referred to as the permanent generation. In addition, interned Strings and class static variables are stored in the permanent generation. The permanent generation is managed by Hotspot and must have enough room for all the class meta-data, interned Strings and class statics used by the Java application.
The proposed implementation will allocate class meta-data in native memory and move interned Strings and class statics to the Java heap. Hotspot will explicitly allocate and free the native memory for the class meta-data. Allocation of new class meta-data would be limited by the amount of available native memory rather than fixed by the value of -XX:MaxPermSize, whether the default or specified on the command line.

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

java8中,取消永久代,方法區存放於元空間(Metaspace),元空間仍然與堆不相連,但與堆共享實體記憶體,邏輯上可認為在堆中。

為什麼移除永久代?
  1. 字串存在永久代中,容易出現效能問題和記憶體溢位。
  2. 永久代大小不容易確定,PermSize指定太小容易造成永久代OOM
  3. 永久代會為 GC 帶來不必要的複雜度,並且回收效率偏低。
  4. Oracle 可能會將HotSpot 與 JRockit 合二為一。

直接記憶體

直接記憶體並不是虛擬機器執行時資料區的一部分,也不是java虛擬機器規範中定義的記憶體區域。 NIO類是一種基於通道和緩衝區的I/O方式,它可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在Java堆中的DirectByteBuffer物件作為這塊直接記憶體的引用進行操作,這樣避免了java堆和navie堆中來回複製資料

相關文章