優化CMS(concurrent garbage collection)
使用CMS,old代的垃圾回收執行線程會和應用程序的線程最大程度的并發執行。這個提供了一個機會來減少最壞延遲的頻率和最壞延遲的時間消耗。CMS沒有執行壓縮,所以可以避免old代空間的stop-the-world壓縮(會讓整個應用暫停運行)。
優化CMS的目標就是避開stop-the-world壓縮垃圾回收,然而,這個說比做起來容易。在一些的部署情況下,這個是不可避免的,尤其是當內存分配受限的時候。
在一些特殊的情況下,CMS比其他類型的垃圾回收需要更多優化,更需要優化young代的空間,以及潛在的優化該什么時候初始化old代的垃圾回收循環。
當從吞吐量垃圾回收器(Throughput)遷移到CMS的時候,有可能會獲得更慢的MinorGC,由于對象從young代轉移到old會更慢 ,由于CMS在old代里面分配的內存是一個不連續的列表,相反,吞吐量垃圾回收器只是在本地線程的分配緩存里面指定一個指針。另外,由于old代的垃圾回收線程和應用的線程是盡可能的并發運行的,所以吞吐量會更小一些。然而,最壞的延遲的頻率會少很多,由于在old代的不可獲取的對象能夠在應用運行的過程被垃圾回收,這樣可以避免old代的空間溢出。
使用CMS,如果old代能夠使用的空間有限,單線程的stop-the-world壓縮垃圾回收會執行。這種情況下,FullGC的時間會比吞吐量垃圾回收器的FullGC時間還要長,導致的結果是,CMS的絕對最差延遲會比吞吐量垃圾回收器的最差延遲嚴重很多。old代的空間溢出以及運行了stop-the-world垃圾回收必須被應用負責人重視,由于在響應上會有更長的中斷。因此,不要讓old代運行得溢出就非常重要了。對于從吞吐量垃圾回收器遷移到CMS的一個比較重要的建議就是提升old代20%到30%的容量。
在優化CMS的時候有幾個注意點,首先,對象從young代轉移到old代的轉移率。其次,CMS重新分配內存的概率。再次,CMS回收對象時候產生的old代的分隔,這個會在可獲得的對象中間產生一些空隙,從而導致了分隔空間。
碎片可以被下面的幾種方法尋址。第一辦法是壓縮old代,壓縮old代空間是通過stop-the-world垃圾回收壓縮完成的,就像前面所說的那樣,stop-the-world垃圾回收會執行很長時間,會嚴重影響應用的響應時間,應該避開。第二種辦法是,對碎片編址,提高old代的空間,這個辦法不能完全解決碎片的問題的,但是可以延遲old代壓縮的時間。通常來講,old代越多內存,由于碎片導致需要執行的壓縮的時間久越長。努力把old的空間增大的目標是在應用的生命周期中,避免堆碎片導致stop-the-world壓縮垃圾回收,換句話說,應用GC最大內存原則。另外一種處理碎片的辦法是減少對象從young代移動到old的概率,就是減少MinorGC,應用MinorGC回收原則。
任期閥值(tenuring threshold)控制了對象該什么時候從young代移動到old代。任期閥值會在后面詳細的介紹,它是HotSpot VM基于young代的占用空間來計算的,尤其是survivor(幸存者)空間的占用量。下面詳細介紹一下survivor空間以及討論任期閥值。
survivor空間
survivor空間是young代的一部分,如下圖所示。young代被分成了一個eden區域和兩個survivor空間。
兩個survivor空間的中一個被標記為“from”,另外一個標記為“to”。新的Java對象被分配到Eden空間。比如說,下面的一條語句:
- <span style="font-size:14px;"> Map<String,String> map = new HashMap<String,String>();</span>
一個新的HashMap對象會被放到eden空間,當eden空間滿了的時候,MinorGC就會執行,任何存活的對象,都從eden空間復制到“to” survivor空間,任何在“from” survivor空間里面的存活對象也會被復制到“to” survivor。MinorGC結束的時候,eden空間和“from” survivor空間都是空的,“to” survivor空間里面存儲存活的對象,然后,在下次MinorGC的時候,兩個survivor空間交換他們的標簽,現在是空的“from” survivor標記成為“to”,“to” survivor標記為“from”。因此,在MinorGC結束的時候,eden空間是空的,兩個survivor空間中的一個是空的。
在MinorGC過程,如果“to” survivor空間不夠大,不能夠存儲所有的從eden空間和from suvivor空間復制過來活動對象,溢出的對象會被復制到old代。溢出遷移到old代,會導致old代的空間快速增長,會導致stop-the-world壓縮垃圾回收,所以,這里要使用MinorGC回收原則。
避免survivor空間溢出可以通過指定survivor空間的大小來實現,以使得survivor有足夠的空間來讓對象存活足夠的歲數。高效的歲數控制會導致只有長時間存活的對象轉移到old代空間。
歲數控制是指一個對象保持在young代里面直到無法獲取,所以讓old代只是存儲長時間保存的對象。
survivor的空間可以大小設置可以用HotSpot命令行參數:-XX:SurvivorRatio=<ratio>
<ratio>必須是以一個大于0的值,-XX:SurvivorRatio=<ratio>表示了每一個survivor的空間和eden空間的比值。下面這個公式可以用來計算survivor空間的大小
- survivor spave size = -Xmn<value>/(-XX:SurvivorRatio=<ratio>+2)
這里有一個+2的理由是有兩個survivor空間,是一個調節參數。ratio設置的越大,survivor的空間越小。為了說明這個問題,假設young代的大小是-Xmn512m而且-XX:SurvivorRatio=6.那么,young代有兩個survivor空間且空間大小是64M,那么eden空間的大小是384M。
同樣假如young代的大小是512M,但是修改-XX:SurvivorRatio=2,這樣的配置會使得每一個survivor空間的大小是128m而eden空間的大小是256M。
對于一個給定大小young代空間大小,減小ratio參數增加survivor空間的大小而且減少eden空間的大小。反之,增加ratio會導致survivor空間減少而且eden空間增大。減少eden空間會導致MinorGC更加頻繁,相反,增加eden空間的大小會導致更小的MinorGC,越多的MinorGC,對象的歲數增長得越快。
為了更好的優化survivor空間的大小和完善young代空間的大小,需要監控任期閥值,任期閥值決定了對象會再young代保存多久。怎么樣來監控和優化任期閥值將在下一節中介紹。
任期閥值
“任期”是轉移的代名詞,換句話說,任期閥值意味著對象移動到old代空間里面。HotSpot VM每次MinorGC的時候都會計算任期,以決定對象是否需要移動到old代去。任期閥值就是對象的歲數。對象的歲數是指他存活過的MinorGC次數。當一個對象被分配的時候,它的歲數是0。在下次MinorGC的時候之后,如果對象還是存活在young代里面,它的歲數就是1。如果再經歷過一次MinorGC,它的歲數變成2,依此類推。在young代里面的歲數超過HotSpot VM指定閥值的對象會被移動到old代里面。換句話說,任期閥值決定對象在young代里面保存多久。
任期閥值的計算依賴于young代里面能夠存放的對象數以及MinorGC之后,“to” servivor的空間占用。HotSpot VM有一個選項-XX:MaxTenuringThreshold=<n>,可以用來指定當時對象的歲數超過<n>的時候,HotSpot VM會把對象移動到old代去。內部計算的任期閥值一定不會超過指定的最大任期閥值。最大任期閥值在可以被設定為0-15,不過在Java 5 update 5之前可以設置為1-31。
不推薦把最大任期閥值設定成0或者超過15,這樣會導致GC的低效率。
如果HotSpot VM它無法保持目標survivor 空間的占用量,它會使用一個小于最大值的任期閥值來維持目標survivor空間的占用量,任何比這個任期閥值的大的對象都會被移動到old代。話句話說,當存活對象的量大于目標survivor空間能夠接受的量的時候,溢出發生了,溢出會導致對象快速的移動到old代,導致不期望的FullGC。甚至會導致更頻繁的stop-the-world壓縮垃圾回收。哪些對象會被移動到old代是根據評估對象的歲數和任期閥值來確定的。因此,很有必要監控任期閥值以避免survivor空間溢出,接下來詳細討論。
監控任期閥值
為了不被內部計算的任期閥值迷惑,我們可以使用命令選項-XX:MaxTenuringThreshod=<n>來指定最大的任期閥值。為了決定出最大的任期閥值,需要監控任期閥值的分布和對象歲數的分布,通過使用下面的選項實現
- -XX:+PrintTenuringDistribution
-XX:+PrintTenuringDistribution的輸出顯示在survivor空間里面有效的對象的歲數情況。閱讀-XX:+PrintTenuringDistribution輸出的方式是觀察在每一個歲數上面,對象的存活的數量,以及其增減情況,以及HotSpot VM計算的任期閥值是不是等于或者近似于設定的最大任期閥值。
-XX:+PrintTenuringDistribution在MinorGC的時候產生任期分布信息。它可以同其他選項一同使用,比如-XX:+PrintGCDateStamps,-XX:+PrintGCTimeStamps以及-XX:+PringGCDetails。當調整survivor空間大小以獲得有效的對象歲數分布,你應該使用-XX:+PrintTenuringDistribution。在生產環境中,它同樣非常有用,可以用來判斷stop-the-world的垃圾回收是否發生。
下面是一個輸出的例子:
Desired survivor size 8388608 bytes, new threshold 1 (max 15)
- age 1: 16690480 bytes, 16690480 total
在這里例子中,最大任期閥值被設置為15,(通過max 15表示)。內部計算出來的任期閥值是1,通過threshold 1表示。Desired survivor size 8388608 bytes表示一個survivor的空間大小。目標survivor的占有率是指目標survivor和兩個survivor空間總和的比值。怎么樣指定期望的survivor空間大小在后面會詳細介紹。在第一行下面,會列出一個對象的歲數列表。每行會列出每一個歲數的字節數,在這個例子中,歲數是1的對象有16690480字節,而且每行后面有一個總的字節數,如果有多行輸出的話,總字節數是前面的每行的累加數。后面舉例說明。
在前面的例子中,由于期望的survivor大小(8388608)比實際總共survivor字節數(16690480)小,也就是說,survivor空間溢出了,這次MinorGC會有一些對象移動到old代。這個就意味著survivor的空間太小了。另外,設定的最大任期閥值是15,但是實際上JVM使用的是1,也表明了survivor的空間太小了。
如果發現survivor區域太小,就增大survivor的空間,下面詳細介紹如何操作。
設定survivor空間
當修改survivor空間的大小的時候,有一點需要記住。當修改survivor空間大小的時候,如果young代的大小不改變,那么eden空間會減小,進一步會導致更頻繁的MinorGC。因此,增加survivor空間的時候,如果young代的空間大小違背了MinorGC頻率的需求,eden空間的大小同需要需要增加。換句話說,當survivor空間增加的時候,young代的大小需要增加。
如果有空間來增加MinorGC的頻率,有兩種選擇,一是拿一些eden空間來增加survivor的空間,二是讓young的空間更大一些。常規來講,更好的選擇是如果有可以使用的內存,增加young代的空間會比減少eden的空間更好一些。讓eden空間大小保持恒定,MinorGC的頻率不會改變,即使調整survivor空間的大小。
使用-XX:+PrintTenuringDistribution選項,對象的總字節數和目標survivor空間占用可以用來計算survivor空間的大小。重復前面的例子:
Desired survivor size 8388608 bytes, new threshold 1 (max 15)
- age 1: 16690480 bytes, 16690480 total
存活對象的總字節數是1669048,這個并發垃圾回收器(CMS)的目標survivor默認使用50%的survivor空間。通過這個信息,我們可以知道survivor空間至少應該是33380960字節,大概是32M。這個計算讓我們知道對survivor空間的預估值需要計算對象的歲數更高效以及防止溢出。為了更好的預估survivor的可用空間,你應該監控應用穩定運行情況下的任期分布,并且使用所有的額外總存活對象的字節數來作為survivor空間的大小。
在這個例子,為了讓應用計算歲數更加有效,survivor空間需要至少提升32M。前面使用的選項是:
- -Xmx1536m -Xms1536m -Xmn512m -XX:SurvivorRatio=30
那么為了保持MinorGC的頻率不發生變化,然后增加survivor空間的大小到32M,那么修改后的選項如下:
- -Xmx1568m -Xms1568m -Xmn544m -XX:SurvivvorRatio=15
當時young代空間增加了,eden空間的大小保持大概相同,且survivor的空間大小增減了。需要注意的時候,-Xmx、-Xms、-Xmn都增加了32m。另外,-XX:SurvivvorRatio=15讓每一個survivor空間的大小都是32m (544/(15+2) = 32)。
如果存在不能增加young代空間大小的限制,那么增加survivor空間大小需要以減少eden空間的大小為代價。下面是一個增加survivor空間大小,每一個survivor空間從16m增減加到32m,那么會見減少eden的空間,從480m減少到448m(512-32-32=448,512-16-16=480)。
- -Xms1536m -Xms1536m -Xmn1512m -XX:SurvivorRatio=14
再次強調,減少eden空間大小會增加MinorGC的頻率。但是,對象會在young代里面保持更長的時間,由于提升survivor的空間。
假如運行同樣的應用,我們保持eden的空間不變,增加survivor空間的大小,如下面選項:
- <span style="font-size:14px;"> -Xmx1568m -Xms1568m -Xmn544m -XX:SurvivorRatio=15</span>
可以產生如下的任期分布:
Desired survivor size 16777216 bytes, new threshold 15 (max 15)- age 1: 6115072 bytes, 6115072 total
- age 2: 286672 bytes, 6401744 total
- age 3: 115704 bytes, 6517448 total
- age 4: 95932 bytes, 6613380 total
- age 5: 89465 bytes, 6702845 total
- age 6: 88322 bytes, 6791167 total
- age 7: 88201 bytes, 6879368 total
- age 8: 88176 bytes, 6967544 total
- age 9: 88176 bytes, 7055720 total
- age 10: 88176 bytes, 7143896 total
- age 11: 88176 bytes, 7232072 total
- age 12: 88176 bytes, 7320248 total
從任期分布的情況來看,survivor空間沒有溢出,由于存活的總大小是7320248,但是預期的survivor空間大小是16777216以及任期閥值和最大任期閥值是相等的。這個表明,對象的老化速度是高效的,而且survivor空間沒有溢出。
在這個例子中,由于歲數超過3的對象很少,你可能像把最大任期閥值設置為3來測試一下,即設置選項-XX:MaxTenuringThreshhold=3,那么整個選項可以設置為:
- -Xmx1568m -Xms1658m -Xmn544m -XX:SurvivorRatio=15 -XX:MaxTenuringThreshold=3
這個選項設置和之前的選項設置的權衡是,后面這個選擇可以避免在MinorGC的時候不必要地把對象從“from” survivor復制到“to” survivor。在應用運行在穩定狀態的情況下,觀察多次MinorGC任期分布情況,看是否有對象最終移動到old代或者顯示的結果還是和前面的結果類似。如果你觀察得到和前面的任期分布情況相同,基本沒有對象的歲數達到15,也沒有survivor的空間溢出,你應該自己設置最大任期閥值以代替JVM默認的15。在這個例子中,沒有長時間存活的對象,由于在他們的歲數沒有到達15的時候就被垃圾回收了。這些對象在MinorGC中被回收了,而不是移動到old代里面。使用并發垃圾回收(CMS)的時候,對象從young代移動到old代最終會導致old的碎片增加,有可能導致stop-the-world壓縮垃圾回收,這些都是不希望出現的。寧可選擇讓對象在“from” survivor和“to” survivor中復制,也不要太快的移動到old代。
你可能需要重復數次監控任期分布、修改survivor空間大小或者重新配置young代的空間大小直到你對應用由于MinorGC引起的延遲滿意為止。如果你發現MinorGC的時間太長,你可以通過減少young代的大小直到你滿意為止。盡管,減少young代的大小,會導致更快地移動對象到old代,可能導致更多的碎片,如果CMS的并發垃圾回收能夠跟上對象的轉移率,這種情況就比不能滿足應用的延遲需求更好。如果這步不能滿足應用的MinorGC的延遲和頻率需求,這個時候就有必要重新審視需求以及修改應用程序了。
如果滿足對MinorGC延遲的需求,包括延遲時間和延遲頻率,你可以進入下一步,優化CMS垃圾回收周期的啟動,下節詳細介紹。