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

OkidoGreen發表於2020-04-05

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

日前檢視某個程式的日誌,發現一直在報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 =17K,說明該次共有17K記憶體從年輕代移到了老年代,可以看出來數量並不多,說明都是生命週期短的物件,只是這種物件有很多。

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

 

GC時,用什麼方法判斷哪些物件是需要回收:
引用計數法(已經不用了)
可達性分析法
前一種簡而言之就是給物件新增一個引用計數器,有其他地方引用時這個計數器+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和Survivor的物件複製到另外一塊Survivor上,並且某個物件存活的時間超過一定minor gc次數直接進入老年代(預設15次,對應虛擬機器引數 -XX:+MaxTenuringThreshold)。

    Full gc:(又叫major gc)

用於回收老年代。當老年代空間不足或者直接呼叫System.gc(不一定有用)時,會進行一次Full gc。(HotSpot還有一些其他複雜的觸發條件,JDK8之前HotSpot的JVM中還有一個永久代(Perm區),如果永久代記憶體不足也會觸發Full gc。永久代主要存放一些class和後設資料的資訊 ---- 對應JVM規範中的方法區)

 一次Full gc很有可能會由一次minor gc觸發,也可能是無法找到一塊連續的空間分配給大物件而觸發。

 

PS:JDK8中HotSpot為什麼要取消永久代

    JDK8取消了永久代,新增了一個叫元空間(Metaspace)的區域,對應的還是JVM規範中的方法區。區別在於元空間使用的並不是JVM中的記憶體,而是使用本地記憶體。

    而這麼做的原因大致有以下幾點:

    1、字串存在永久代中,容易出現效能問題和記憶體溢位。

  2、類及方法的資訊等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢位,太大則容易導致老年代溢位。

  3、永久代會為 GC 帶來不必要的複雜度,並且回收效率偏低。

  4、Oracle 可能會將HotSpot 與 JRockit 合二為一。

 

補充下JDK8記憶體模型圖:

參考:

 

https://blog.csdn.net/antony9118/article/details/51425581(兩個Survivor的意義)

https://zhidao.baidu.com/question/1111800566588999699.html(Full GC什麼時候觸發)

https://blog.csdn.net/l1394049664/article/details/81486470#%E4%BA%94%E3%80%81java8%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%E5%9B%BE(JDK8記憶體模型圖)

https://blog.csdn.net/quinnnorris/article/details/75040538(可達性分析演算法解析)

https://segmentfault.com/a/1190000007726689(卡表是什麼)

相關文章