1. 程式人生 > >基於JDK8的JVM記憶體模型詳解與GC策略

基於JDK8的JVM記憶體模型詳解與GC策略

JVM記憶體模型總覽

首先看一下JVM記憶體模型圖

在這裡插入圖片描述

  • 程式計數器Program Counter Register 程式計數器是一塊較小的記憶體區,可以看做是當前執行緒所執行的位元組碼的行號指示器,如果執行緒正在執行一個JAVA方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址,如果正在執行的是NATIVE方法,這個計數器值為空(Undefined),此記憶體區域是唯一一個在JAVA虛擬機器規範中沒有規定任何OutOfMemoryError的區域

    注:這裡有問題是計數器值為空,程式怎麼往下執行 參考C++理解是:當執行緒中呼叫native方法的時候,則重新啟動一個新的執行緒,那麼新的執行緒的計數器為空則不會影響當前執行緒的計數器,相互獨立。

  • 虛擬機器棧VM Stack 描述的是JAVA方法執行的記憶體模型,每個方法在執行的時候都會建立一個棧幀,用於儲存區域性變量表,運算元棧,動態連結,方法介面等資訊 區域性變量表儲存了編譯期可知的各種基本資料型別(boolean, byte, char, short, int, float, double, long), 物件引用(reference型別和returnAddress型別(指向一條位元組碼指令的地址) 執行緒請求的棧深度不夠會報StackOverflowError異常 棧動態擴充套件的容量不夠會報OutOfMemoryError異常

  • 本地方法棧Native Stack 本地方法棧類似於虛擬機器棧,只不過本地方法棧使用的是本地方法

  • 堆Heap 幾乎所有的物件例項都在堆上分配記憶體, 圖示關於堆的結構 在這裡插入圖片描述

  1. JAVA物件優先在Eden區分配,當Eden區沒有足夠的空間時觸發一次Minor GC ,觸發Minor GC時,Eden和from區中的存活物件會被複制到to區,然後from和to交換指標,以保證下次Minor GC時,to區還是空的,如果survival區無法容納的物件將通過分配擔保機制直接進入老年區
  2. 分配擔保機制可以通過HandlePromotionFailure配置,如果不允許的話,則直接發生FULL GC
  3. 新生代(Young Generation)的最大大小將根據總堆的最大大小和NewRatio引數的值來計算。引數的“不受限制”預設值MaxNewSize意味著計算值不受限制,MaxNewSize除非MaxNewSize在命令列中指定了值
  4. 一般情況下,不允許-XX:Newratio值小於1,即Old要比Young大
  5. 大物件直接進入老年區的判斷是根據PretenureSizeThreshold設定的閾值,所謂大物件時指需要大量連續記憶體空間的Java物件,最典型的大物件就是那種很長的字串以及陣列(筆者列出的例子中的byte[]陣列就是典型的大物件)
  6. 發生full GC的條件是:
1)呼叫System.gc時,系統建議執行Full GC,但是不必然執行
(2)老年代空間不足
(3)方法去空間不足
(4)通過Minor GC後進入老年代的平均大小大於老年代的可用記憶體
(5)由Eden區、From Space區向To Space區複製時,物件大小大於To Space可用記憶體,則把該物件轉存到老年代,且老年代的可用記憶體小於該物件大小
  1. 物件存活判斷
- 引用計數:每個物件有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數為0時可以回收。此方法簡單,無法解決物件相互迴圈引用的問題  
- 可達性分析:從GC Roots開始向下搜尋,搜尋所走過的路徑稱為引用鏈。當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的,不可達物件  
  1. GC Roots物件包括
> 虛擬機器棧(棧幀中的本地變量表)中引用的物件  
> 方法區中類靜態屬性引用的物件
> 方法區中常量引用的物件  
> 本地方法棧中JNI(即一般說的Native方法)引用的物件
> 已啟動且未停止的java執行緒
  1. TLAB(Thread Local Allocation Buffer),即執行緒本地分配快取區,這是一個執行緒專用的記憶體分配區域,可以使用引數 -XX:+UseTLAB,預設開啟,這個是用於解決多執行緒競爭堆記憶體分配問題,核心原理是每個執行緒可以向JAVA虛擬機器申請一段連續的記憶體,作為執行緒私有的TLAB,這個操作需要加鎖
引數 預設值 作用
MinHeapFreeRatio 40 GC後,如果發現空閒堆記憶體小於整個預估堆記憶體的40%,則放大堆記憶體的預估最大值,但不超過固定最大值
MaxHeapFreeRatio 70 GC後,如果發現空閒堆記憶體佔到整個預估堆記憶體的70%,則收縮堆記憶體預估最大值
Xms 實體記憶體的1/64(<1GB) 初始堆大小
Xmx 實體記憶體的1/4(<1GB) 最大堆大小
NewRatio 2 年輕代(包括Eden和兩個Survivor區)與年老代的比值
NewSize 1310M 設定年輕代大小
MaxNewSize 不限 設定年輕代大小最大值
SurvivorRatio 8 Eden區與Survivor區的大小比值
MaxTenuringThreshold 15 垃圾最大年齡
PretenureSizeThreshold 0 超過這個值直接在old區分配,預設值是0,意思是不管多大都是先在eden中分配
  • 方法區Method Area JAVA虛擬機器規範把方法區描述為堆的一個邏輯部分,但是它卻有一個Non-Heap的別名,用於儲存已被虛擬機器載入的類資訊,常亮,靜態變數, 即時編譯器編譯後的程式碼等資料

在JDK 8中,永久代被刪除,類元資料在本機記憶體中分配。預設情況下,可用於類元資料的本機記憶體量是無限制的。使用該選項MaxMetaspaceSize可以為用於類元資料的本機記憶體量設定上限。


metaspace其實由兩大部分組成

Klass Metaspace就是用來存klass的,klass是我們熟知的class檔案在jvm裡的執行時資料結構,不過有點要提的是我們看到的類似A.class其實是存在heap裡的,是java.lang.Class的一個物件例項。這塊記憶體是緊接著Heap的,和我們之前的perm一樣,這塊記憶體大小可通過-XX:CompressedClassSpaceSize引數來控制,這個引數前面提到了預設是1G,但是這塊記憶體也可以沒有,假如沒有開啟壓縮指標就不會有這塊記憶體,這種情況下klass都會存在NoKlass Metaspace裡,另外如果我們把-Xmx設定大於32G的話,其實也是沒有這塊記憶體的,因為會這麼大記憶體會關閉壓縮指標開關。還有就是這塊記憶體最多隻會存在一塊。

NoKlass Metaspace專門來存klass相關的其他的內容,比如method,constantPool等,這塊記憶體是由多塊記憶體組合起來的,所以可以認為是不連續的記憶體塊組成的。這塊記憶體是必須的,雖然叫做NoKlass Metaspace,但是也其實可以存klass的內容,上面已經提到了對應場景。

Klass Metaspace和NoKlass Mestaspace都是所有classloader共享的,所以類載入器們要分配記憶體,但是每個類載入器都有一個SpaceManager,來管理屬於這個類載入的記憶體小塊。如果Klass Metaspace用完了,那就會OOM了,不過一般情況下不會,NoKlass Mestaspace是由一塊塊記憶體慢慢組合起來的,在沒有達到限制條件的情況下,會不斷加長這條鏈,讓它可以持續工作。

元空間的特點

- 充分利用了Java語言規範中的好處:類及相關的元資料的生命週期與類載入器的一致。
- 每個載入器有專門的儲存空間
- 只進行線性分配
- 不會單獨回收某個類
- 省掉了GC掃描及壓縮的時間
- 元空間裡的物件的位置是固定的
- 如果GC發現某個類載入器不再存活了,會把相關的空間整個回收掉

元空間的記憶體分配模型

- 絕大多數的類元資料的空間都從本地記憶體中分配
- 用來描述類元資料的類(klasses)也被刪除了
- 分元資料分配了多個虛擬記憶體空間
- 給每個類載入器分配一個記憶體塊的列表。塊的大小取決於類載入器的型別; sun/反射/代理對應的類載入器的塊會小一些
- 歸還記憶體塊,釋放記憶體塊列表
- 一旦元空間的資料被清空了,虛擬記憶體的空間會被回收掉
- 減少碎片的策略
  • 執行時常量池Runtime Constant Pool 執行時常量池時方法區中的一部分,用於存放編譯期生成的各種字面量和符號引用,並不是只有編譯期才能產生常量,執行期間也有可能將新的常量放入常量池,因此也會有可能丟擲OutOfMemoryError異常 常見的有字串常量池

參考: [1] 深入理解Java虛擬機器第二版

歡迎關注微信交流 在這裡插入圖片描述