天天看點

java 記憶體模型詳解1.概述2.堆(Heap)3.Java虛拟機棧(JVM Stack)4.本地方法棧(Native Stack)5.方法區6.程式計數器(PC)7.元空間

1.概述

JVM記憶體模型,也即運作時資料區由以下五部分組成:

java 記憶體模型詳解1.概述2.堆(Heap)3.Java虛拟機棧(JVM Stack)4.本地方法棧(Native Stack)5.方法區6.程式計數器(PC)7.元空間
  • 虛拟機棧
  • 本地方法棧
  • 方法區
  • 程式計數器

    其中,堆、方法區是線程共享的,虛拟機棧、Native棧、程式計數器是線程私有的。

2.堆(Heap)

幾乎所有對象執行個體和數組都要在堆上配置設定,是以是VM管理的最大一塊記憶體, 也是垃圾收集器的主要活動區域。

由于現代VM采用分代收集算法, 是以Java堆從GC的角度還可以細分為: 新生代(Eden區、From Survivor區和To Survivor區)和老年代; 而從記憶體配置設定的角度來看, 線程共享的Java堆還還可以劃分出多個線程私有的配置設定緩沖區(TLAB)。 而進一步劃分的目的是為了更好地回收記憶體和更快地配置設定記憶體。

按照虛拟機規範,堆可以處在實體上不連續的記憶體空間上,隻要邏輯連續即可。如果記憶體空間不足,會報OutOfMemoryError。

java 記憶體模型詳解1.概述2.堆(Heap)3.Java虛拟機棧(JVM Stack)4.本地方法棧(Native Stack)5.方法區6.程式計數器(PC)7.元空間

2.1 新生代

所有新生成的對象首先都是放在新生代的。新生代的目标就是盡可能快速的收集掉那些生命周期短的對象。

新生代分三個區:一個Eden區,兩個Survivor區。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象将被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象将被複制到另外一個Survivor區,當這個Survivor也滿了的時候,從第一個Survivor區複制過來的并且此時還存活的對象,将被複制“老年代(Tenured)”。

需要注意,Survivor的兩個區是對稱的,沒先後關系,是以同一個區中可能同時存在從Eden複制過來對象,和從前一個Survivor複制過來的對象,而複制到老年代的隻有從第一個Survivor去過來的對象。而且,Survivor區總有一個是空的。

2.2 老年代

下列情況,對象會被移至老年代:

  • 在新生代中經曆了N次垃圾回收後仍然存活的對象
  • 大對象超過了PretenureSizeThreshold預設值。

是以,老年代一般存放生命周期較長或者大對象。它的記憶體容量一般較大,記憶體不足時觸發full gc。

2.3 永久代

見後面的方法區。

3.Java虛拟機棧(JVM Stack)

虛拟機棧描述的是Java方法執行的記憶體模型: 每個方法被執行時會建立一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動态連結、傳回位址等資訊。每個方法被調用至傳回的過程, 就對應着一個棧幀在虛拟機棧中從入棧到出棧的過程(VM提供了-Xss來指定線程的最大棧空間, 該參數也直接決定了函數調用的最大深度)。

棧的大小可以用-Xss參數控制,如果棧深不夠,會報StackOverflowError。

如果棧的記憶體不足,會報OutOfMemoryError。

3.1 棧幀(Frame)

每次方法調用都會建立一個新的棧幀并把它壓棧到棧頂。當方法正常傳回或者調用過程中抛出未捕獲的異常時,棧幀将出棧。除了棧幀的壓棧和出棧,棧不能被直接操作。

每個棧幀包含:

  • 局部變量表
  • 操作數棧
  • 動态連結
  • 傳回位址
    java 記憶體模型詳解1.概述2.堆(Heap)3.Java虛拟機棧(JVM Stack)4.本地方法棧(Native Stack)5.方法區6.程式計數器(PC)7.元空間

3.1.1 局部變量表

局部變量表包含了方法執行過程中的所有變量,包括 this 引用、所有方法參數、其他局部變量。它由數組構成。

對于類方法(也就是靜态方法),方法參數從下标 0 開始;對于對象方法,位置0保留為 this。

包括如下變量類型:

  • boolean
  • byte
  • short
  • char
  • long
  • int
  • float
  • double
  • reference
  • returnAddress

局部變量表的容量機關是Slot,4個位元組,32位 。是以,除了long和double類型以外,所有的變量類型都占用一個slot。long和 double需要占用兩個連續的slot。

3.1.2.操作數棧

操作數棧用于存儲位元組碼執行過程中的操作數。當一個方法剛開始執行時,其操作數棧是空的,方法在執行的過程中會有各種位元組碼指令往操作數棧中寫入或讀取操作數。舉例來說,當執行一個整數加法的指令iadd時,執行引擎會将操作數棧棧頂的兩個元素取出(出棧),相加獲得結果後再壓入棧。

int i;

被編譯成下面的位元組碼:

0: iconst_0 // 把0壓入操作數棧頂

1: istore_1 // 從操作數棧頂取出元素0并存儲到局部變量表中1

3.1.3.動态連結

每個棧幀都包含一個指向運作時常量池中該棧幀所屬方法的引用,持有這個引用是為了支援方法調用過程中的動态連接配接。在Class檔案的常量池中存有大量的符号引用,位元組碼中的方法調用指令就以常量池中指向方法的符号引用為參數。這些符号引用一部分會在類加載階段或第一次使用的時候轉化為直接引用,這種轉化稱為靜态解析。另外一部分将在每一次的運作期期間轉化為直接引用,這部分稱為動态連接配接。

3.1.4.傳回位址

當一個方法執行完成後有兩種傳回方式:

  • 正常傳回:執行引擎執行到任意一個傳回的位元組碼指令
  • 異常傳回:在方法執行過程中遇到異常而退出。異常包括虛拟機内部異常以及代碼中使用athrow位元組碼抛出的異常

無論以何種方式傳回,方法退出前都需要回到方法被調用的位置。一般來說方法正常退出時,調用者PC計數器的值即為傳回位址;異常退出時,傳回位址通過異常處理器來确定。

4.本地方法棧(Native Stack)

什麼是本地方法(Native Method)?

簡單地講,一個Native Method就是一個java調用非java代碼的接口,它為我們提供了一個非常簡潔的接口,而且我們無需去了解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 ;
}
           

為什麼會調非Java的代碼?主要是由于JVM可能會和底層作業系統互動,作業系統的庫函數很有可能是C或者其他非Java語言實作的。

本地方法棧的作用和虛拟機棧的作用非常類似,隻不過是針對本地方法進行的操作。

虛拟機規範沒有對本地方法棧的實作語言、使用方式、資料結構做要求,是以具體的虛拟機可以自由實作。

和虛拟機棧一樣,本地方法棧也會抛出StackOverflowError和OutOfMemoryError。

5.方法區

即我們常說的永久代(Permanent Generation), 它是一段連續的記憶體空間,用于存儲被JVM加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。它還有個别名叫:非堆(non-heap),意思也就是能和堆區分開。

HotSpot VM把GC分代收集擴充至方法區, 即使用Java堆的永久代來實作方法區, 這樣HotSpot的垃圾收集器就可以像管理Java堆一樣管理這部分記憶體, 而不必為方法區開發專門的記憶體管理器(永久帶的記憶體回收的主要目标是針對常量池的回收和類型的解除安裝, 是以收益一般很小)

可以通過設定-XX:MaxPermSize的值來控制永久代的大小,32位機器預設的永久代的大小為64M,64位的機器則為85M。

永久代的垃圾回收和老年代的垃圾回收是綁定的,一旦其中一個區域被占滿,這兩個區都要進行垃圾回收。但是有一個明顯的問題,由于我們可以通過‑XX:MaxPermSize 設定永久代的大小,一旦類的中繼資料超過了設定的大小,程式就會耗盡記憶體,并出現記憶體溢出錯誤(OOM)。

不過在1.7的HotSpot已經将原本放在永久代的字元串常量池移出。

而在1.8中, 永久區已經被徹底移除, 取而代之的是元空間Metaspace。(詳細可參看後面的“元空間”一節)

5.1.Classloader 引用

所有的類加載之後都包含一個加載自身的加載器的引用,反過來每個類加載器都包含它們加載的所有類的引用。

5.2.運作時常量池

它用于存放編譯期生成的各種字面量和符号引用, 這部分内容會存放到方法區的運作時常量池中。但Java語言并不要求常量一定隻能在編譯期産生, 即并非預置入Class檔案中常量池的内容才能進入方法區運作時常量池, 運作期間也可能将新的常量放入池中, 如String的intern()方法。

字面量包括:

  • 類名
  • 變量名稱和值(執行個體、靜态變量)
  • 常量名和值
  • 方法名
  • 屬性名

如下代碼:

Object foo = new Object();
           

編譯成位元組碼:

0:    new #2             // Class java/lang/Object
1:    dup
2:    invokespecial #3    // Method java/ lang/Object "<init>"( ) V
           

new 操作碼的後面緊跟着操作數 #2 。這個操作數是常量池的一個索引,表示它指向常量池的第二個實體。第二個實體是一個類的引用,這個實體反過來引用了另一個在常量池中包含 UTF8 編碼的字元串類名的實體(// Class java/lang/Object)。然後,這個符号引用被用來尋找 java.lang.Object 類。new 操作碼建立一個類執行個體并初始化變量。新類執行個體的引用則被添加到操作數棧。

dup 操作碼建立一個操作數棧頂元素引用的額外拷貝。最後用 invokespecial 來調用第 2 行的執行個體初始化方法。操作碼也包含一個指向常量池的引用。初始化方法把操作數棧出棧的頂部引用當做此方法的一個參數。最後這個新對象隻有一個引用,這個對象已經完成了建立及初始化。

5.3.字段資料

  • 字段名
  • 類型
  • 修飾符
  • 屬性(Attribute)

5.4.方法資料

  • 方法名
  • 傳回值類型
  • 參數類型(按順序)
  • 修飾符
  • 屬性

5.5.方法代碼

  • 位元組碼
  • 操作數棧大小
  • 局部變量大小
  • 局部變量表

5.6.異常表

異常表像這樣存儲每個異常處理資訊:

  • 開始點
  • 結束點
  • 異常處理代碼的程式計數器(PC)偏移量
  • 被捕獲的異常類對應的常量池下标

如果一個方法有定義 try-catch 或者 try-finally 異常處理器,那麼就會建立一個異常表。它為每個異常處理器和 finally 代碼塊存儲必要的資訊,包括處理器覆寫的代碼塊區域和處理異常的類型。

當方法抛出異常時,JVM 會尋找比對的異常處理器。如果沒有找到,那麼方法會立即結束并彈出目前棧幀,這個異常會被重新抛到調用這個方法的方法中(在新的棧幀中)。如果所有的棧幀都被彈出還沒有找到比對的異常處理器,那麼這個線程就會終止。如果這個異常在最後一個非守護程序抛出(比如這個線程是主線程),那麼也有會導緻 JVM 程序終止。

Finally 異常處理器比對所有的異常類型,且不管什麼異常抛出 finally 代碼塊都會執行。在這種情況下,當沒有異常抛出時,finally 代碼塊還是會在方法最後執行。這種靠在代碼 return 之前跳轉到 finally 代碼塊來實作。

5.7.其他

所有線程共享同一個方法區,是以通路方法區資料的和動态連結的程序必須線程安全。如果兩個線程試圖通路一個還未加載的類的字段或方法,必須隻加載一次,而且兩個線程必須等它加載完畢才能繼續執行。

Oracle JVM 的 jconsle 顯示方法區和 code cache 區被當做為非堆記憶體,而 OpenJDK 則顯示 CodeCache 被當做 VM 中對象堆(ObjectHeap)的一個獨立的域。

6.程式計數器(PC)

一塊較小的記憶體空間, 作用是目前線程所執行位元組碼的行号訓示器。在JVM模型中, 位元組碼解釋器就是通過改變PC值來選取下一條需要執行的位元組碼指令,分支、循環、跳轉、異常處理、線程恢複等基礎功能都需要依賴PC完成。

  • 如果線程正在執行的是一個Java方法,則PC記錄的是虛拟機位元組碼指令的位址。
  • 如果執行的是Native方法,則PC的值為空(Undefined)。

不同于OS以程序為機關排程, JVM中的并發是通過線程切換并配置設定時間片執行來實作的. 在任何一個時刻, 一個處理器核心隻會執行一條線程中的指令。是以, 為了線程切換後能恢複到正确的執行位置, 每條線程都需要有一個獨立的程式計數器, 這類記憶體被稱為“線程私有”記憶體。

這塊記憶體區域也是虛拟機規範 唯一沒有定義OutOfMemoryError異常的區域。

7.元空間

在JDK8之前,類的中繼資料和常量都存放在一個與堆記憶體相鄰的資料區,即永久代。但是在這種情況下有一個問題,如果類的中繼資料大小超過了應用的可配置設定記憶體,那麼就會出現記憶體溢出問題。

在JDK8之後,永久代被移除,原本存儲在永久代的資料将存放在一個叫做元空間的本地記憶體區域。

java 記憶體模型詳解1.概述2.堆(Heap)3.Java虛拟機棧(JVM Stack)4.本地方法棧(Native Stack)5.方法區6.程式計數器(PC)7.元空間

元空間是存儲在本地記憶體的。

7.1.元空間的容量

  1. 預設情況下,類中繼資料隻受可用的本地記憶體限制。
  2. 參數MaxMetaspaceSize用于限制本地記憶體配置設定給類中繼資料的大小。如果沒有指定這個參數,元空間會在運作時根據需要動态調整。
  3. 如果GC發現某個類加載器不再存活了,會把相關的空間整個回收掉。

7.2.元空間的記憶體管理

元空間的記憶體管理由元空間虛拟機來完成。隻要類加載器存活,其加載的類的中繼資料也是存活的,因而不會被回收掉。

元空間虛拟機負責元空間的配置設定,其采用的形式為組塊配置設定。組塊的大小因類加載器的類型而異。在元空間虛拟機中存在一個全局的空閑組塊清單。當一個類加載器需要組塊時,它就會從這個全局的組塊清單中擷取并維持一個自己的組塊清單。當一個類加載器不再存活,那麼其持有的組塊将會被釋放,并傳回給全局組塊清單。類加載器持有的組塊又會被分成多個塊,每一個塊存儲一個單元的元資訊。組塊中的塊是線性配置設定(指針碰撞配置設定形式)。組塊配置設定自記憶體映射區域。這些全局的虛拟記憶體映射區域以連結清單形式連接配接,一旦某個虛拟記憶體映射區域清空,這部分記憶體就會傳回給作業系統。

java 記憶體模型詳解1.概述2.堆(Heap)3.Java虛拟機棧(JVM Stack)4.本地方法棧(Native Stack)5.方法區6.程式計數器(PC)7.元空間

上圖中,類加載器1和3使用了特定大小組塊。 而類加載器2和4使用小型或者中型的組塊。

7.3.元空間調優

對于一個64位的伺服器端JVM來說,其預設的–XX:MetaspaceSize值為21MB。這就是初始的高水位線。一旦觸及到這個水位線,Full GC将會被觸發并解除安裝沒有用的類(即這些類對應的類加載器不再存活),然後這個高水位線将會重置。新的高水位線的值取決于GC後釋放了多少元空間。如果釋放的空間不足,這個高水位線則上升。如果釋放空間過多,則高水位線下降。如果初始化的高水位線設定過低,上述高水位線調整情況會發生很多次。通過垃圾回收器的日志我們可以觀察到Full GC多次調用。

經過多次GC之後,元空間虛拟機自動調節高水位線,以此來推遲下一次垃圾回收到來。

7.4.MaxMetaspaceSize的調優

  • -XX:MaxMetaspaceSize={unlimited}
  • 限制類的中繼資料使用的記憶體大小,以免出現虛拟記憶體切換以及本地記憶體配置設定失敗。如果懷疑有類加載器出現洩露,應當使用這個參數;32位機器上,如果位址空間可能會被耗盡,也應當設定這個參數。
  • 元空間的初始大小是21M——這是GC的初始的高水位線,超過這個大小會進行Full GC來進行類的回收。
  • 如果啟動後GC過于頻繁,請将該值設定得大一些 可以設定成和持久代一樣的大小,以便推遲GC的執行時間

7.5.相關工具

  1. jmap -clstats PID 列印類加載器資料。(-clstats是-permstat的替代方案,在JDK8之前,-permstat用來列印類加載器的資料)。
  2. jstat -gc PID 用來列印元空間的資訊
# jstat -gc 1
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU
   CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
                  
                        
           

其中MC, MU的值就是元空間的資訊。

  • MC:Metaspace Capacity(KB) 目前元空間大小
  • MU: Metaspace Utilization(KB) 元空間使用

    jcmd PID GC.class_stats 一個新的診斷指令,用來連接配接到運作的JVM并輸出詳盡的類中繼資料的柱狀圖。

7.6.存在的問題

由于元空間虛拟機采用了組塊配置設定的形式,同時區塊的大小由類加載器類型決定。類資訊并不是固定大小,是以有可能配置設定的空閑區塊和類需要的區塊大小不同,這種情況下可能導緻碎片存在。元空間虛拟機目前并不支援壓縮操作,是以碎片化是目前最大的問題。