日前查看某個程序的日志,發現一直在報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,引用失效時-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