JDK5.0垃圾收集最佳化之--Don't Pause

tbase發表於2008-06-02
原本想把題目更簡單的定為--《不要停》的,但還是自己YY一下就算了。
Java開發Server最大的障礙,就是JDK1.4版之前的的序列垃圾收集機制會引起長時間的服務暫停,明白原理後,想想那些用JDK1.3寫Server的先輩,不得不後怕。
好在JDK1.4已開始支援多執行緒並行的後臺垃圾收集演算法,JDK5.0則最佳化了預設值的設定。[@more@]

一、參考資料:

Tuning Garbage Collection with the 5.0 Java Virtual Machine 官方指南。
Hotspot memory management whitepaper 官方白皮書。
Java Tuning White Paper 官方文件。
FAQ about Garbage Collection in the Hotspot 官方FAQ,JVM1.4.2。
Java HotSpot 虛擬機器中的垃圾收集 JavaOne2004上的中文ppt
A Collection of JVM Options JVM選項的超完整收集。
二、基本概念

1、堆(Heap)

JVM管理的記憶體叫堆。在32Bit作業系統上有1.5G-2G的限制,而64Bit的就沒有。

JVM初始分配的記憶體由-Xms指定,預設是實體記憶體的1/64但小於1G。

JVM最大分配的記憶體由-Xmx指定,預設是實體記憶體的1/4但小於1G。

預設空餘堆記憶體小於40%時,JVM就會增大堆直到-Xmx的最大限制,可以由-XX:MinHeapFreeRatio=指定。
預設空餘堆記憶體大於70%時,JVM會減少堆直到-Xms的最小限制,可以由-XX:MaxHeapFreeRatio=指定。

伺服器一般設定-Xms、-Xmx相等以避免在每次GC 後調整堆的大小,所以上面的兩個引數沒啥用。


2.基本收集演算法

複製:將堆內分成兩個相同空間,從根(ThreadLocal的物件,靜態物件)開始訪問每一個關聯的活躍物件,將空間A的活躍物件全部複製到空間B,然後一次性回收整個空間A。
因為只訪問活躍物件,將所有活動物件複製走之後就清空整個空間,不用去訪問死物件,所以遍歷空間的成本較小,但需要巨大的複製成本和較多的記憶體。
標記清除(mark-sweep):收集器先從根開始訪問所有活躍物件,標記為活躍物件。然後再遍歷一次整個記憶體區域,把所有沒有標記活躍的物件進行回收處理。該演算法遍歷整個空間的成本較大暫停時間隨空間大小線性增大,而且整理後堆裡的碎片很多。
標記整理(mark-sweep-compact):綜合了上述兩者的做法和優點,先標記活躍物件,然後將其合併成較大的記憶體塊。
可見,沒有免費的午餐,無論採用複製還是標記清除演算法,自動的東西都要付出很大的效能代價。

3.分代

分代是Java垃圾收集的一大亮點,根據物件的生命週期長短,把堆分為3個代:Young,Old和Permanent,根據不同代的特點採用不同的收集演算法,揚長避短也。

Young(Nursery),年輕代。研究表明大部分物件都是朝生暮死,隨生隨滅的。因此所有收集器都為年輕代選擇了複製演算法。
複製演算法優點是隻訪問活躍物件,缺點是複製成本高。因為年輕代只有少量的物件能熬到垃圾收集,因此只需少量的複製成本。而且複製收集器只訪問活躍物件,對那些佔了最大比率的死物件視而不見,充分發揮了它遍歷空間成本低的優點。

Young的預設值為4M,隨堆記憶體增大,約為1/15,JVM會根據情況動態管理其大小變化。
-XX:NewRatio= 引數可以設定Young與Old的大小比例,-server時預設為1:2,但實際上young啟動時遠低於這個比率?如果信不過JVM,也可以用-Xmn硬性規定其大小,有文件推薦設為Heap總大小的1/4。

Young的大小非常非常重要,見“後面暫停時間優先收集器”的論述。

Young裡面又分為3個區域,一個Eden,所有新建物件都會存在於該區,兩個Survivor區,用來實施複製演算法。每次複製就是將Eden和第一塊 Survior的活物件複製到第2塊,然後清空Eden與第一塊Survior。Eden與Survivor的比例由-XX:SurvivorRatio =設定,預設為32。Survivio大了會浪費,小了的話,會使一些年輕物件潛逃到老人區,引起老人區的不安,但這個引數對效能並不重要。

Old(Tenured),年老代。年輕代的物件如果能夠挺過數次收集,就會進入老人區。老人區使用標記整理演算法。因為老人區的物件都沒那麼容易死的,採用複製演算法就要反覆的複製物件,很不合算,只好採用標記清理演算法,但標記清理演算法其實也不輕鬆,每次都要遍歷區域內所有物件,所以還是沒有免費的午餐啊。

-XX:MaxTenuringThreshold=設定熬過年輕代多少次收集後移入老人區,CMS中預設為0,熬過第一次GC就轉入,可以用-XX:+PrintTenuringDistribution檢視。

Permanent,持久代。裝載Class資訊等基礎資料,預設64M,如果是類很多很多的服務程式,需要加大其設定-XX:MaxPermSize=,否則它滿了之後會引起 fullgc()或Out of Memory。 注意Spring,Hibernate這類喜歡AOP動態生成類的框架需要更多的持久代記憶體。

4.minor/major collection

每個代滿了之後都會促發collection,(另外Concurrent Low Pause Collector預設在老人區68%的時候促發)。GC用較高的頻率對young進行掃描和回收,這種叫做minor collection。
而因為成本關係對Old的檢查回收頻率要低很多,同時對Young和Old的收集稱為major collection。
System.gc()會引發major collection,使用-XX:+DisableExplicitGC禁止它,或設為CMS併發-XX:+ExplicitGCInvokesConcurrent。

5.小結

Young -- minor collection -- 複製演算法

Old(Tenured) -- major colletion -- 標記清除/標記整理演算法


三、收集器

1.古老的序列收集器(Serial Collector)

使用 -XX:+UseSerialGC,策略為年輕代序列復制,年老代序列標記整理。

2.吞吐量優先的並行收集器(Throughput Collector)

使用 -XX:+UseParallelGC ,也是JDK5 -server的預設值。策略為:
1.年輕代暫停應用程式,多個垃圾收集執行緒並行的複製收集,執行緒數預設為CPU個數,CPU很多時,可用–XX:ParallelGCThreads=減少執行緒數。
2.年老代暫停應用程式,與序列收集器一樣,單垃圾收集執行緒標記整理。

所以需要2+的CPU時才會優於序列收集器,適用於後臺處理,科學計算。

可以使用-XX:MaxGCPauseMillis= 和 -XX:GCTimeRatio 來調整GC的時間。

3.暫停時間優先的併發收集器(Concurrent Low Pause Collector-CMS)

前面說了這麼多,都是為了這節做鋪墊......

使用-XX:+UseConcMarkSweepGC,策略為:
1.年輕代同樣是暫停應用程式,多個垃圾收集執行緒並行的複製收集。
2.年老代則只有兩次短暫停,其他時間應用程式與收集執行緒併發的清除。

3.1 年老代詳述

並行(Parallel)與併發(Concurrent)僅一字之差,並行指多條垃圾收集執行緒並行,併發指使用者執行緒與垃圾收集執行緒併發,程式在繼續執行,而垃圾收集程式執行於另一個個CPU上。

併發收集一開始會很短暫的停止一次所有執行緒來開始初始標記根物件,然後標記執行緒與應用執行緒一起併發執行,最後又很短的暫停一次,多執行緒並行的重新標記之前可能因為併發而漏掉的物件,然後就開始與應用程式併發的清除過程。可見,最長的兩個遍歷過程都是與應用程式併發執行的,比以前的序列演算法改進太多太多了!!!

序列標記清除是等年老代滿了再開始收集的,而併發收集因為要與應用程式一起執行,如果滿了才收集,應用程式就無記憶體可用,所以系統預設68%滿的時候就開始收集。記憶體已設得較大,吃記憶體又沒有這麼快的時候,可以用-XX:CMSInitiatingOccupancyFraction=恰當增大該比率。

3.2 年輕代詳述

可惜對年輕代的複製收集,依然必須停止所有應用程式執行緒,原理如此,只能靠多CPU,多收集執行緒併發來提高收集速度,但除非你的Server獨佔整臺伺服器,否則如果伺服器上本身還有很多其他執行緒時,切換起來速度就..... 所以,搞到最後,暫停時間的瓶頸就落在了年輕代的複製演算法上。

因此Young的大小設定挺重要的,大點就不用頻繁GC,而且增大GC的間隔後,可以讓多點物件自己死掉而不用複製了。但Young增大時,GC造成的停頓時間攀升得非常恐怖,比如在我的機器上,預設8M的Young,只需要幾毫秒的時間,64M就升到90毫秒,而升到256M時,就要到300毫秒了,峰值還會攀到恐怖的800ms。誰叫複製演算法,要等Young滿了才開始收集,開始收集就要停止所有執行緒呢。

3.3 持久代

可設定-XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled,使CMS收集持久代的類,而不是fullgc,netbeans5.5 performance文件的推薦。

4.增量(train演算法)收集器(Incremental Collector)

已停止維護,–Xincgc選項預設轉為併發收集器。

四、暫停時間顯示

加入下列引數 (請將PrintGC和Details中間的空格去掉,CSDN很怪的認為是禁止字句)

-verbose:gc -XX:+PrintGC Details -XX:+PrintGCTimeStamps

會程式執行過程中將顯示如下輸出

9.211: [GC 9.211: [ParNew: 7994K->0K(8128K), 0.0123935 secs] 427172K->419977K(524224K), 0.0125728 secs]

顯示在程式執行的9.211秒發生了Minor的垃圾收集,前一段資料針對新生區,從7994k整理為0k,新生區總大小為8128k,程式暫停了12ms,而後一段資料針對整個堆。

對於年老代的收集,暫停發生在下面兩個階段,CMS-remark的中斷是17毫秒:

[GC [1 CMS-initial-mark: 80168K(196608K)] 81144K(261184K), 0.0059036 secs]

[1 CMS-remark: 80168K(196608K)] 82493K(261184K),0.0168943 secs]

再加兩個引數 -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime對暫停時間看得更清晰。

五、真正不停的BEA JRockit 與Sun RTS2.0

Bea的JRockit 5.0 R27 的特色之一是動態決定的垃圾收集策略,使用者可以決定自己關心的是吞吐量,暫停時間還是確定的暫停時間,再由JVM在執行時動態決定、改變改變垃圾收集策略。

它的Deterministic GC的選項是-Xgcprio: deterministic,號稱可以把暫停可以控制在10-30毫秒,非常的牛,一句Deterministic道盡了RealTime的真諦。不過細看一下文件,30ms的測試環境是1 GB heap 和 平均 30% 的活躍物件(也就是300M)活動物件,2 個 Xeon 3.6 GHz 4G記憶體 ,或者是4 個Xeon 2.0 GHz,8G記憶體。

最可惜JRockt的license很奇怪,雖然平時使用免費,但這個30ms的選項就需要購買整個Weblogic Real Time Server的license。

其他免費選項,有:

-Xgcprio:pausetime -Xpausetarget=210ms
因為免費,所以最低只能設定到200ms pause target。 200ms是Sun認為Real-Time的分界線。
-Xgc:gencon
普通的併發做法,效率也不錯。
JavaOne2007上有Sun的 Java Real-Time System 2.0 的介紹,RTS2.0基於JDK1.5,在Real-Time Garbage Collctor上又有改進,但還在beta版狀態,只供給OEM,更怪。

六、JDK 6.0的改進

因為JDK5.0在Young較大時的表現還是不夠讓人滿意,又繼續看JDK6.0的改進,結果稍稍失望,不涉及我最頭痛的年輕代複製收集改良。

1.年老代的標識-清除收集,並行執行標識
JDK5.0只開了一條收集程式與應用執行緒併發標識,而6.0可以開多條收集執行緒來做標識,縮短標識老人區所有活動物件的時間。

2.加大了Young區的預設大小
預設大小從4M加到16M,從堆記憶體的1/15增加到1/7

3.System.gc()可以與應用程式併發執行
使用-XX:+ExplicitGCInvokesConcurrent 設定

七、小結

1. JDK5.0/6.0

對於伺服器應用,我們使用Concurrent Low Pause Collector,對年輕代,暫停時多執行緒並行複製收集;對年老代,收集器與應用程式並行標記--整理收集,以達到儘量短的垃圾收集時間。

本著沒有深刻測試前不要胡亂最佳化的宗旨,命令列屬性只需簡單寫為:

-server -XmsM -XmxM -XX:+UseConcMarkSweepGC -XX:+PrintGC Details -XX:+PrintGCTimeStamps
然後要根據應用的情況,在測試軟體輔助可以下看看有沒有JVM的預設值和自動管理做的不夠的地方可以調整,如-xmn 設Young的大小,-XX:MaxPermSize設持久代大小等。

2. JRockit 6.0 R27.2

但因為JDK5的測試結果實在不能滿意,後來又嘗試了JRockit,總體效果要好些。
JRockit的特點是動態垃圾收集器是根據使用者關心的特徵動態決定收集演算法的,引數如下

-XmsM -XmxM -Xgcprio:pausetime -Xpausetarget=200ms -XgcReport -XgcPause -Xverbose:memory
本篇文章來源於 新技術天空 原文連結:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/249132/viewspace-1004992/,如需轉載,請註明出處,否則將追究法律責任。

相關文章