28、談談你的GC調優思路?

目錄

今天我要問你的問題是,談談你的 GC 調優思路?

典型回答

考點分析

知識擴展

首先,先來整體瞭解一下 G1 GC 的內部結構和主要機制。

你可以思考下 region 設計有什麼副作用?

前面介紹了 G1 的內部機制,並且穿插了部分調優建議,下面從整體上給出一些調優的建議。

一課一練


我發現,目前不少外部資料對 G1 的介紹大多還停留在 JDK 7 或更早期的實現,很多結論已經存在較大偏差,甚至一些過去的 GC 選項已經不再推薦使用。所以,今天我會選取新版 JDK 中的默認 G1 GC 作爲重點進行詳解,並且我會從調優實踐的角度,分析典型場景和調優思路。下面我們一起來更新下這方面的知識。

今天我要問你的問題是,談談你的 GC 調優思路?

典型回答

談到調優,這一定是針對特定場景、特定目的的事情, 對於 GC 調優來說,首先就需要清楚調優的目標是什麼?從性能的角度看,通常關注三個方面,內存佔用(footprint)、延時(latency)和吞吐量(throughput),大多數情況下調優會側重於其中一個或者兩個方面的目標,很少有情況可以兼顧三個不同的角度。當然,除了上面通常的三個方面,也可能需要考慮其他;

GC 相關的場景,例如,OOM 也可能與不合理的 GC 相關參數有關;或者,應用啓動速度方面的需求,GC 也會是個考慮的方面。

基本的調優思路可以總結爲:

  •   理解應用需求和問題,確定調優目標。假設,我們開發了一個應用服務,但發現偶爾會出現性能抖動,出現較長的服務停頓。評估用戶可接受的響應時間和業務量,將目標簡化爲,希望 GC 暫停儘量控制在 200ms 以內,並且保證一定標準的吞吐量。

 

  •   掌握 JVM 和 GC 的狀態,定位具體的問題,確定真的有 GC 調優的必要。具體有很多方法,比如,通過 jstat 等工具查看 GC 等相關狀態,可以開啓  GC 日誌,或者是利用操作系統提供的診斷工具等。例如,通過追蹤 GC 日誌,就可以查找是不是 GC 在特定時間發生了長時間的暫停,進而導致了應用響應不及時。

 

  •   這裏需要思考,選擇的 GC 類型是否符合我們的應用特徵,如果是,具體問題表現在哪裏,是 Minor GC 過長,還是 Mixed GC 等出現異常停頓情況;如果不是,考慮切換到什麼類型,如 CMS 和 G1 都是更側重於低延遲的 GC 選項。

 

  •   通過分析確定具體調整的參數或者軟硬件配置。

 

  •   驗證是否達到調優目標,如果達到目標,即可以考慮結束調優;否則,重複完成分析、調整、驗證這個過程。

 

考點分析

今天考察的 GC 調優問題是 JVM 調優的一個基礎方面,很多 JVM 調優需求,最終都會落實在 GC 調優上或者與其相關,我提供的是一個常見的思路。

真正快速定位和解決具體問題,還是需要對 JVM 和 GC 知識的掌握,以及實際調優經驗的總結,有的時候甚至是源自經驗積累的直覺判斷。面試官可能會繼續問項目中遇到的真實問題,如果你能清楚、簡要地介紹其上下文,然後將診斷思路和調優實踐過程表述出來,會是個很好的加分項。

 

專欄雖然無法提供具體的項目經驗,但是可以幫助你掌握常見的調優思路和手段,這不管是面試還是在實際工作中都是很有幫助的。另外,我會還會從下面不同角度進行補充:

  •   上一講中我已經談到,涉及具體的 GC 類型,JVM 的實際表現要更加複雜。目前,G1 已經成爲新版 JDK 的默認選擇,所以值得你去深入理解。
  •   因爲 G1 GC 一直處在快速發展之中,我會側重它的演進變化,尤其是行爲和配置相關的變化。並且,同樣是因爲 JVM 的快速發展,即使是收集 GC 日誌等方面也發生了較大改進,這也是爲什麼我在上一講留給你的思考題是有關日誌相關選項,看完講解相信你會很驚訝。
  •   從 GC 調優實踐的角度,理解通用問題的調優思路和手段。

 

知識擴展

首先,先來整體瞭解一下 G1 GC 的內部結構和主要機制。

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

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(Young GC)叫作 Minor GC,老年代 GC 叫作 Major GC,區別於整體性的 Full GC。但是現代 GC 中,這種概念已經不再準確,對於 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 區域的“移動”,其實是“複製”,本質上是一個新的對象。在這個過程中,需要必須保證老年代到新生代的跨區引用仍然有效。下面的示意圖說明了相關設計。


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

描述 G1 內部的資料很多,我就不重複了,如果你想了解更多內部結構和算法等,我建議參考一些具體的介紹,書籍方面我推薦 Charlie Hunt 等撰寫的《Java Performance Companion》。

接下來,我介紹下大家可能還不瞭解的 G1 行爲變化,它們在一定程度上解決了專欄其他講中提到的部分困擾,如類型卸載不及時的問題。

  •   上面提到了 Humongous 對象的分配和回收,這是很多內存問題的來源,Humongous region  作爲老年代的一部分,通常認爲它會在併發標記結束後才進行回收,但是在新版 G1 中,Humongous 對象回收採取了更加激進的策略。
  •    我們知道 G1 記錄了老年代 region 間對象引用,Humongous 對象數量有限,所以能夠快速的知道是否有老年代對象引用它。如果沒有,能夠阻止它被回收的唯一可能,就是新生代是否有對象引用了它,但這個信息是可以在 Young  GC 時就知道的,所以完全可以在 Young GC 中就進行 Humongous 對象的回收,不用像其他老年代對象那樣,等待併發標記結束。
  •   我在專欄第 5 講,提到了在 8u20 以後字符串排重的特性,在垃圾收集過程中,G1 會把新創建的字符串對象放入隊列中,然後在 Young GC  之後,併發地(不會 STW)將內部數據(char 數組,JDK 9 以後是 byte  數組)一致的字符串進行排重,也就是將其引用同一個數組。你可以使用下面參數激活:
-XX:+UseStringDeduplication

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

  •   類型卸載是個長期困擾一些 Java 應用的問題,在專欄第 25 講中,我介紹了一個類只有當加載它的自定義類加載器被回收後,才能被卸載。元數據區替換了永久代之後有所改善,但還是可能出現問題。

G1 的類型卸載有什麼改進嗎?很多資料中都談到,G1 只有在發生 Full GC 時才進行類型卸載,但這顯然不是我們想要的。你可以加上下面的參數查看類型卸載:

-XX:+TraceClassUnloading

幸好現代的 G1 已經不是如此了,8u40 以後,G1 增加並默認開啓下面的選項:

-XX:+ClassUnloadingWithConcurrentMark

也就是說,在併發標記階段結束後,JVM 即進行類型卸載。

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

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

-XX:+G1UseAdaptiveIHOP
  • 在現有的資料中,大多指出 G1 的 Full GC 是最差勁的單線程串行 GC。其實,如果採用的是最新的 JDK,你會發現 Full GC 也是並行進行的了,在通用場景中的表現還優於 Parallel GC 的 Full GC 實現。

當然,還有很多其他的改變,比如更快的 Card Table 掃描等,這裏不再展開介紹,因爲它們並不帶來行爲的變化,基本不影響調優選擇。

 

前面介紹了 G1 的內部機制,並且穿插了部分調優建議,下面從整體上給出一些調優的建議。

首先,建議儘量升級到較新的 JDK 版本,從上面介紹的改進就可以看到,很多人們常常討論的問題,其實升級 JDK 就可以解決了。

第二,掌握 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 調優指南等,遠不是幾句話可以囊括的。需要注意的是,也要避免過度調優,G1 對大堆非常友好,其運行機制也需要浪費一定的空間,有時候稍微多給堆一些空間,比進行苛刻的調優更加實用。

今天我梳理了基本的 GC 調優思路,並對 G1 內部結構以及最新的行爲變化進行了詳解。總的來說,G1 的調優相對簡單、直觀,因爲可以直接設定暫停時間等目標,並且其內部引入了各種智能的自適應機制,希望這一切的努力,能夠讓你在日常應用開發時更加高效。

 

一課一練

關於今天我們討論的題目你做到心中有數了嗎?今天的思考題是,定位 Full GC 發生的原因,有哪些方式?

答:

  1. 首先通過printgcdetail 查看fullgc頻率以及時長
  2. 通過dump 查看內存中哪些對象多,這些可能是引起fullgc的原因,看是否能優化
  3. 如果堆大或者是生產環境,可以開起jmc 飛行一段時間,查看這期間的相關數據來訂位問題
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章