1. 程式人生 > >JVM記憶體模型及三種GC的詳解

JVM記憶體模型及三種GC的詳解

(本文基於JDK6)

說到GC,首先要對Java 的記憶體模型有所瞭解。

Java 的記憶體模型各個代的預設排列有如下圖(適用JDK1.4.*  到 JDK6):

Java 的記憶體模型分為

Young(年輕代)

Tenured(終身代)

Perm(永久代)

更多關於記憶體模型的文章看這裡:

在堆記憶體中的GC可以分為Minor GC(次要GC)和 Major GC(主要GC),次要GC是在年輕代進行收集的GC,職責是在Eden區滿的時候收集dead的物件和轉移存活的物件;主要GC是在終生代滿時進行收集的GC,主要GC較次要GC需要更多時間。 使用jvm引數-verbose:gc 就可以輸出每一次GC的詳細資訊。

你可能在程式執行起來以後看到如下輸出:

[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]




你可以看到兩次Minor GC(次要GC)和一次Major GC(主要GC)。

325407K->83000K   箭頭前後的數字分別代表收集前和收集後的堆記憶體佔用情況。 (776768K) 括號內的數字代表總共分配的堆記憶體空間,注意,這個值不包括其中一個Survior空間,也不包括 permanent generation(永久代)。

最後的時間0.2300771 secs指的是GC所耗費的時間。

如果執行時加上VM引數-XX:+PrintGCDetails    將輸出更詳細的資訊。如下顯示了Eden區和Heap記憶體在GC前後的變化:

[GC [DefNew: 64575K->959K(64576K), 0.0457646 secs] 196016K->133633K(261184K), 0.0459067 secs]




如果執行時加上VM引數-XX:+PrintGCTimeStamps 則可以得到GC發生的時間。

以下輸出顯示了在程式執行到111.042 秒的時候發生的GC,包括一次在Eden區的次要GC和發生在Tenured區的主要GC:

111.042: [GC 111.042: [DefNew: 8128K->8128K(8128K), 0.0000505 secs]111.042: [Tenured: 18154K->2311K(24576K), 0.1290354 secs] 26282K->2311K(32704K), 0.1293306 secs]

GC效能主要的衡量指標有兩個:Throughput和Pauses。吞吐量(Throughput)是不做GC的時間與總時間的百分比,分子包括分配記憶體空間的時間。中斷(Pauses)是測量時間段內由於GC而導致的應用暫停次數。 對使用者而言,對GC的需求往往是不一樣的。一般的web應用對吞吐量要求不高,由於GC而引起的偶爾中斷也是可以容忍的。然而一個互動性強的實時應用系統來說,經常性的中斷將帶來糟糕的使用者體驗。 即時性(Promptness)和足印(footprint)也是某些使用者考慮的問題。 即時性是物件死去到所佔記憶體釋放的時間間隔,這個指數是分散式應用如使用RMI的分散式應用的一項需要考慮的因素。足印是一種過程的集合,代表可伸縮性。

HOTSPOT JVM總共擁有3種不同的GC,各有各總自的特點和應用場景:

  • serial collector (序列GC)任何時刻都是使用一個執行緒執行GC操作,這種GC線上程間通訊沒有大的開銷的應用會有相對不錯的執行效率。最適合單處理器的系統;多處理器系統對這種GC而言並不能提升收集的效率。JVM預設情況下就是使用這個GC,這種GC有個形象的別名叫做"stop-the-world",當JVM在用這個GC收集垃圾的時候,你的app別想幹其他事。你也可以用這個引數 -XX:+UseSerialGC 顯式的宣告使用。 預設的serial gc可以應付絕大多數的app。除非以下情況:這是一個執行在大記憶體多處理器的機器上的多執行緒的大應用。
  • parallel collector (並行GC,或者叫 throughput collector ) 會以並行的方式執行minor collections(次級GC), 能較大的減少GC的開銷。其誕生的初衷就是專門給執行在多處理器,多執行緒硬體上的中大型應用的。在特定的硬體和OS環境條件下這是預設選項,顯式宣告使用 -XX:+UseParallelGC 引數。始於JDK1.3.1。 parallel compaction 是在 J2SE 5.0 update 6引入的新特性,並在Java SE 6 得到增強。它允許使用並行的方式執行Major collections(主要GC)。如果不開啟parallel compaction, major collections 將以單一執行緒的方式執行。 通過引數 -XX:+UseParallelOldGC 顯式使用該特性。
  • concurrent collector (同步GC)同時執行大多數的任務 (GC的同時應用也在執行)來保證GC引起的中斷時間儘量的短。主要應用在實時性要求重於總體吞吐量要求的中大型應用,即使如此,降低中斷時間的技術還是會導致應用程式效能的少許降低。可以使用引數 -XX:+UseConcMarkSweepGC 使用該特性。

parallel collector的一些注意點:

parallel collector從JDK5開始就是Server端JVM的預設選擇,需要加VM引數 -server 。

parallel collector還採用一些細節調優的策略如:

  • 限定最大GC中斷時間
  • 吞吐量限定
  • 足印(伸縮量)設定

可以用vm引數 -XX:MaxGCPauseMillis=<N> 來限定最大GC中斷時間,單位是ms,規定GC產生的中斷時間不能超過指定的時間。預設沒有這個限制。一旦使用了這個引數,heap空間和其他相關引數會做出相應的調整來滿足最大GC中斷時間的要求。

吞吐量限定使用-XX:GCTimeRatio=<N> 來設定GC時間的比率,N 的值 = 沒有花在GC上的時間/GC的時間 因此GC的時間佔用總時間的百分比公式= 1 / (1 + <N>) 。比如 -XX:GCTimeRatio=19 意味著將有1/20的時間花在GC上。預設值=99。

足印(伸縮量)實際上就是heap堆記憶體的調整。最大Heap容量使用引數 -Xmx<N> 宣告。

以上引數中任何一個的改動,都會引起另外兩個的改變。三者的優先順序如上順序一至。

如果太多時間黑白花費在GC上,parallel collector將丟擲OOM,這臨界值大概是98%;也可以使用-XX:-UseGCOverheadLimit 關閉這個特性。

concurrent collector的一些注意點:

不適用於單處理器的系統,事實上在單處理器系統上執行concurrent collector 效率反而降低。如果只能執行在單處理器的系統上,那記得開啟增量模式(incremental mode)。

前面幾種GC都是在Tenured區滿了以後觸發主要GC操作;concurrent collector卻是在Tenured區滿溢之前就進行主要GC。如果concurrent collector沒有趕在Tenured區滿前收集完或者還沒有開始收集的話,就會產生長時間的中斷。引數-XX:CMSInitiatingOccupancyFraction=<N> 可以指定觸發主要GC的臨界值,N(0-100)代表的是Tenured區飽和程度百分比。一旦Tenured區飽和程度達到這個臨界值,主要GC就發生了。

concurrent collection的生命週期一般包括如下幾步:

  • 停止應用的所有執行緒,標識出所有可到達的物件集合,然後恢復應用的所有執行緒
  • 使用一個或幾個處理器資源同步跟蹤可到達的物件,應用執行緒同時執行
  • 使用一個處理器資源同步地重新定位那些自從上一個步驟以來可能修改過的物件
  • 停止應用的所有執行緒,重新定位那些自從上一個步驟以來可能修改過的物件,然後恢復應用的所有執行緒
  • 使用一個處理器資源同步地收集不被引用的物件
  • 使用一個處理器資源同步地重新定義堆記憶體,並且為下一次GC生命週期作好資料準備

concurrent collection在整個收集的過程中,至少會佔有一到兩個處理器,而且不會自動放棄佔有的處理器資源。

這個特性會讓只有一到兩個處理器的系統很難過。為處理這個問題,需要藉助增量模式(incremental mode)。

增量模式 的核心思想是 將整個GC生命週期分解成一段段的時間塊分步進行,以此減少中斷的時間,但是不可避免的是伴隨著吞吐量的下降。

使用引數 -XX:+CMSIncrementalMode 開啟增量模式。