1. 程式人生 > >JVM實用引數(五)新生代垃圾回收

JVM實用引數(五)新生代垃圾回收

原文連結  作者: PATRICK PESCHLOW ;譯者:嚴亮

本部分,我們將關注堆(heap) 中一個主要區域,新生代(young generation)。首先我們會討論為什麼調整新生代的引數會對應用的效能如此重要,接著我們將學習新生代相關的JVM引數。

單純從JVM的功能考慮,並不需要新生代,完全可以針對整個堆進行操作。新生代存在的唯一理由是優化垃圾回收(GC)的效能。更具體說,把堆劃分為新生代和老年代有2個好處:簡化了新物件的分配(只在新生代分配記憶體),可以更有效的清除不再需要的物件(即死物件)(新生代和老年代使用不同的GC演算法)

通過廣泛研究面向物件實現的應用,發現一個共同特點:很多物件的生存時間都很短。同時研究發現,新生物件很少引用生存時間長的物件。結合這2個特點,很明顯 GC 會頻繁訪問新生物件,例如在堆中一個單獨的區域,稱之為新生代。在新生代中,GC可以快速標記回收”死物件”,而不需要掃描整個Heap中的存活一段時間的”老物件”。

SUN/Oracle 的HotSpot JVM 又把新生代進一步劃分為3個區域:一個相對大點的區域,稱為”伊甸園區(Eden)”;兩個相對小點的區域稱為”From 倖存區(survivor)”和”To 倖存區(survivor)”。按照規定,新物件會首先分配在 Eden 中(如果新物件過大,會直接分配在老年代中)。在GC中,Eden 中的物件會被移動到survivor中,直至物件滿足一定的年紀(定義為熬過GC的次數),會被移動到老年代。

基於大多數新生物件都會在GC中被收回的假設。新生代的GC 使用複製演算法。在GC前To 倖存區(survivor)保持清空,物件儲存在 Eden 和 From 倖存區(survivor)中,GC執行時,Eden中的倖存物件被複制到 To 倖存區(survivor)。針對 From 倖存區(survivor)中的倖存物件,會考慮物件年齡,如果年齡沒達到閥值(tenuring threshold),物件會被複制到To 倖存區(survivor)。如果達到閥值物件被複制到老年代。複製階段完成後,Eden 和From 倖存區中只儲存死物件,可以視為清空。如果在複製過程中To 倖存區被填滿了,剩餘的物件會被複制到老年代中。最後 From 倖存區和 To倖存區會調換下名字,在下次GC時,To 倖存區會成為From 倖存區。

總結一下,物件一般出生在Eden區,年輕代GC過程中,物件在2個倖存區之間移動,如果物件存活到適當的年齡,會被移動到老年代。當物件在老年代死亡時,就需要更高級別的GC,更重量級的GC演算法(複製演算法不適用於老年代,因為沒有多餘的空間用於複製)

現在應該能理解為什麼新生代大小非常重要了(譯者,有另外一種說法:新生代大小並不重要,影響GC的因素主要是倖存物件的數量),如果新生代過小,會導致新生物件很快就晉升到老年代中,在老年代中物件很難被回收。如果新生代過大,會發生過多的複製過程。我們需要找到一個合適大小,不幸的是,要想獲得一個合適的大小,只能通過不斷的測試調優。這就需要JVM引數了

-XX:NewSize and -XX:MaxNewSize


就像可以通過引數(-Xms and -Xmx) 指定堆大小一樣,可以通過引數指定新生代大小。設定 XX:MaxNewSize 引數時,應該考慮到新生代只是整個堆的一部分,新生代設定的越大,老年代區域就會減少。一般不允許新生代比老年代還大,因為要考慮GC時最壞情況,所有物件都晉升到老年代。(譯者:會發生OOM錯誤) -XX:MaxNewSize 最大可以設定為-Xmx/2 .

考慮效能,一般會通過引數 -XX:NewSize 設定新生代初始大小。如果知道新生代初始分配的物件大小(經過監控) ,這樣設定會有幫助,可以節省新生代自動擴充套件的消耗。

-XX:NewRatio
可以設定新生代和老年代的相對大小。這種方式的優點是新生代大小會隨著整個堆大小動態擴充套件。引數 -XX:NewRatio 設定老年代與新生代的比例。例如 -XX:NewRatio=3 指定老年代/新生代為3/1. 老年代佔堆大小的 3/4 ,新生代佔 1/4 .

如果針對新生代,同時定義絕對值和相對值,絕對值將起作用。下面例子:
$ java -XX:NewSize=32m -XX:MaxNewSize=512m -XX:NewRatio=3 MyApp

以上設定, JVM 會嘗試為新生代分配四分之一的堆大小,但不會小於32MB或大於521MB

在設定新生代大小問題上,使用絕對值還是相對值,不存在通用準則 。如果瞭解應用的記憶體使用情況,設定固定大小的堆和新生代更有利,當然也可以設定相對值。如果對應用的記憶體使用一無所知,正確的做法是不要設定任何引數,如果應用執行良好。很好,我們不用做任何額外動作.如果遇到效能或OutOfMemoryErrors, 在調優之前,首先需要進行一系列有目的的監控測試,縮小問題的根源。

-XX:SurvivorRatio
引數 -XX:SurvivorRatio 與 -XX:NewRatio 類似,作用於新生代內部區域。-XX:SurvivorRatio 指定伊甸園區(Eden)與倖存區大小比例. 例如, -XX:SurvivorRatio=10 表示伊甸園區(Eden)是 倖存區To 大小的10倍(也是倖存區From的10倍).所以,伊甸園區(Eden)佔新生代大小的10/12, 倖存區From和倖存區To 每個佔新生代的1/12 .注意,兩個倖存區永遠是一樣大的..

設定倖存區大小有什麼作用? 假設倖存區相對伊甸園區(Eden)太小, 相應新生物件的伊甸園區(Eden)永遠很大空間, 我們當然希望,如果這些物件在GC時全部被回收,伊甸園區(Eden)被清空,一切正常.然而,如果有一部分物件在GC中倖存下來, 倖存區只有很少空間容納這些物件.結果大部分倖存物件在一次GC後,就會被轉移到老年代 ,這並不是我們希望的.考慮相反情況, 假設倖存區相對伊甸園區(Eden)太大,當然有足夠的空間,容納GC後的倖存物件. 但是過小的伊甸園區(Eden),意味著空間將越快耗盡,增加新生代GC次數,這是不可接受的。

總之,我們希望最小化短命物件晉升到老年代的數量,同時也希望最小化新生代GC 的次數和持續時間.我們需要找到針對當前應用的折中方案, 尋找適合方案的起點是 瞭解當前應用中物件的年齡分佈情況。

-XX:+PrintTenuringDistribution
引數 -XX:+PrintTenuringDistribution 指定JVM 在每次新生代GC時,輸出倖存區中物件的年齡分佈。例如:
Desired survivor size 75497472 bytes, new threshold 15 (max 15)
- age 1: 19321624 bytes, 19321624 total
- age 2: 79376 bytes, 19401000 total
- age 3: 2904256 bytes, 22305256 total

第一行說明倖存區To大小為 75 MB. 也有關於老年代閥值(tenuring threshold)的資訊, 老年代閥值,意思是物件從新生代移動到老年代之前,經過幾次GC(即, 物件晉升前的最大年齡). 上例中,老年代閥值為15,最大也是15.

之後行表示,對於小於老年代閥值的每一個物件年齡,本年齡中物件所佔位元組 (如果當前年齡沒有物件,這一行會忽略). 上例中,一次 GC 後倖存物件大約 19 MB, 兩次GC 後倖存物件大約79 KB , 三次GC 後倖存物件大約 3 MB .每行結尾,顯示直到本年齡全部物件大小.所以,最後一行的 total 表示倖存區To 總共被佔用22 MB . 倖存區To 總大小為 75 MB ,當前老年代閥值為15,可以斷定在本次GC中,沒有物件會移動到老年代。現在假設下一次GC 輸出為:

Desired survivor size 75497472 bytes, new threshold 2 (max 15)
- age 1: 68407384 bytes, 68407384 total
- age 2: 12494576 bytes, 80901960 total
- age 3: 79376 bytes, 80981336 total
- age 4: 2904256 bytes, 83885592 total

對比前一次老年代分佈。明顯的,年齡2和年齡3 的物件還保持在倖存區中,因為我們看到年齡3和4的物件大小與前一次年齡2和3的相同。同時發現倖存區中,有一部分物件已經被回收,因為本次年齡2的物件大小為 12MB ,而前一次年齡1的物件大小為 19 MB。最後可以看到最近的GC中,有68 MB 新物件,從伊甸園區移動到倖存區。

注意,本次GC 倖存區佔用總大小 84 MB -大於75 MB. 結果,JVM 把老年代閥值從15降低到2,在下次GC時,一部分物件會強制離開倖存區,這些物件可能會被回收(如果他們剛好死亡)或移動到老年代。

-XX:InitialTenuringThreshold, -XX:MaxTenuringThreshold and -XX:TargetSurvivorRatio
引數 -XX:+PrintTenuringDistribution 輸出中的部分值可以通過其它引數控制。通過 -XX:InitialTenuringThreshold 和 -XX:MaxTenuringThreshold 可以設定老年代閥值的初始值和最大值。另外,可以通過引數 -XX:TargetSurvivorRatio 設定倖存區的目標使用率.例如 , -XX:MaxTenuringThreshold=10 -XX:TargetSurvivorRatio=90 設定老年代閥值的上限為10,倖存區空間目標使用率為90%。

有多種方式,設定新生代行為,沒有通用準則。我們必須清楚以下2中情況:
1 如果從年齡分佈中發現,有很多物件的年齡持續增長,在到達老年代閥值之前。這表示 -XX:MaxTenuringThreshold 設定過大
2 如果 -XX:MaxTenuringThreshold 的值大於1,但是很多物件年齡從未大於1.應該看下倖存區的目標使用率。如果倖存區使用率從未到達,這表示物件都被GC回收,這正是我們想要的。 如果倖存區使用率經常達到,有些年齡超過1的物件被移動到老年代中。這種情況,可以嘗試調整倖存區大小或目標使用率。

-XX:+NeverTenure and -XX:+AlwaysTenure
最後,我們介紹2個頗為少見的引數,對應2種極端的新生代GC情況.設定引數 -XX:+NeverTenure , 物件永遠不會晉升到老年代.當我們確定不需要老年代時,可以這樣設定。這樣設定風險很大,並且會浪費至少一半的堆記憶體。相反設定引數 -XX:+AlwaysTenure, 表示沒有幸存區,所有物件在第一次GC時,會晉升到老年代。
沒有合理的場景使用這個引數。可以在測試環境中,看下這樣設定會發生什麼有趣的事.但是並不推薦使用這些引數.

結論
適當的配置新生代非常重要,有相當多的引數可以設定新生代。然而,單獨調整新生代,而不考慮老年代是不可能優化成功的。當調整堆和GC設定時,我們總是應該同時考慮新生代和老年代。

在本系列的下面2部分,我們將討論 HotSpot JVM 中老年代 GC 策略,我們會學習“吞吐量GC收集器” 和 “併發低延遲GC收集器”,也會了解收集器的基本準則,演算法和調整引數.