JVM 年輕代和年老代 大小設定

追尋北極發表於2017-11-22
有許多現成的調優經驗的介紹。Charlie Hunt寫的《Java Performance》一書裡有很詳細的介紹。中文版就快出了,敬請關注。 
其中涉及GC調優的部分在過往的JavaOne裡也有session介紹過。請搜這個標題:"Step-by-Step: Garbage Collection Tuning in the Java HotSpot™ Virtual Machine" 

不過那種很具體的現成經驗畢竟是別人在他們見過的環境裡沉澱下來的,並不一定適用於所有情況。所以怎樣的調優方法適合自己,還是得理解了系統底層的工作原理然後再在實際環境里加以應用、變通才好。 

對HotSpot VM裡的GC不熟悉的,至少應該把Sun以前出的HotSpot VM的GC調優白皮書讀了。 

==================== 

為啥HotSpot VM裡收集有兩種概念,一種是young GC/minor GC,另一種是full GC/major GC;為啥後者不是叫old GC? 

JVM <wbr>年輕代和年老代 <wbr>大小設定
因為young GC只收集young gen,但full GC會收集整個GC堆。 
HotSpot VM的full GC會收集整個Java堆,包括其中的young gen與old gen;同時也會順便收集不屬於Java堆的perm gen。 
Young + old + perm構成了HotSpot VM的整個GC堆。至少目前還是這樣。 
(JDK8裡的HotSpot VM就沒有perm gen了。請注意。) 

CMS在併發模式工作的時候是隻收集old gen的。但一旦併發模式失敗(發生concurrent mode failure)就有選擇性的會進行全堆收集,也就是退回到full GC。 

==================== 

大小分配怎樣才合理取決於某個具體應用的物件的存活模式。 

這涉及到分代式GC的原理。最初為何要把GC堆劃分為多個區域,以不同的頻率來收集它們?本來就是為了讓每次收集的效率都最大(在收集的區域裡儘可能回收出可用空間)。如果一個應用裡物件的存活模式滿足弱分代假設,那麼把新生物件放在同一個區域裡,每次收集這個區域的效率都應該比較高(因為假設是新生物件活不了多久就死了)。 

有人專門研究這個。可以用"java object demography"這組關鍵字來搜已有資料。 

==================== 

舉例:可能很多人都有一種印象,young gen應該比old gen小。籠統說確實如此,因為在最壞情況下young gen裡可能所有物件都還活著,而如果它們全部都要晉升到old gen的話,那old gen裡的剩餘空間必須能容納下這些物件才行,這就需要old gen比young gen大(否則young GC就無法進行,而必須做full GC才能應付了)。 
實際上卻不總是這樣的。所謂“最壞情況”在很多系統裡是永遠不會出現的。調優就是要針對實際應用裡物件的存活模式來破除這些“最壞情況”的假設帶來的限制。 

許多Web應用裡物件會有這樣的特徵: 
·(a) 有一部分物件幾乎一直活著。這些可能是常用資料的cache之類的 
·(b) 有一部分物件建立出來沒多久之後就沒用了。這些很可能會響應一個請求時建立出來的臨時物件 
·(c) 最後可能還有一些中間的物件,建立出來之後不會馬上就死,但也不會一直活著。 


如果是這樣的模式,那young gen可以設定得非常大,大到每次young GC的時候裡面的多數物件(b)最好已經死了。 
想像一下,如果young gen太小,每次滿了就觸發一次young GC,那麼young GC就會很頻繁,或許很多臨時物件(b)正好還在被是使用(還沒死),這樣的話young GC的收集效率就會比較低。要避免這樣的情況,最好是就是把young gen設大一些。 

那old gen怎麼辦?如果是上面說的情況,那old gen至少要足以裝下所有長期存活的物件(a);同時也要留出一定的餘地用來容納young GC沒能清理掉的臨時物件。 

這樣,最後調整出來的結果很可能young GC反而比old gen大許多。這完全沒問題。 

只有(a)和(b)的話就完美了,現實中最頭疼的就是針對(c)物件的調優。它們或許會經歷多次young GC之後仍然存活,於是晉升到old gen;但晉升上去之後或許很快就又死掉了。 
這種物件最好能不讓晉升到old gen(可以讓它們在survivor space裡多來回倒騰幾次再晉升,也就是想辦法增加tenuring threshold;不過HotSpot VM裡的GC不讓外界對此多插手,想減小MaxTenuringThreshold很容易,想增加實際有效的tenuring threshold就沒那麼容易了)。但如果真的不讓它們晉升,young GC的暫停時間就會增長(在survivor space裡來回倒騰物件意味著要來回拷貝,這會花時間)。 
所以有一種策略是儘量讓這種物件的大部分在young GC中消耗掉(在保持young GC的暫停時間不超過某個預期值的前提下),而“漏”到old gen的那些讓諸如CMS之類的併發GC來解決。 
總之這裡要做一定的tradeoff就是了。 

================= 

知道了原理之後在現實中要如何實踐呢? 

首先得了解硬性限制:某個伺服器總共有多少記憶體,其中最多可以分配多少給某個應用程式;有沒有一些服務對響應時間有嚴格要求,有的話限制是多少,之類的。 

然後看看應用的特徵是怎樣的。可以藉助一些工具來了解物件的存活情況,例如NetBeans的profiler就有這樣的功能(老文件);許多其它主流Java profiler也有類似的功能。 
這些工具的精度和效能開銷各異,總之自己摸索下看看吧。 

情況瞭解清楚了就可以開始迭代調整各種引數看實際執行的表現如何。迭代到滿意為止。 
要分析實際GC的執行狀況,首要切入點就是分析GC日誌。有很多工具能把HotSpot VM的GC日誌視覺化。我以前一直在用的是這個:GCHisto。 
然後像Twitter做的這個工具也可以抽取一些GC的輔助統計資訊:https://github.com/twitter/jvmgcprof 

================= 

那個…上面隨便寫了些。文字不通順的地方請輕拍,要整理成“文章”的話又要燒腦細胞了… 

沒說清楚的地方請另外補充背景知識… 
例如這個:http://www.infoq.com/interviews/szegedi-performance-tuning,Attila Szegedi的GC調優經驗。 
還有這個:http://www.infoq.com/presentations/Understanding-Java-Garbage-Collection,Gil Tene談GC。
   

如果老年代設定過小,就會頻繁觸發full gc,full gc是非常耗時的。年輕代在經過n(hotspot預設是15)輪後會進入老年代,這樣老年代頂不住了,就會觸發full gc,回收時需要stop the world,這樣系統經常發生長時間停頓,影響系統的吞吐量




原文地址:http://blog.sina.com.cn/s/blog_4adc4b090102vr1v.html

相關文章