本部分,我們将關注堆(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 幸存區。

<a href="https://blog.codecentric.de/files/2011/08/young_gc.png" target="_blank">https://blog.codecentric.de/files/2011/08/young_gc.png</a>
上圖示範gc過程,黃色表示死對象,綠色表示剩餘空間,紅色表示幸存對象
總結一下,對象一般出生在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 .
如果針對新生代,同時定義絕對值和相對值,絕對值将起作用。下面例子:
<code>$ java -xx:newsize=32m -xx:maxnewsize=512m -xx:newratio=3 myapp</code>
以上設定, 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時,輸出幸存區中對象的年齡分布。例如:
<code>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</code>
第一行說明幸存區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 輸出為:
<code>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</code>
對比前一次老年代分布。明顯的,年齡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收集器”,也會了解收集器的基本準則,算法和調整參數.