1、簡介
a、意義
垃圾回收可以有效的防止記憶體洩露,有效的使用空閑的記憶體。 記憶體洩露指該記憶體空間使用完畢後未回收,在不涉及複雜資料結構的一般情況下,java的記憶體洩露表現為一個記憶體對象的生命周期超出了程式需要它的時間長度,也将其稱為“對象遊離”。
b、判斷
判斷廢棄常量判斷無用的類
- 如果常量池中的某個常量沒有被任何引用所引用,則該常量是廢棄常量。
- 該類的所有執行個體都已經被回收,即java堆中不存在該類的執行個體對象。
- 加載該類的類加載器已經被回收。
- 該類所對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射機制通路該類的方法。
c、特性
- 垃圾回收機制隻負責回收記憶體中的對象,不會回收任何實體資源(例如資料庫連接配接、網絡IO等資源 );
- 程式無法精确控制垃圾回收的運作,垃圾回收會在合适的時候進行。當對象永久地失去引用後,系統就會在合适的時候回收它所占有的記憶體 ;
- 垃圾回收機制回收任何對象執之前,總會調用finalize () 方法,該方法可能是該對象重新複活(然一個引用變量重新引用該對象 ), 進而導緻垃圾回收機制取消回收。
2、常用JVM參數配置
- -server//伺服器模式
- -Xmx2g //JVM最大允許配置設定的堆記憶體,按需配置設定
- -Xms2g //JVM初始配置設定的堆記憶體,一般和Xmx配置成一樣以避免每次gc後JVM重新配置設定記憶體。
- -Xmn256m //年輕代記憶體大小,整個JVM記憶體=年輕代 + 年老代 + 持久代
- -XX:PermSize=128m //持久代記憶體大小
- -Xss256k //設定每個線程的堆棧大小
- -XX:+DisableExplicitGC //忽略手動調用GC, System.gc()的調用就會變成一個空調用,完全不觸發GC
- -XX:+UseConcMarkSweepGC //并發标記清除(CMS)收集器
- -XX:+CMSParallelRemarkEnabled //降低标記停頓
- -XX:+UseCMSCompactAtFullCollection //在FULL GC的時候對年老代的壓縮
- -XX:LargePageSizeInBytes=128m //記憶體頁的大小
- -XX:+UseFastAccessorMethods //原始類型的快速優化
- -XX:+UseCMSInitiatingOccupancyOnly //使用手動定義初始化定義開始CMS收集
- -XX:CMSInitiatingOccupancyFraction=70 //使用cms作為垃圾回收使用70%後開始CMS
對于CMS GC時出現promotion failed和concurrent mode failure的調優:
-server
-Xms6000M
-Xmx6000M
-Xmn500M
-XX:PermSize=M
-XX:MaxPermSize=M
-XX:SurvivorRatio=
-XX:MaxTenuringThreshold=
-Xnoclassgc
-XX:+DisableExplicitGC
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=
-XX:+CMSClassUnloadingEnabled
-XX:-CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=
-XX:SoftRefLRUPolicyMSPerMB=
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:log/gc.log
産生原因:
promotion failed是在進行Minor GC時,survivor space放不下、對象隻能放入舊生代,而此時舊生代也放不下造成的;concurrent mode failure是在執行CMS GC的過程中同時有對象要放入舊生代,而此時舊生代空間不足造成的。
3、垃圾收集算法
a、引用計數
每個對象計算指向它的指針的數量,當有一個指針指向自己時計數值加1;當删除一個指向自己的指針時,計數值減1,如果計數值減為0,說明已經不存在指向該對象的指針了,是以它可以被安全的銷毀了。
- 優點是它在進行垃圾回收的時候無需挂起程式,常用在實時系統中;空間上的引用局部性比較好。某個對象的引用計數變為0後,系統無需通路位于堆中其他頁面的單元。廢棄即回收。
- 缺點:每次建立和銷毀都要更新引用計數值,會引起額外的開銷;引用計數占據了額外的空間;無法處理環形引用;
b、标記-清除
标記和清除兩個階段,标記出所有需要回收的對象,标記完成後統一回收所有被标記的對象。
- 缺點:效率問題,标記和清楚過程的效率都不高;空間問題,标記清楚後會産生大量不連續的記憶體碎片,空間碎片太多可能會導緻當程式在以後的運作過程中需要配置設定較大對象時無法找到足夠連續的記憶體空間而不得不提前出發另一次垃圾收集動作。
c、複制算法
為了解決效率問題,複制算法将可用記憶體按容量劃分為大小相等的兩塊,每次隻是用其中一塊。當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。
- 優點:這樣使得每次都是對其中的一塊進行記憶體回收,沒存配置設定時也就不用考慮記憶體碎片等複雜情況,隻要移動堆頂指針,按順序配置設定記憶體即可,實作簡單,運作高效。
- 缺點:可用記憶體縮小為原來的一半。
實際上新生代中的對象98%是朝生夕死的,并不需要按照1:1的比例來劃分記憶體空間,而是将記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survivor。回收時,将Eden和Survivor中還存活着的兌現個一次性地拷貝到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的Survivor的空間。HotSpot虛拟機預設Eden和Survivor的大小比例是8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的90%,隻有10%的記憶體是會被浪費的。
d、标記整理
标記-整理(Mark-Compact)算法不直接對可回收對象進行清理,而是讓所有可用的對象都向一端移動,然後直接清理掉邊界意外的記憶體。即在清理無用對象完成後讓所有存活的對象都向一端移動,并更新引用其對象的指針。缺點:在标記-清除的基礎上還需進行對象的移動,成本相對較高,好處則是不會産生記憶體碎片。
e、分代收集
據記憶體中對象的存活周期不同,将記憶體劃分為幾塊,java的虛拟機中一般把記憶體劃分為新生代和年老代,當新建立對象時一般在新生代中配置設定記憶體空間,當新生代垃圾收集器回收幾次之後仍然存活的對象會被移動到年老代記憶體中,當大對象在新生代中無法找到足夠的連續記憶體時也直接在年老代中建立。
新生代使用複制和标記-清除垃圾收集算法。
年老代中的對象一般都是長生命周期對象,對象的存活率比較高,是以在年老代中使用标記-整理垃圾回收算法。
java虛拟機記憶體中的方法區在Sun HotSpot虛拟機中被稱為永久代,是被各個線程共享的記憶體區域,它用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯後的代碼等資料。永久代垃圾回收比較少,效率也比較低,但是也必須進行垃圾回收,否則會永久代記憶體不夠用時仍然會抛出OutOfMemoryError異常。
3、垃圾收集器
串行垃圾回收:
串行垃圾回收器通過持有應用程式所有的線程進行工作。它為單線程環境設計,隻使用一個單獨的線程進行垃圾回收,通過當機所有應用程式線程進行工作,是以可能不适合伺服器環境。它最适合的是簡單的指令行程式。通過JVM參數-XX:+UseSerialGC可以使用串行垃圾回收器。
并行垃圾回收:
并行垃圾回收器也叫做 throughput collector 。它是JVM的預設垃圾回收器。與串行垃圾回收器不同,它使用多線程進行垃圾回收。不過同的是:當執行垃圾回收的時候它也會當機所有的應用程式線程。
并發垃圾回收:
并發标記垃圾回收使用多線程掃描堆記憶體,标記需要清理的執行個體并且清理被标記過的執行個體。相比并行垃圾回收器,并發标記掃描垃圾回收器使用更多的CPU來確定程式的吞吐量。如果我們可以為了更好的程式性能配置設定更多的CPU,那麼并發标記上掃描垃圾回收器是更好的選擇相比并發垃圾回收器。通過JVM參數 XX:+USeParNewGC 打開并發标記掃描垃圾回收器。
^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
Serial收集器
Serial收集器是最基本、發展曆史最悠久的單線程收集器,但它的“單線程”的意義并不僅僅說明它隻會使用一個CPU或一條收集線程去完成垃圾收集工作。重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。
ParNew收集器
ParNew收集器是Serial收集器的多線程版本。除了使用多條線程進行垃圾收集之外,其餘行為包括Serial收集器可用的所有控制參數、收集算法、Stop The World、對象配置設定規則、回收政策等都與Serial收集器完全一樣。這兩種收集器在實作上也共用了相當多的代碼。ParNew收集器是許多運作在Server模式下的虛拟機中首選的新生代收集器。
Parallel Scavenge收集器
Parallel Scavenge收集器是新生代使用複制算法的并行多線程收集器。它的關注點與其它收集器不同,CMS等收集器的關注點盡可能地縮短垃圾收集時使用者線程的停頓時間,而Parallel Scavenge收集器的目标則是達到一個可控制的吞吐量(Throughput),也被稱為吞吐量優先收集器。
所謂吞吐量就是CPU用于運作使用者代碼時間與CPU總消耗時間的比值。吞吐量=運作使用者代碼時間/運作使用者代碼時間+垃圾收集時間。其停頓時間短,适合需要與使用者互動的程式,良好的響應速度能提升使用者體驗,高吞吐量則可以高效率地利用CPU時間,盡快完成程式的運算任務。其主要适合在背景運算而不需要太多互動的任務。
Parallel Scavenge提供兩個參數精确控制吞吐量,-XX:MaxGCPauseMillis控制最大垃圾收集停頓時間和-XX:GCTimeRatio設定吞吐量大小 。MaxGCPauseMillis允許的值是一個大于零的毫秒數,收集器将盡力保證記憶體回收花費的時間不超過設定值。GC停頓時間縮小是以犧牲吞吐量和新生代空間來換取的,也就是要使停頓時間更短,需要使新生代的空間減小,這樣垃圾回收的頻率會增加,吞吐量也降下來了。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,使用标記-整理算法的單線程收集器。對于Client模式,主要在于給Client模式下的虛拟機使用;而對于Server模式,在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,或作為CMS收集器的後備預案,在并發收集發生Concurrent Mode Failure時使用。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用“标記-整理”算法的多線程收集器。在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge+Parallel Old收集器。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以擷取最短回收停頓時間為目标的基于“标記—清除”算法的收集器。主要的Java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間短,以給使用者較好體驗。
其運作整個過程分為4個步驟:
- 初始标記(CMS initial mark)。初始标記僅僅隻是标記一下GC Roots能直接關聯到的對象,速度很快,需要“Stop The World”。
- 并發标記(CMS concurrent mark)。并發标記階段就是進行GC Roots Tracing的過程。
- 重新标記(CMS remark)。重新标記階段是為了修正并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄,這個階段的停頓時間一般會比初始标記階段稍長一些,但遠比并發标記的時間短,仍然需要“Stop The World”。
- 并發清除(CMS concurrent sweep)。并發清除階段會清除對象。
由于整個過程中耗時最長的并發标記和并發清除過程收集器線程都可以與使用者線程一起工作,是以,從總體上來說,CMS收集器的記憶體回收過程是與使用者線程一起并發執行的。
優點:
- CMS是一款優秀的收集器,它的主要優點在名字上已經展現出來了:并發收集、低停頓。
缺點:
-
CMS收集器對CPU資源非常敏感。其實,面向并發設計的程式都對CPU資源比較敏感。在并發階段,它雖然不會導緻使用者線程停頓,但是會因為占用了一部分線程(或者說CPU資源)而導緻應用程式變慢,總吞吐量會降低。
CMS預設啟動的回收線程數是(CPU數量+3)/ 4,也就是當CPU在4個以上時,并發回收時垃圾收集線程不少于25%的CPU資源,并且随着CPU數量的增加而下降。但是當CPU不足4個(譬如2個)時,CMS對使用者程式的影響就可能變得很大。
- CMS收集器無法處理浮動垃圾。可能出現“Concurrent Mode Failure”失敗而導緻另一次Full GC的産生。由于CMS并發清理階段使用者線程還在運作着,伴随程式運作自然就還會有新的垃圾不斷産生,這一部分垃圾出現在标記過程之後,CMS無法在當次收集中處理掉它們,隻好留待下一次GC時再清理掉。這一部分垃圾就稱為“浮動垃圾”。也是由于在垃圾收集階段使用者線程還需要運作,那也就還需要預留有足夠的記憶體空間給使用者線程使用,是以CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供并發收集時的程式運作使用。要是CMS運作期間預留的記憶體無法滿足程式需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛拟機将啟動後備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。
- CMS收集器會産生大量空間碎片。CMS是一款基于“标記—清除”算法實作的收集器,這意味着收集結束時會有大量空間碎片産生。空間碎片過多時,将會給大對象配置設定帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來配置設定目前對象,不得不提前觸發一次Full GC。
G1收集器
G1(Garbage-First)是一款面向服務端應用的垃圾收集器。
G1具備如下特點。
-
并發
G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過并發的方式讓Java程式繼續執行。
-
分代收集
與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠采用不同的方式去處理新建立的對象和已經存活了一段時間、熬過多次GC的舊對象以擷取更好的收集效果。
-
空間整合
與CMS的“标記—清理”算法不同,G1從整體來看是基于“标記—整理”算法實作的收集器,從局部(兩個Region之間)上來看是基于“複制”算法實作的,但無論如何,這兩種算法都意味着G1運作期間不會産生記憶體空間碎片,收集後能提供規整的可用記憶體。這種特性有利于程式長時間運作,配置設定大對象時不會因為無法找到連續記憶體空間而提前觸發下一次GC。
-
可預測的停頓
這是G1相對于CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明确指定在一個長度為M毫秒的時間片段内,消耗在垃圾收集上的時間不得超過N毫秒。
在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的記憶體布局就與其他收集器有很大差别,它将整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是實體隔離的了,它們都是一部分Region(不需要連續)的集合。
G1收集器之是以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在背景維護一個優先清單,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分記憶體空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間内可以擷取盡可能高的收集效率。
G1收集器的運作大緻可劃分為以下幾個步驟:
-
初始标記(Initial Marking)
初始标記階段僅僅隻是标記一下GC Roots能直接關聯到的對象,并且修改TAMS(Next Top at Mark Start)的值,讓下一階段使用者程式并發運作時,能在正确可用的Region中建立新對象,這階段需要停頓線程,但耗時很短。
-
并發标記(Concurrent Marking)
并發标記階段是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與使用者程式并發執行。
-
最終标記(Final Marking)
最終标記階段是為了修正在并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分标記記錄,虛拟機将這段時間對象變化記錄線上程Remembered Set Logs裡面,最終标記階段需要把Remembered Set Logs的資料合并到Remembered Set中,這階段需要停頓線程,但是可并行執行。
-
篩選回收(Live Data Counting and Evacuation)
篩選回收階段首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃,這個階段其實也可以做到與使用者程式一起并發執行,但是因為隻回收一部分Region,時間是使用者可控制的,而且停頓使用者線程将大幅提高收集效率。
4、記憶體配置設定與回收
- 對象優先配置設定到Eden。HotSpot JVM把年輕代分為了三部分:1個Eden區和2個Survivor區(分别叫from和to),預設比例為8:1。一般情況下,新建立的對象都會被配置設定到Eden區(一些大對象特殊處理),這些對象經過第一次Minor GC後,如果仍然存活,将會被移到Survivor區。對象在Survivor區中每熬過一次Minor GC,年齡就會增加1歲,當它的年齡增加到一定程度時,就會被移動到年老代中。由于年輕代中的對象基本都是朝生夕死的(80%以上),是以在年輕代的垃圾回收算法使用的是複制算法,複制算法的基本思想就是将記憶體分為兩塊,每次隻用其中一塊,當這一塊記憶體用完,就将還活着的對象複制到另外一塊上面。複制算法不會産生記憶體碎片。在GC開始的時候,對象隻會存在于Eden區和名為“From”的Survivor區,Survivor區“To”是空的。緊接着進行GC,Eden區中所有存活的對象都會被複制到“To”,而在“From”區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到一定值(年齡門檻值,可以通過-XX:MaxTenuringThreshold來設定)的對象會被移動到年老代中,沒有達到門檻值的對象會被複制到“To”區域。經過這次GC後,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到“To”區被填滿,“To”區被填滿之後,會将所有對象移動到年老代中。
- 大對象直接配置設定在年老代。大對象簡單的了解就是可能會占用堆空間很大的對象執行個體,比如說一個很大的字元串數組,因為虛拟機預設是将對象執行個體配置設定在年輕代的Eden區。年輕代垃圾收集器主要采用複制算法,如果大對象存活周期比較長意味着其會被多次複制,最重要的是當年輕代沒有足夠的空間來存放大對象時,會引發年輕代垃圾回收。一旦大對象數量比較多,年輕代則會頻繁垃圾回收。為了防止這種情況的發生,通過-XX:PretenureSizeThreshold參數來指定直接配置設定到老年代的執行個體大小。
- 長期存活的對象進入年老代。當對象在Eden區中經過一次年輕代的回收成功轉入Survivor之後,它的年齡就為1,後續每發生一次年輕代的垃圾回收,隻要該對象還存活,則它的年齡就加1,直到其年齡達到設定的最大值(預設為15),然後将被移到年老代。為了可以靈活的控制這個年齡,通過 -XXMaxTenuringThreshold參數來控制。
參考文獻:
周志明. 深入了解 Java 虛拟機: JVM 進階特性與最佳實踐[J]. 2010.