JVM 系列文章之 Java 的記憶體區域

pjmike_pj發表於2018-09-01

前言

下面關於 Java 的記憶體區域介紹大部分參考深入理解Java虛擬機器,也參考了網上很多資料,以下圖片均摘自網路

執行時資料區域

Java虛擬機器在執行 Java 程式的過程中會把它管理的記憶體劃分為若干個不同的資料區域。根據《Java 虛擬機器規範》將 Java虛擬機器所管理的記憶體分為以下幾個執行時資料區域:

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

jvm_data

程式計數器

程式計數器 ,也稱作 PC暫存器或者指令地址暫存器。在組合語言中,它儲存的是程式當前執行的指令的地址(或者說是儲存一條),當CPU需要執行指令時,需要從程式計數器中得到當前需要執行的指令所在儲存單元的地址,然後根據得到的地址獲取指令,在得到指令之後,程式計數器便自動加1或者根據轉移指標得到下一條指令的地址,如此迴圈,直至執行完所有的指令。

在JVM中,程式計數器是一塊較小的記憶體空間,它可以看做是當前執行緒所執行的位元組碼的行號指示器,位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一跳需要執行的位元組碼指令

由於Java 虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時間,一個處理器(對於多核處理器來說是一個核心)都只會執行一條執行緒中的指令。因此,為了執行緒切換後能夠恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存,這是 "執行緒私有的"

如果執行緒正在執行的是一個 Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是 Native方法,計數器值為空,此記憶體區域是唯一一個在 Java虛擬機器規範中沒有規定任何 OutOfMemoryError 情況的區域

Java虛擬機器棧

Java虛擬機器棧也是執行緒私有的,它的生命週期與執行緒相同,它描述的是 Java 方法執行的記憶體模型: 每個方法在執行的同時都會建立一個棧幀( Stack Frame)用於儲存區域性變數表,運算元棧,動態連結,方法出口等資訊。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。至於關於棧幀的具體介紹後續文章再分析。

因為除了棧幀的出棧和入棧之外,Java虛擬機器棧不會再受其他因素的影響,所以 棧幀可以在系統的堆中分配(注意,是系統的Heap而不是Java 堆)

JVM保留了兩個記憶體區:Java 堆和本機(或系統堆)。這個堆具有不同的用途,並使用不同的機制進行維護,Java堆就是我下面要將的包含物件例項的"堆",而系統的堆使用作業系統的底層 malloc 和 free機制進行分配,且用於底層實施特定的Java物件。

Java虛擬機器棧所使用的記憶體不需要保證是連續的。

Java虛擬機器棧可能發生如下異常情況:

  • 如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲 StackOverflowError 異常
  • 如果虛擬機器棧可以動態擴充套件(當前大部分的Java虛擬機器都可以動態擴充套件,只不過Java虛擬機器規範中也允許固定長度的虛擬機器棧),如果擴充套件時無法申請到足夠的記憶體,就會丟擲 OutOfMemoryError 異常。

注意: Java虛擬機器棧就是棧,也可以成為"堆疊",只是堆疊這種說法容易讓人混淆。關於Java虛擬機器的堆,棧,堆疊如何去理解這類問題,JVM專家R大也在知乎上對其進行了詳細的解答,傳送門: Java虛擬機器的堆、棧、堆疊如何去理解? - RednaxelaFX的回答 - 知乎

本地方法棧

本地方法棧(Native Method Stack)與虛擬機器棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機器棧為虛擬機器執行 Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的是 Native方法服務。在虛擬機器規範中對本地方法棧中方法使用的語言,使用方法與資料結構並沒有強制規定,因此具體的虛擬機器可以自由實現它。

看到這個本地方法棧,總是對本地方法有些疑惑,下面簡單說下Native Method

什麼是 Native Method

一個Natvie Method就是一個Java呼叫非Java程式碼的介面,它由非Java語言實現,比如C語言

在定義一個 Native Method時,並不提供實現體(有些像定義一個 Java Interface),因為其實現體是由非Java語言在外面實現的,比如:

public class IHaveNatives
    {
      native public void Native1( int x ) ;
      native static public long Native2() ;
      native synchronized private float Native3( Object o ) ;
      native void Native4( int[] ary ) throws Exception ;
    }
複製程式碼

native方法可以返回任何 Java型別,也能夠實現異常控制。

為什麼使用Native Method

Java對一些層次的任務用 Java實現不容易,對某些程式效率不高:

  • Java與Java外的環境互動:

Java與一些底層系統如作業系統或某些硬體交換資訊,native方法提供一個非常簡潔的介面,無需瞭解Java應用之外的細節。

  • 與作業系統互動

通過使用本地方法讓 Java實現 JRE與底層系統的互動

  • Sun'Java

Sun的直譯器是用 C實現的,JRE 大部分用 Java實現,其通過一些本地方法與外界互動

所以對於本地方法棧來說,它本質是為本地方法服務的,如果某個虛擬機器實現的本地方法介面是使用 C連線模型的話,那麼它的本地方法棧就是 C棧。下圖展示了一個Java棧和本地方法棧之間的跳轉,(圖片摘自網路):

native

Java堆

對於大多數應用來說,Java 堆(Java Heap)是Java 虛擬機器所管理的記憶體中最大的一塊。Java 堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體

Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱作為 "GC堆"。從記憶體回收的角度看,由於現在收集器基本採用分代收集演算法,(關於垃圾演算法的介紹後續文章分析),所以 Java 堆中還可以細分為: 新生代和老年代,再細緻一點有 Eden空間,From Survivor空間,To Survivor 空間等

根據 Java 虛擬機器規範的規定, Java 堆可以處於物理不連續的記憶體空間中,只要邏輯是連續的即可,就像我們的磁碟空間一樣。在實現時,即可以實現成固定大小的,也可以是可擴充套件的,不過當前主流的虛擬機器都是按照可擴充套件來實現的 (通過 -Xmx 和 -Xms 控制)。

如果堆上沒有記憶體完成例項分配,並且 堆也無法再擴充套件時,將會丟擲 OutOfMemoryError異常。

JVM中堆和棧的區別

這裡簡單說說JVM中堆和棧的區別:

  • 功能不同
    • 棧記憶體用來儲存區域性變數,運算元棧等資訊
    • 堆記憶體用來儲存Java中的物件
  • 共享性不同
    • 棧記憶體是執行緒私有的
    • 堆記憶體是所有執行緒共有的
  • 空間大小
    • 棧的空間大小遠遠小於堆的
  • 異常錯誤不同
    • 棧有兩種異常情況,如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲 StackOverflowError 異常,如果虛擬機器棧動態擴充套件時無法申請到足夠的記憶體,就會丟擲 OutOfMemoryError 異常。
    • 堆一般在堆中沒有記憶體完成例項分配,並且堆也無法進行擴充套件時,丟擲 OutOfMemoryError 異常。

方法區

方法區(Method Area) 與 Java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊,常量,靜態變數,即時編譯器編譯後的程式碼等資料。雖然Java虛擬機器規範把方法區描述為 堆的一個邏輯部分,但是它有一個別名叫做 "非堆".

對於HotSpot虛擬機器來講,方法區域又被稱為 "永久代",本質上兩者並不等價,僅僅是因為HotSpot虛擬機器的設計團隊選擇把 GC 分代收集擴充套件至方法區,或者說使用 永久代來實現方法區而已,這樣的HotSpot的垃圾收集器可以像 管理 Java堆一樣管理這部分記憶體,能夠省去專門為方法區編寫記憶體管理程式碼的工作。對於其他虛擬機器(如 BEA JRockit,IBM J9等)來說是不存在永久代的概念的。

在JDK 1.7及以前的HotSpot JVM中,方法區位於永久代(Permanent Generation,PermGen) 中。如下圖,是JDK 1.7及以前的 Java堆記憶體的結構圖,裡面包含了 Permanent Generation:

permanent

由於 永久代內可能發生記憶體洩漏或溢位的問題(永久代有 -XX:MaxPermSize的上限)而導致的 java.lang.OutOfMemoryError: PermGen space,JEP小組從JDK 1.7 開始就籌劃移除永久代 (JEP 122: Remove the Permanent Generation,並且在 JDK 1.7 中把字串常量,符號引用等移除了永久代。到了Java 8 ,永久代被徹底地移除了 JVM,取而代之的是元空間 (Metaspace)

permanent generation remove
JDK 8開始將類的後設資料放到本地堆記憶體(native heap)中,這一塊區域就叫 Metaspace,關於 Metaspace 的介紹請參考 Metaspace in Java 8

執行時常量池

前面講 方法區的時候就提到,執行時常量池是方法區的一部分,它是 class檔案中每一個類或介面的常量池表的執行時表示形式。它包括了若干種不同的常量,常量池表存放 編譯器生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。

changliang
執行時常量池具有動態性,執行期間也可以將新的量放到執行時常量池中,典型的應用是 String 類的 intern方法:

public native String intern()
複製程式碼

String類的 intern方法會從字串常量池中查詢當前字串是否存在,如果存在,就會直接返回當前字串,若不存在就會將當前字串放入常量池中,再返回。關於String.intern()更為詳盡的分析,請參閱文章: 深入解析String#intern

而從JDK 1.7開始,字串常量和符號引用等被移除永久代:

  • 符號引用遷移至系統堆記憶體 (Native Heap)
  • 字串字面量遷移至 Java堆(Java Heap)

小結

以上的分析參考了深入理解Java虛擬機器這本書,同時也參考了很多優秀的文章。在此過程中,我們要注意JDK版本變化帶來的問題,比如在 JDK 8版本中,永久代被徹底移除了。

當上面提到一個JVM的巨牛級別的人物——R大,R大是國內JVM巨牛級人物,他的回答都是非常權威的,所以學習 JVM的知識可以多參考 R大的分析。目前本人對 JVM也是一枚渣渣級選手,現在輸出對JVM的一些學習筆記。如有錯誤之處,歡迎指出。

參考資料 & 鳴謝

相關文章