1. 程式人生 > 實用技巧 >GC(Allocation Failure)引發的一些JVM知識點梳理

GC(Allocation Failure)引發的一些JVM知識點梳理

日前檢視某個程式的日誌,發現一直在報GC相關的資訊,不確定這樣的資訊是代表正確還是不正確,所以正好藉此機會再複習下GC相關的內容:

以其中一行為例來解讀下日誌資訊:

[GC (Allocation Failure) [ParNew: 367523K->1293K(410432K), 0.0023988 secs] 522739K->156516K(1322496K), 0.0025301 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]


GC:
表明進行了一次垃圾回收,前面沒有Full修飾,表明這是一次Minor GC ,注意它不表示只GC新生代,並且現有的不管是新生代還是老年代都會STW



Allocation Failure:
表明本次引起GC的原因是因為在年輕代中沒有足夠的空間能夠儲存新的資料了。

ParNew:
表明本次GC發生在年輕代並且使用的是ParNew垃圾收集器。ParNew是一個Serial收集器的多執行緒版本,會使用多個CPU和執行緒完成垃圾收集工作(預設使用的執行緒數和CPU數相同,可以使用-XX:ParallelGCThreads引數限制)。該收集器採用複製演算法回收記憶體,期間會停止其他工作執行緒,即Stop The World。

367523K->1293K(410432K):單位是KB
三個引數分別為:GC前該記憶體區域(這裡是年輕代)使用容量,GC後該記憶體區域使用容量,該記憶體區域總容量。

0.0023988 secs:
該記憶體區域GC耗時,單位是秒

522739K->156516K(1322496K):
三個引數分別為:堆區垃圾回收前的大小,堆區垃圾回收後的大小,堆區總大小。

0.0025301 secs:
該記憶體區域GC耗時,單位是秒

[Times: user=0.04 sys=0.00, real=0.01 secs]:
分別表示使用者態耗時,核心態耗時和總耗時

分析下可以得出結論:
該次GC新生代減少了367523-1293=366239K
Heap區總共減少了522739-156516=366223K
366239 – 366223 =16K,說明該次共有16K記憶體從年輕代移到了老年代,可以看出來數量並不多,說明都是生命週期短的物件,只是這種物件有很多。

我們需要的是儘量避免Full GC的發生,讓物件儘可能的在年輕代就回收掉,所以這裡可以稍微增加一點年輕代的大小,讓那17K的資料也儲存在年輕代中。

GC時,用什麼方法判斷哪些物件是需要回收:

  1. 引用計數法(已經不用了)
  2. 可達性分析法

前一種簡而言之就是給物件新增一個引用計數器,有其他地方引用時這個計數器+1,引用失效時-1,為0時就可以刪除掉了。但是它不能解決迴圈引用的問題,所以一般使用的都是後一種演算法。

可達性分析法的基本思路就是通過一系列名為GC Roots的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的,那就可以回收掉了。

GC Roots一般都是些堆外指向堆內的引用,例如:

  • JVM棧中引用的物件
  • 方法區中靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 本地方法棧中引用的物件



以CMS為例,補充一些知識點介紹:

複製演算法介紹
因為新生代物件生命週期一般很短,現在一般將該記憶體區域劃分為三塊部分,一塊大的叫Eden,兩塊小的叫Survivor。他們之間的比例一般為8:1:1。

使用的時候只使用Eden + 一塊Survivor。用Eden區用滿時會進行一次minor gc,將存活下面的物件複製到另外一塊Survivor上。如果另一塊Survivor放不下(對應虛擬機器引數為 XX:TargetSurvivorRatio,預設50,即50%),物件直接進入老年代
(使用CMS時,預設的新生代收集器是ParNew)(有時新生代GC時,需要找到老年代中引用的新生代物件,這個時候會用到一種叫“卡表”的技術,避免老年代的全表掃描,具體怎麼操作的暫時還不知道……)

Survivor區的意義:
如果沒有survivor,Eden每進行一次minor gc,存活的物件就會進入老年代,老年代很快被填滿就會進入major gc。由於老年代空間一般很大,所以進行一次gc耗時要長的多!尤其是頻繁進行full GC,對程式的響應和連線都會有影響!
Survivor存在就是減少被送到老年代的物件,進而減少Full gc的發生。預設設定是經歷了16次minor gc還在新生代中存活的物件才會被送到老年代。


為什麼要有兩個Survivor:
主要是為了解決記憶體碎片化和效率問題。如果只有一個Survivor時,每觸發一次minor gc都會有資料從Eden放到Survivor,一直這樣迴圈下去。注意的是,Survivor區也會進行垃圾回收,這樣就會出現記憶體碎片化問題。如下圖所示:

碎片化會導致堆中可能沒有足夠大的連續空間存放一個大物件,影響程式效能。如果有兩塊Survivor就能將剩餘物件集中到其中一塊Survivor上,避免碎片問題。如下圖所示:

Minor GC和Full GC的區別以及觸發條件
Minor GC:
對於複製演算法來說,當年輕代Eden區域滿的時候會觸發一次Minor GC,將Eden和From Survivor的物件複製到另外一塊To Survivor上。
注意:如果某個物件存活的時間超過一定Minor gc次數會直接進入老年代,不在分配到To Survivor上(預設15次,對應虛擬機器引數 -XX:+MaxTenuringThreshold)。

Full GC:
用於清理整個堆空間。它的觸發條件主要有以下幾種:
顯式呼叫System.gc方法(建議JVM觸發)。
方法區空間不足(JDK8及之後不會有這種情況了,詳見下文)
老年代空間不足,引起Full GC。這種情況比較複雜,有以下幾種:

3.1 大物件直接進入老年代引起,由-XX:PretenureSizeThreshold引數定義
3.2 Minor GC時,經歷過多次Minor GC仍存在的物件進入老年代。上面提過,由-XX:MaxTenuringThreashold引數定義
3.3 Minor GC時,動態物件年齡判定機制會將物件提前轉移老年代。年齡從小到大進行累加,當加入某個年齡段後,累加和超過survivor區域 * -XX:TargetSurvivorRatio的時候,從這個年齡段往上的年齡的物件進入老年代
3.4 Minor GC時,Eden和From Space區向To Space區複製時,大於To Space區可用記憶體,會直接把物件轉移到老年代


JVM的空間分配擔保機制可能會觸發Full GC:
在進行Minor GC之前,JVM的空間擔保分配機制可能會觸發3.2、3.3和3.4發生,即觸發一次Full GC。
空間擔保分配是指在發生Minor GC之前,虛擬機器會檢查老年代最大可用的連續空間是否大於新生代所有物件的總空間。
如果大於,則此次Minor GC是安全的。
如果小於,則虛擬機器會檢視HandlePromotionFailure設定值是否允許擔保失敗。如果HandlePromotionFailure=true,那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的物件的平均大小,如果大於,則嘗試進行一次Minor GC,但這次Minor GC依然是有風險的,失敗後會重新發起一次Full gc;如果小於或者HandlePromotionFailure=false,則改為直接進行一次Full GC。

所有才會說一次Full GC很有可能是由一次Minor GC觸發的。



JDK8中HotSpot為什麼要取消永久代
JDK8取消了永久代,新增了一個叫元空間(Metaspace)的區域,對應的還是JVM規範中的方法區(主要存放一些class和元資料的資訊)。區別在於元空間使用的並不是JVM中的記憶體,而是使用本地記憶體
而這麼做的原因大致有以下幾點:
1、字串存在永久代中,容易出現效能問題和記憶體溢位。
  2、類及方法的資訊等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢位,太大則容易導致老年代溢位。
  3、永久代會為 GC 帶來不必要的複雜度,並且回收效率偏低。
  4、Oracle 可能會將HotSpot 與 JRockit 合二為一。

補充下JDK8記憶體模型圖:

轉自:https://blog.csdn.net/zc19921215/article/details/83029952