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

效能測試系列-java gc調優

效能測試中除了需要做好效能測試外,我們還需要做效能測試後的,效能調優,需要發現效能問題,也需要做效能調優,在做效能調優中,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 對大堆非常友好,其執行機制也需要浪費一定的空間,有時候稍微多給堆一些空間,比進行苛刻的調優更加實用。