java jvm內存管理/gc策略/參數設置


1. JVM內存管理:深入垃圾收集器與內存分配策略

http://www.iteye.com/topic/802638

Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的高牆,牆外面的人想進去,牆里面的人卻想出來。

概述:

  說起垃圾收集(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:內存分配測試代碼

 
  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]


總結

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

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

 

2. 一次Java垃圾收集調優實戰

http://www.iteye.com/topic/212967

1 資料

  • JDK5.0垃圾收集優化之--Don't Pause(花錢的年華) 
  • 編寫對GC友好,又不泄漏的代碼(花錢的年華) 
  • JVM調優總結 
  • JDK 6所有選項及默認值 

2 GC日志打印

  GC調優是個很實驗很伽利略的活兒,GC日志是先決的數據參考和最終驗證:

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps(GC發生的時間)
-XX:+PrintGCApplicationStoppedTime(GC消耗了多少時間)
-XX:+PrintGCApplicationConcurrentTime(GC之間運行了多少時間)

3 收集器選擇

CMS收集器:暫停時間優先

   配置參數:-XX:+UseConcMarkSweepGC
   已默認無需配置的參數:-XX:+UseParNewGC(Parallel收集新生代) -XX:+CMSPermGenSweepingEnabled(CMS收集持久代) -XX:UseCMSCompactAtFullCollection(full gc時壓縮年老代)

   初始效果:1g堆內存的新生代約60m,minor gc約5-20毫秒,full gc約130毫秒。

Parallel收集器:吞吐量優先

    配置參數: -XX:+UseParallelGC -XX:+UseParallelOldGC(Parallel收集年老代,從JDK6.0開始支持)

    已默認無需配置的參數: -XX:+UseAdaptiveSizePolicy(動態調整新生代大小)

    初始效果:1g堆內存的新生代約90-110m(動態調整),minor gc約5-20毫秒,full gc有無UseParallelOldGC 參數分別為1.3/1.1秒,差別不大。

    另外-XX:MaxGCPauseMillis=100 設置minor gc的期望最大時間,JVM會以此來調整新生代的大小,但在此測試環境中對象死的太快,此參數作用不大。

4 調優實戰

      Parallel收集高達1秒的暫停時間基本不可忍受,所以選擇CMS收集器。

      在被壓測的Mule 2.0應用里,每秒都有大約400M的海量短命對象產生:

  1. 因為默認60M的新生代太小了,頻繁發生minor gc,大約0.2秒就進行一次。
  2. 因為CMS收集器中MaxTenuringThreshold(生代對象撐過過多少次minor gc才進入年老代的設置)默認0,存活的臨時對象不經過Survivor區直接進入年老代,不久就占滿年老代發生full gc。

     對這兩個參數的調優,既要改善上面兩種情況,又要避免新生代過大,復制次數過多造成minor gc的暫停時間過長。

  1. 使用-Xmn調到1/3 總內存。觀察后設置-Xmn500M,新生代實際約460m。(用-XX:NewRatio設置無效,只能用 -Xmn)。
  2. 添加-XX:+PrintTenuringDistribution 參數觀察各個Age的對象總大小,觀察后設置-XX:MaxTenuringThreshold=5。

      優化后,大約1.1秒才發生一次minor gc,且速度依然保持在15-20ms之間。同時年老代的增長速度大大減緩,很久才發生一次full gc,

      參數定稿:

 -server -Xms1024m -Xmx1024m -Xmn500m -XX:+UseConcMarkSweepGC   -XX:MaxTenuringThreshold=5 
-XX:+ExplicitGCInvokesConcurrent

      最后服務處理速度從1180 tps 上升到1380 tps,調整兩個參數提升17%的性能還是筆很划算的買賣。

 

     另外,JDK6 Update 7自帶了一個VisualVM工具,內里就是之前也有用過的Netbean Profiler,類似JConsole一樣使用,可以看到線程狀態,內存中對象以及方法的CPU時間等調優重要參考依據。免費捆綁啊,Sun 這樣搞法,其他做Profiler的公司要關門了。

3.JVM gc參數設置與分析

來自:http://hi.baidu.com/i1see1you/blog/item/7ba0d250c30131481038c20c.html

 

一.概述

java的最大好處是自動垃圾回收,這樣就無需我們手動的釋放對象空間了,但是也產生了相應的負效果,gc是需要時間和資源的,不好的gc會嚴重影響系統的系能,因此良好的gc是JVM的高性能的保證。JVM堆分為新生代,舊生代和年老代,新生代可用的gc方式有:串行gc(Serial Copying),並行回收gc(Parellel Scavenge),並行gc(ParNew),舊生代和年老代可用的gc方式有串行gc(Serial MSC),並行gc(Parallel MSC),並發gc(CMS)。

二.回收方式的選擇

jvm有client和server兩種模式,這兩種模式的gc默認方式是不同的:

clien模式下,新生代選擇的是串行gc,舊生代選擇的是串行gc

server模式下,新生代選擇的是並行回收gc,舊生代選擇的是並行gc

一般來說我們系統應用選擇有兩種方式:吞吐量優先和暫停時間優先,對於吞吐量優先的采用server默認的並行gc方式,對於暫停時間優先的選用並發gc(CMS)方式。

三.CMS gc

CMS,全稱Concurrent Low Pause Collector,是jdk1.4后期版本開始引入的新gc算法,在jdk5和jdk6中得到了進一步改進,它的主要適合場景是對響應時間的重要性需求大於對吞吐量的要求,能夠承受垃圾回收線程和應用線程共享處理器資源,並且應用中存在比較多的長生命周期的對象的應用。CMS是用於對tenured generation的回收,也就是年老代的回收,目標是盡量減少應用的暫停時間,減少full gc發生的幾率,利用和應用程序線程並發的垃圾回收線程來標記清除年老代。在我們的應用中,因為有緩存的存在,並且對於響應時間也有比較高的要求,因此希望能嘗試使用CMS來替代默認的server型JVM使用的並行收集器,以便獲得更短的垃圾回收的暫停時間,提高程序的響應性。
    CMS並非沒有暫停,而是用兩次短暫停來替代串行標記整理算法的長暫停,它的收集周期是這樣:
    初始標記(CMS-initial-mark) -> 並發標記(CMS-concurrent-mark) -> 重新標記(CMS-remark) -> 並發清除(CMS-concurrent-sweep) ->並發重設狀態等待下次CMS的觸發(CMS-concurrent-reset)

    其中的1,3兩個步驟需要暫停所有的應用程序線程的。第一次暫停從root對象開始標記存活的對象,這個階段稱為初始標記;第二次暫停是在並發標記之后,暫停所有應用程序線程,重新標記並發標記階段遺漏的對象(在並發標記階段結束后對象狀態的更新導致)。第一次暫停會比較短,第二次暫停通常會比較長,並且 remark這個階段可以並行標記。

    而並發標記、並發清除、並發重設階段的所謂並發,是指一個或者多個垃圾回收線程和應用程序線程並發地運行,垃圾回收線程不會暫停應用程序的執行,如果你有多於一個處理器,那么並發收集線程將與應用線程在不同的處理器上運行,顯然,這樣的開銷就是會降低應用的吞吐量。Remark階段的並行,是指暫停了所有應用程序后,啟動一定數目的垃圾回收進程進行並行標記,此時的應用線程是暫停的。

四.full  gc

full gc是對新生代,舊生代,以及持久代的統一回收,由於是對整個空間的回收,因此比較慢,系統中應當盡量減少full gc的次數。

如下幾種情況下會發生full gc:

《舊生代空間不足

《持久代空間不足

《CMS GC時出現了promotion failed和concurrent mode failure

《統計得到新生代minor gc時晉升到舊生代的平均大小小於舊生代剩余空間

《直接調用System.gc,可以DisableExplicitGC來禁止

《存在rmi調用時,默認會每分鍾執行一次System.gc,可以通過-Dsun.rmi.dgc.server.gcInterval=3600000來設置大點的間隔。

五.示例

下面對如下的參數進行分析:

JAVA_OPTS="-server -Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m -XX:SurvivorRatio=4

-verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log -Djava.awt.headless=true -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000

-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15"

-Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m

Xms,即為jvm啟動時得JVM初始堆大小,Xmx為jvm的最大堆大小,xmn為新生代的大小,permsize為永久代的初始大小,MaxPermSize為永久代的最大空間。

-XX:SurvivorRatio=4

SurvivorRatio為新生代空間中的Eden區和救助空間Survivor區的大小比值,默認是32,也就是說Eden區是 Survivor區的32倍大小,要注意Survivo是有兩個區的,因此Surivivor其實占整個young genertation的1/34。調小這個參數將增大survivor區,讓對象盡量在survitor區呆長一點,減少進入年老代的對象。去掉救助空間的想法是讓大部分不能馬上回收的數據盡快進入年老代,加快年老代的回收頻率,減少年老代暴漲的可能性,這個是通過將-XX:SurvivorRatio 設置成比較大的值(比如65536)來做到。

 -verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log

將虛擬機每次垃圾回收的信息寫到日志文件中,文件名由file指定,文件格式是平文件,內容和-verbose:gc輸出內容相同。

-Djava.awt.headless=true

Headless模式是系統的一種配置模式。在該模式下,系統缺少了顯示設備、鍵盤或鼠標。

-XX:+PrintGCTimeStamps -XX:+PrintGCDetails

設置gc日志的格式

-Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000 

指定rmi調用時gc的時間間隔

-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15

采用並發gc方式,經過15次minor gc 后進入年老代六.一些常見問題1.為了避免Perm區滿引起的full gc,建議開啟CMS回收Perm區選項:
+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled

2.默認CMS是在tenured generation沾滿68%的時候開始進行CMS收集,如果你的年老代增長不是那么快,並且希望降低CMS次數的話,可以適當調高此值:
-XX:CMSInitiatingOccupancyFraction=80

3.遇到兩種fail引起full gc:Prommotion failed和Concurrent mode failed時:
Prommot


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM