一步步優化JVM五:優化延遲或者響應時間(2)

優化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空間。比如說,下面的一條語句:
   
   Map<String,String> map = new HashMap<String,String>();


   一個新的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空間的大小,如下面選項:

 
 -Xmx1568m -Xms1568m -Xmn544m -XX:SurvivorRatio=15


   可以產生如下的任期分佈:
   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垃圾回收週期的啓動,下節詳細介紹。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章