1. 程式人生 > >性能測試系列-java gc調優

性能測試系列-java gc調優

操作 發生 失敗 應用 目標 社區 為什麽 不知道 current

性能測試中除了需要做好性能測試外,我們還需要做性能測試後的,性能調優,需要發現性能問題,也需要做性能調優,在做性能調優中,jvm的性能調優是經常遇到的一個。

隨著jdk版本的迅速變化,jdk裏面的GC算法也是發生了很多變化,新版的jdk中,G1的已經成了jdk的默認算法了,性能測試中,我們經常關註的比較多的就是tps,吞吐率,內存占用,CPU占用,響應時間,其中GC

的回收對響應時間有非常大的影響,早期的GC回收,基本都會造成很長時間的Stop-The-World 的暫停,新GC算法很多都是圍繞降低Stop-The-World 的暫停時間,使得平均響應時間盡量變短,TPS提升的更高。

從內存區域的角度,G1 同樣存在著年代的概念,但是與我前面介紹的內存結構很不一樣,其內部是類似棋盤狀的一個個 region 組成,請參考下面的示意圖。

備註:摘選自:Java GC調優怎麽做?楊曉峰 出處 | 極客時間《Java 核心技術 36 講》專欄

技術分享圖片

region 的大小是一致的,數值是在 1M 到 32M 字節之間的一個 2 的冪值數,JVM 會盡量劃分 2048 個左右、同等大小的 region,這點可以從源碼 heapRegionBounds.hpp 中看到。當然這個數字既可以手動調整,G1 也會根據堆大小自動進行調整。

在 G1 實現中,年代是個邏輯概念,具體體現在,一部分 region 是作為 Eden,一部分作為 Survivor,除了意料之中的 Old region,G1 會將超過 region 50% 大小的對象(在應用中,通常是 byte 或 char 數組)歸類為 Humongous 對象,並放置在相應的 region 中。邏輯上,Humongous region 算是老年代的一部分,因為復制這樣的大對象是很昂貴的操作,並不適合新生代 GC 的復制算法。

region 設計本身可能存儲在的不足:

region 大小和大對象很難保證一致,這會導致空間的浪費。不知道你有沒有註意到,我的示意圖中有的區域是 Humongous 顏色,但沒有用名稱標記,這是為了表示,特別大的對象是可能占用超過一個 region 的。並且,region 太小不合適,會令你在分配大對象時更難找到連續空間,這是一個長久存在的情況,請參考 OpenJDK 社區的討論。這本質也可以看作是 JVM 的 bug,盡管解決辦法也非常簡單,直接設置較大的 region 大小,參數如下:

-XX:G1HeapRegionSize=<N, 例如 16>M

從 GC 算法的角度,G1 選擇的是復合算法,可以簡化理解為:

  • 在新生代,G1 采用的仍然是並行的復制算法,所以同樣會發生 Stop-The-World 的暫停。

  • 在老年代,大部分情況下都是並發標記,而整理(Compact)則是和新生代 GC 時捎帶進行,並且不是整體性的整理,而是增量進行的。

在過去,我們一般將年輕代(新生代)的GC稱為Minor GC,老年代 GC 叫作 Major GC,全局整體性的GC叫做full GC,但是新版jdk版本中,已經和過去有了很大的不同了,對於我們講的G1算法來說:

  • Minor GC 仍然存在,雖然具體過程會有區別,會涉及 Remembered Set 等相關處理。

  • 老年代回收,則是依靠 Mixed GC。並發標記結束後,JVM 就有足夠的信息進行垃圾收集,Mixed GC 不僅同時會清理 Eden、Survivor 區域,而且還會清理部分 Old 區域。可以通過設置下面的參數,指定觸發閾值,並且設定最多被包含在一次 Mixed GC 中的 region 比例。

–XX:G1MixedGCLiveThresholdPercent
–XX:G1OldCSetRegionThresholdPercent


從 G1 內部運行的角度,下面的示意圖描述了 G1 正常運行時的狀態流轉變化,當然,在發生逃逸失敗等情況下,就會觸發 Full GC。

技術分享圖片

在G1中出現了很多的新概念,比如Remembered Set,用於記錄和維護 region 之間對象的引用關系。為什麽需要這麽做呢?試想,新生代 GC 是復制算法,也就是說,類似對象從 Eden 或者 Survivor 到 to 區域的“移動”,其實是“復制”,本質上是一個新的對象。在這個過程中,需要必須保證老年代到新生代的跨區引用仍然有效。下面的示意圖說明了相關設計。

備註:摘選自:Java GC調優怎麽做?楊曉峰 出處 | 極客時間《Java 核心技術 36 講》專欄

技術分享圖片

G1 的很多開銷都是源自 Remembered Set,例如,它通常約占用 Heap 大小的 20% 或更高,這可是非常可觀的比例。並且,我們進行對象復制的時候,因為需要掃描和更改 Card Table 的信息,這個速度影響了復制的速度,進而影響暫停時間。

在G1 算法中記錄了老年代 region 間對象引用,Humongous 對象數量有限,所以能夠快速的知道是否有老年代對象引用它。如果沒有,能夠阻止它被回收的唯一可能,就是新生代是否有對象引用了它,但這個信息是可以在 Young GC 時就知道的,所以完全可以在 Young GC 中就進行 Humongous 對象的回收,不用像其他老年代對象那樣,等待並發標記結束。

  • 8u20 以後字符串排重的特性,在垃圾收集過程中,G1 會把新創建的字符串對象放入隊列中,然後在 Young GC 之後,並發地(不會 STW)將內部數據(char 數組,JDK 9 以後是 byte 數組)一致的字符串進行排重,也就是將其引用同一個數組。你可以使用下面參數激活:

-XX:+UseStringDeduplication


這種排重雖然可以節省不少內存空間,但這種並發操作會占用一些 CPU 資源,也會導致 Young GC 稍微變慢。

通過
-XX:+TraceClassUnloading
可以查看到G1 算法的類型卸載方式,8u40 以後的jdk版本中,G1 增加並默認開啟下面的選項:
-XX:+ClassUnloadingWithConcurrentMark

在並發標記階段結束後,JVM 即進行類型卸載,並不會在發生了full GC才進行類型卸載。

在之前的jdk版本中,老年代對象回收,基本要等待並發標記結束。這意味著,如果並發標記結束不及時,導致堆已滿,但老年代空間還沒完成回收,就會觸發 Full GC,所以觸發並發標記的時機很重要。早期的 G1 調優中,通常會設置下面參數,但是很難給出一個普適的數值,往往要根據實際運行結果調整.

-XX:InitiatingHeapOccupancyPercent


在 JDK 9 之後的 G1 實現中,這種調整需求會少很多,因為 JVM 只會將該參數作為初始值,會在運行時進行采樣,獲取統計數據,然後據此動態調整並發標記啟動時機。對應的 JVM 參數如下,默認已經開啟:


-XX:+G1UseAdaptiveIHOP
在新的jdk中,full gc已經從單線程串行 GC 變更為了並行進行了,在通用場景中的表現還優於 Parallel GC 的 Full GC 實現。

性能GC調優一些建議如下:(摘選自:Java GC調優怎麽做?楊曉峰 出處 | 極客時間《Java 核心技術 36 講》

1、首先,建議盡量升級到較新的 JDK 版本,從上面介紹的改進就可以看到,很多人們常常討論的問題,其實升級 JDK 就可以解決了。
2、掌握 GC 調優信息收集途徑。掌握盡量全面、詳細、準確的信息,是各種調優的基礎,不僅僅是 GC 調優。我們來看看打開 GC 日誌,這似乎是很簡單的事情,可是你確定真的掌握了嗎?
除了常用的兩個選項,
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps


還有一些非常有用的日誌選項,很多特定問題的診斷都是要依賴這些選項:

-XX:+PrintAdaptiveSizePolicy   // 打印 G1 Ergonomics 相關信息


我們知道 GC 內部一些行為是適應性的觸發的,利用 PrintAdaptiveSizePolicy,我們就可以知道為什麽 JVM 做出了一些可能我們不希望發生的動作。例如,G1 調優的一個基本建議就是避免進行大量的 Humongous 對象分配,如果 Ergonomics 信息說明發生了這一點,那麽就可以考慮要麽增大堆的大小,要麽直接將 region 大小提高。


如果是懷疑出現引用清理不及時的情況,則可以打開下面選項,掌握到底是哪裏出現了堆積。

-XX:+PrintReferenceGC


另外,建議開啟選項下面的選項進行並行引用處理。

-XX:+ParallelRefProcEnabled

需要註意的一點是,JDK 9 中 JVM 和 GC 日誌機構進行了重構,其實我前面提到的

PrintGCDetails 已經被標記為廢棄,而 PrintGCDateStamps 已經被移除,指定它會導致 JVM 無法啟動。可以使用下面的命令查詢新的配置參數。

java -Xlog:help


最後,來看一些通用實踐,理解了我前面介紹的內部結構和機制,很多結論就一目了然了,例如

  • 如果發現 Young GC 非常耗時,這很可能就是因為新生代太大了,我們可以考慮減小新生代的最小比例。

-XX:G1NewSizePercent

降低其最大值同樣對降低 Young GC 延遲有幫助。

-XX:G1MaxNewSizePercent


如果我們直接為 G1 設置較小的延遲目標值,也會起到減小新生代的效果,雖然會影響吞吐量。

  • 如果是 Mixed GC 延遲較長,我們應該怎麽做呢?

還記得前面說的,部分 Old region 會被包含進 Mixed GC,減少一次處理的 region 個數,就是個直接的選擇之一。

我在上面已經介紹了 G1OldCSetRegionThresholdPercent 控制其最大值,還可以利用下面參數提高 Mixed GC 的個數,當前默認值是 8,Mixed GC 數量增多,意味著每次被包含的 region 減少。

-XX:G1MixedGCCountTarget


需要註意的是,要避免過度調優,G1 對大堆非常友好,其運行機制也需要浪費一定的空間,有時候稍微多給堆一些空間,比進行苛刻的調優更加實用。






性能測試系列-java gc調優