深入垃圾收集器與內存分配策略

概述: 

  說起垃圾收集(Garbage Collection,下文簡稱GC),大部分人都把這項技術當做Java語言的伴生產物。事實上GC的歷史遠遠比Java來得久遠,在1960年誕生於MIT的Lisp是第一門真正使用內存動態分配和垃圾收集技術的語言。當Lisp還在胚胎時期,人們就在思考GC需要完成的3件事情:哪些內存需要回收?什麼時候回收?怎麼樣回收? 

  經過半個世紀的發展,目前的內存分配策略與垃圾回收技術已經相當成熟,一切看起來都進入“自動化”的時代,那爲什麼我們還要去了解GC和內存分配?答案很簡單:當需要排查各種內存溢出、泄漏問題時,當垃圾收集成爲系統達到更高併發量的瓶頸時,我們就需要對這些“自動化”的技術有必要的監控、調節手段。 

  把時間從1960年撥回現在,回到我們熟悉的Java語言。本文第一章中介紹了Java內存運行時區域的各個部分,其中程序計數器、VM棧、本地方法棧三個區域隨線程而生,隨線程而滅;棧中的幀隨着方法進入、退出而有條不紊的進行着出棧入棧操作;每一個幀中分配多少內存基本上是在Class文件生成時就已知的(可能會由JIT動態晚期編譯進行一些優化,但大體上可以認爲是編譯期可知的),因此這幾個區域的內存分配和回收具備很高的確定性,因此在這幾個區域不需要過多考慮回收的問題。而Java堆和方法區(包括運行時常量池)則不一樣,我們必須等到程序實際運行期間才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,我們本文後續討論中的“內存”分配與回收僅僅指這一部分內存。 

對象已死? 

  在堆裏面存放着Java世界中幾乎所有的對象,在回收前首先要確定這些對象之中哪些還在存活,哪些已經“死去”了,即不可能再被任何途徑使用的對象。 

引用計數算法(Reference Counting) 

  最初的想法,也是很多教科書判斷對象是否存活的算法是這樣的:給對象中添加一個引用計數器,當有一個地方引用它,計數器加1,當引用失效,計數器減1,任何時刻計數器爲0的對象就是不可能再被使用的。 

  客觀的說,引用計數算法實現簡單,判定效率很高,在大部分情況下它都是一個不錯的算法,但引用計數算法無法解決對象循環引用的問題。舉個簡單的例子:對象A和B分別有字段b、a,令A.b=B和B.a=A,除此之外這2個對象再無任何引用,那實際上這2個對象已經不可能再被訪問,但是引用計數算法卻無法回收他們。 

根搜索算法(GC Roots Tracing) 

  在實際生產的語言中(Java、C#、甚至包括前面提到的Lisp),都是使用根搜索算法判定對象是否存活。算法基本思路就是通過一系列的稱爲“GC Roots”的點作爲起始進行向下搜索,當一個對象到GC Roots沒有任何引用鏈(Reference Chain)相連,則證明此對象是不可用的。在Java語言中,GC Roots包括: 

  1.在VM棧(幀中的本地變量)中的引用 
  2.方法區中的靜態引用 
  3.JNI(即一般說的Native方法)中的引用 

生存還是死亡? 

  判定一個對象死亡,至少經歷兩次標記過程:如果對象在進行根搜索後,發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記,並在稍後執行他的finalize()方法(如果它有的話)。這裏所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。這點是必須的,否則一個對象在finalize()方法執行緩慢,甚至有死循環什麼的將會很容易導致整個系統崩潰。finalize()方法是對象最後一次逃脫死亡命運的機會,稍後GC將進行第二次規模稍小的標記,如果在finalize()中對象成功拯救自己(只要重新建立到GC Roots的連接即可,譬如把自己賦值到某個引用上),那在第二次標記時它將被移除出“即將回收”的集合,如果對象這時候還沒有逃脫,那基本上它就真的離死不遠了。 

  需要特別說明的是,這裏對finalize()方法的描述可能帶點悲情的藝術加工,並不代表筆者鼓勵大家去使用這個方法來拯救對象。相反,筆者建議大家儘量避免使用它,這個不是C/C++裏面的析構函數,它運行代價高昂,不確定性大,無法保證各個對象的調用順序。需要關閉外部資源之類的事情,基本上它能做的使用try-finally可以做的更好。 

關於方法區 

  方法區即後文提到的永久代,很多人認爲永久代是沒有GC的,《Java虛擬機規範》中確實說過可以不要求虛擬機在這區實現GC,而且這區GC的“性價比”一般比較低:在堆中,尤其是在新生代,常規應用進行一次GC可以一般可以回收70%~95%的空間,而永久代的GC效率遠小於此。雖然VM Spec不要求,但當前生產中的商業JVM都有實現永久代的GC,主要回收兩部分內容:廢棄常量與無用類。這兩點回收思想與Java堆中的對象回收很類似,都是搜索是否存在引用,常量的相對很簡單,與對象類似的判定即可。而類的回收則比較苛刻,需要滿足下面3個條件: 

  1.該類所有的實例都已經被GC,也就是JVM中不存在該Class的任何實例。 
  2.加載該類的ClassLoader已經被GC。 
  3.該類對應的java.lang.Class 對象沒有在任何地方被引用,如不能在任何地方通過反射訪問該類的方法。 

  是否對類進行回收可使用-XX:+ClassUnloading參數進行控制,還可以使用-verbose:class或者-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類加載、卸載信息。 

  在大量使用反射、動態代理、CGLib等bytecode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要JVM具備類卸載的支持以保證永久代不會溢出。 

垃圾收集算法 

  在這節裏不打算大量討論算法實現,只是簡單的介紹一下基本思想以及發展過程。最基礎的蒐集算法是“標記-清除算法”(Mark-Sweep),如它的名字一樣,算法分層“標記”和“清除”兩個階段,首先標記出所有需要回收的對象,然後回收所有需要回收的對象,整個過程其實前一節講對象標記判定的時候已經基本介紹完了。說它是最基礎的收集算法原因是後續的收集算法都是基於這種思路並優化其缺點得到的。它的主要缺點有兩個,一是效率問題,標記和清理兩個過程效率都不高,二是空間問題,標記清理之後會產生大量不連續的內存碎片,空間碎片太多可能會導致後續使用中無法找到足夠的連續內存而提前觸發另一次的垃圾蒐集動作。 

  爲了解決效率問題,一種稱爲“複製”(Copying)的蒐集算法出現,它將可用內存劃分爲兩塊,每次只使用其中的一塊,當半區內存用完了,僅將還存活的對象複製到另外一塊上面,然後就把原來整塊內存空間一次過清理掉。這樣使得每次內存回收都是對整個半區的回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存就可以了,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半,未免太高了一點。 

  現在的商業虛擬機中都是用了這一種收集算法來回收新生代,IBM有專門研究表明新生代中的對象98%是朝生夕死的,所以並不需要按照1:1的比例來劃分內存空間,而是將內存分爲一塊較大的eden空間和2塊較少的survivor空間,每次使用eden和其中一塊survivor,當回收時將eden和survivor還存活的對象一次過拷貝到另外一塊survivor空間上,然後清理掉eden和用過的survivor。Sun Hotspot虛擬機默認eden和survivor的大小比例是8:1,也就是每次只有10%的內存是“浪費”的。當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有10%以內的對象存活,當survivor空間不夠用時,需要依賴其他內存(譬如老年代)進行分配擔保(Handle Promotion)。 

  複製收集算法在對象存活率高的時候,效率有所下降。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保用於應付半區內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。因此人們提出另外一種“標記-整理”(Mark-Compact)算法,標記過程仍然一樣,但後續步驟不是進行直接清理,而是令所有存活的對象一端移動,然後直接清理掉這端邊界以外的內存。 

  當前商業虛擬機的垃圾收集都是採用“分代收集”(Generational Collecting)算法,這種算法並沒有什麼新的思想出現,只是根據對象不同的存活週期將內存劃分爲幾塊。一般是把Java堆分作新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法,譬如新生代每次GC都有大批對象死去,只有少量存活,那就選用複製算法只需要付出少量存活對象的複製成本就可以完成收集。 

垃圾收集器 

  垃圾收集器就是收集算法的具體實現,不同的虛擬機會提供不同的垃圾收集器。並且提供參數供用戶根據自己的應用特點和要求組合各個年代所使用的收集器。本文討論的收集器基於Sun Hotspot虛擬機1.6版。 

圖1.Sun JVM1.6的垃圾收集器 
 

  圖1展示了1.6中提供的6種作用於不同年代的收集器,兩個收集器之間存在連線的話就說明它們可以搭配使用。在介紹着些收集器之前,我們先明確一個觀點:沒有最好的收集器,也沒有萬能的收集器,只有最合適的收集器。 

1.Serial收集器 
  單線程收集器,收集時會暫停所有工作線程(我們將這件事情稱之爲Stop The World,下稱STW),使用複製收集算法,虛擬機運行在Client模式時的默認新生代收集器。 

2.ParNew收集器 
  ParNew收集器就是Serial的多線程版本,除了使用多條收集線程外,其餘行爲包括算法、STW、對象分配規則、回收策略等都與Serial收集器一摸一樣。對應的這種收集器是虛擬機運行在Server模式的默認新生代收集器,在單CPU的環境中,ParNew收集器並不會比Serial收集器有更好的效果。 

3.Parallel Scavenge收集器 
  Parallel Scavenge收集器(下稱PS收集器)也是一個多線程收集器,也是使用複製算法,但它的對象分配規則與回收策略都與ParNew收集器有所不同,它是以吞吐量最大化(即GC時間佔總運行時間最小)爲目標的收集器實現,它允許較長時間的STW換取總吞吐量最大化。 

4.Serial Old收集器 
  Serial Old是單線程收集器,使用標記-整理算法,是老年代的收集器,上面三種都是使用在新生代收集器。 

5.Parallel Old收集器 
  老年代版本吞吐量優先收集器,使用多線程和標記-整理算法,JVM 1.6提供,在此之前,新生代使用了PS收集器的話,老年代除Serial Old外別無選擇,因爲PS無法與CMS收集器配合工作。 

6.CMS(Concurrent Mark Sweep)收集器 
  CMS是一種以最短停頓時間爲目標的收集器,使用CMS並不能達到GC效率最高(總體GC時間最小),但它能儘可能降低GC時服務的停頓時間,這一點對於實時或者高交互性應用(譬如證券交易)來說至關重要,這類應用對於長時間STW一般是不可容忍的。CMS收集器使用的是標記-清除算法,也就是說它在運行期間會產生空間碎片,所以虛擬機提供了參數開啓CMS收集結束後再進行一次內存壓縮。 
內存分配與回收策略 

  瞭解GC其中很重要一點就是了解JVM的內存分配策略:即對象在哪裏分配和對象什麼時候回收。 

  關於對象在哪裏分配,往大方向講,主要就在堆上分配,但也可能經過JIT進行逃逸分析後進行標量替換拆散爲原子類型在棧上分配,也可能分配在DirectMemory中(詳見本文第一章)。往細節處講,對象主要分配在新生代eden上,也可能會直接老年代中,分配的細節決定於當前使用的垃圾收集器類型與VM相關參數設置。我們可以通過下面代碼來驗證一下Serial收集器(ParNew收集器的規則與之完全一致)的內存分配和回收的策略。讀者看完Serial收集器的分析後,不妨自己根據JVM參數文檔寫一些程序去實踐一下其它幾種收集器的分配策略。 

清單1:內存分配測試代碼 
Java代碼  收藏代碼
  1. public class YoungGenGC {  
  2.   
  3.     private static final int _1MB = 1024 * 1024;  
  4.   
  5.     public static void main(String[] args) {  
  6.         // testAllocation();  
  7.         testHandlePromotion();  
  8.         // testPretenureSizeThreshold();  
  9.         // testTenuringThreshold();  
  10.         // testTenuringThreshold2();  
  11.     }  
  12.   
  13.     /** 
  14.      * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 
  15.      */  
  16.     @SuppressWarnings("unused")  
  17.     public static void testAllocation() {  
  18.         byte[] allocation1, allocation2, allocation3, allocation4;  
  19.         allocation1 = new byte[2 * _1MB];  
  20.         allocation2 = new byte[2 * _1MB];  
  21.         allocation3 = new byte[2 * _1MB];  
  22.         allocation4 = new byte[4 * _1MB];  // 出現一次Minor GC  
  23.     }  
  24.   
  25.     /** 
  26.      * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 
  27.      * -XX:PretenureSizeThreshold=3145728 
  28.      */  
  29.     @SuppressWarnings("unused")  
  30.     public static void testPretenureSizeThreshold() {  
  31.         byte[] allocation;  
  32.         allocation = new byte[4 * _1MB];  //直接分配在老年代中  
  33.     }  
  34.   
  35.     /** 
  36.      * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 
  37.      * -XX:+PrintTenuringDistribution 
  38.      */  
  39.     @SuppressWarnings("unused")  
  40.     public static void testTenuringThreshold() {  
  41.         byte[] allocation1, allocation2, allocation3;  
  42.         allocation1 = new byte[_1MB / 4];  // 什麼時候進入老年代決定於XX:MaxTenuringThreshold設置  
  43.         allocation2 = new byte[4 * _1MB];  
  44.         allocation3 = new byte[4 * _1MB];  
  45.         allocation3 = null;  
  46.         allocation3 = new byte[4 * _1MB];  
  47.     }  
  48.   
  49.     /** 
  50.      * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 
  51.      * -XX:+PrintTenuringDistribution 
  52.      */  
  53.     @SuppressWarnings("unused")  
  54.     public static void testTenuringThreshold2() {  
  55.         byte[] allocation1, allocation2, allocation3, allocation4;  
  56.         allocation1 = new byte[_1MB / 4];   // allocation1+allocation2大於survivo空間一半  
  57.         allocation2 = new byte[_1MB / 4];    
  58.         allocation3 = new byte[4 * _1MB];  
  59.         allocation4 = new byte[4 * _1MB];  
  60.         allocation4 = null;  
  61.         allocation4 = new byte[4 * _1MB];  
  62.     }  
  63.   
  64.     /** 
  65.      * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure 
  66.      */  
  67.     @SuppressWarnings("unused")  
  68.     public static void testHandlePromotion() {  
  69.         byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;  
  70.         allocation1 = new byte[2 * _1MB];  
  71.         allocation2 = new byte[2 * _1MB];  
  72.         allocation3 = new byte[2 * _1MB];  
  73.         allocation1 = null;  
  74.         allocation4 = new byte[2 * _1MB];  
  75.         allocation5 = new byte[2 * _1MB];  
  76.         allocation6 = new byte[2 * _1MB];  
  77.         allocation4 = null;  
  78.         allocation5 = null;  
  79.         allocation6 = null;  
  80.         allocation7 = new byte[2 * _1MB];  
  81.     }  
  82. }  


規則一:通常情況下,對象在eden中分配。當eden無法分配時,觸發一次Minor GC。 

  執行testAllocation()方法後輸出了GC日誌以及內存分配狀況。-Xms20M -Xmx20M -Xmn10M這3個參數確定了Java堆大小爲20M,不可擴展,其中10M分配給新生代,剩下的10M即爲老年代。-XX:SurvivorRatio=8決定了新生代中eden與survivor的空間比例是1:8,從輸出的結果也清晰的看到“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代總可用空間爲9216K(eden+1個survivor)。 

  我們也注意到在執行testAllocation()時出現了一次Minor GC,GC的結果是新生代6651K變爲148K,而總佔用內存則幾乎沒有減少(因爲幾乎沒有可回收的對象)。這次GC是發生的原因是爲allocation4分配內存的時候,eden已經被佔用了6M,剩餘空間已不足分配allocation4所需的4M內存,因此發生Minor GC。GC期間虛擬機發現已有的3個2M大小的對象全部無法放入survivor空間(survivor空間只有1M大小),所以直接轉移到老年代去。GC後4M的allocation4對象分配在eden中。 

清單2:testAllocation()方法輸出結果 

[GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap 
def new generation   total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000) 
  eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000) 
  from space 1024K,  14% used [0x032d0000, 0x032f5370, 0x033d0000) 
  to   space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000) 
tenured generation   total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000) 
   the space 10240K,  60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000) 
compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) 
   the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000) 
No shared spaces configured. 

規則二:配置了PretenureSizeThreshold的情況下,對象大於設置值將直接在老年代分配。 

  執行testPretenureSizeThreshold()方法後,我們看到eden空間幾乎沒有被使用,而老年代的10M控件被使用了40%,也就是4M的allocation對象直接就分配在老年代中,則是因爲PretenureSizeThreshold被設置爲3M,因此超過3M的對象都會直接從老年代分配。 

清單3: 

Heap 
def new generation   total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000) 
  eden space 8192K,   8% used [0x029d0000, 0x02a77e98, 0x031d0000) 
  from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000) 
  to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000) 
tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000) 
   the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000) 
compacting perm gen  total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000) 
   the space 12288K,  17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000) 
No shared spaces configured. 

規則三:在eden經過GC後存活,並且survivor能容納的對象,將移動到survivor空間內,如果對象在survivor中繼續熬過若干次回收(默認爲15次)將會被移動到老年代中。回收次數由MaxTenuringThreshold設置。 

  分別以-XX:MaxTenuringThreshold=1和-XX:MaxTenuringThreshold=15兩種設置來執行testTenuringThreshold(),方法中allocation1對象需要256K內存,survivor空間可以容納。當MaxTenuringThreshold=1時,allocation1對象在第二次GC發生時進入老年代,新生代已使用的內存GC後非常乾淨的變成0KB。而MaxTenuringThreshold=15時,第二次GC發生後,allocation1對象則還留在新生代survivor空間,這時候新生代仍然有404KB被佔用。 

清單4: 
MaxTenuringThreshold=1 

[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 1 (max 1) 
- age   1:     414664 bytes,     414664 total 
: 4859K->404K(9216K), 0.0065012 secs] 4859K->4500K(19456K), 0.0065283 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 1 (max 1) 
: 4500K->0K(9216K), 0.0009253 secs] 8596K->4500K(19456K), 0.0009458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap 
def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000) 
  eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000) 
  from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000) 
  to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000) 
tenured generation   total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000) 
   the space 10240K,  43% used [0x033d0000, 0x03835348, 0x03835400, 0x03dd0000) 
compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) 
   the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000) 
No shared spaces configured. 

MaxTenuringThreshold=15 
[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 15 (max 15) 
- age   1:     414664 bytes,     414664 total 
: 4859K->404K(9216K), 0.0049637 secs] 4859K->4500K(19456K), 0.0049932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 15 (max 15) 
- age   2:     414520 bytes,     414520 total 
: 4500K->404K(9216K), 0.0008091 secs] 8596K->4500K(19456K), 0.0008305 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap 
def new generation   total 9216K, used 4582K [0x029d0000, 0x033d0000, 0x033d0000) 
  eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000) 
  from space 1024K,  39% used [0x031d0000, 0x03235338, 0x032d0000) 
  to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000) 
tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000) 
   the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000) 
compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) 
   the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000) 
No shared spaces configured. 

規則四:如果在survivor空間中相同年齡所有對象大小的累計值大於survivor空間的一半,大於或等於個年齡的對象就可以直接進入老年代,無需達到MaxTenuringThreshold中要求的年齡。 

  執行testTenuringThreshold2()方法,並將設置-XX:MaxTenuringThreshold=15,發現運行結果中survivor佔用仍然爲0%,而老年代比預期增加了6%,也就是說allocation1、allocation2對象都直接進入了老年代,而沒有等待到15歲的臨界年齡。因爲這2個對象加起來已經到達了512K,並且它們是同年的,滿足同年對象達到survivor空間的一半規則。我們只要註釋掉其中一個對象new操作,就會發現另外一個就不會晉升到老年代中去了。 

清單5: 
[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 1 (max 15) 
- age   1:     676824 bytes,     676824 total 
: 5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 15 (max 15) 
: 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap 
def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000) 
  eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000) 
  from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000) 
  to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000) 
tenured generation   total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000) 
   the space 10240K,  46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000) 
compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) 
   the space 12288K,  17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000) 
No shared spaces configured. 

規則五:在Minor GC觸發時,會檢測之前每次晉升到老年代的平均大小是否大於老年代的剩餘空間,如果大於,改爲直接進行一次Full GC,如果小於則查看HandlePromotionFailure設置看看是否允許擔保失敗,如果允許,那仍然進行Minor GC,如果不允許,則也要改爲進行一次Full GC。 

  前面提到過,新生代纔有複製收集算法,但爲了內存利用率,只使用其中一個survivor空間來作爲輪換備份,因此當出現大量對象在GC後仍然存活的情況(最極端就是GC後所有對象都存活),就需要老年代進行分配擔保,把survivor無法容納的對象直接放入老年代。與生活中貸款擔保類似,老年代要進行這樣的擔保,前提就是老年代本身還有容納這些對象的剩餘空間,一共有多少對象在GC之前是無法明確知道的,所以取之前每一次GC晉升到老年代對象容量的平均值與老年代的剩餘空間進行比較決定是否進行Full GC來讓老年代騰出更多空間。 

  取平均值進行比較其實仍然是一種動態概率的手段,也就是說如果某次Minor GC存活後的對象突增,大大高於平均值的話,依然會導致擔保失敗,這樣就只好在失敗後重新進行一次Full GC。雖然擔保失敗時做的繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure打開,避免Full GC過於頻繁。 

清單6: 
HandlePromotionFailure = false 

[GC [DefNew: 6651K->148K(9216K), 0.0078936 secs] 6651K->4244K(19456K), 0.0079192 secs] [Times: user=0.00 sys=0.02, real=0.02 secs] 
[GC [DefNew: 6378K->6378K(9216K), 0.0000206 secs][Tenured: 4096K->4244K(10240K), 0.0042901 secs] 10474K->4244K(19456K), [Perm : 2104K->2104K(12288K)], 0.0043613 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

HandlePromotionFailure = true 

[GC [DefNew: 6651K->148K(9216K), 0.0054913 secs] 6651K->4244K(19456K), 0.0055327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC [DefNew: 6378K->148K(9216K), 0.0006584 secs] 10474K->4244K(19456K), 0.0006857 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

總結 

  本章介紹了垃圾收集的算法、6款主要的垃圾收集器,以及通過代碼實例具體介紹了新生代串行收集器對內存分配及回收的影響。 

  GC在很多時候都是系統併發度的決定性因素,虛擬機之所以提供多種不同的收集器,提供大量的調節參數,是因爲只有根據實際應用需求、實現方式選擇最優的收集方式才能獲取最好的性能。沒有固定收集器、參數組合,也沒有最優的調優方法,虛擬機也沒有什麼必然的行爲。筆者看過一些文章,撇開具體場景去談論老年代達到92%會觸發Full GC(92%應當來自CMS收集器觸發的默認臨界點)、98%時間在進行垃圾收集系統會拋出OOM異常(98%應該來自parallel收集器收集時間比率的默認臨界點)其實意義並不太大。因此學習GC如果要到實踐調優階段,必須瞭解每個具體收集器的行爲、優勢劣勢、調節參數。 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章