1. 對象是否存活
垃圾回收器在對堆進行回收錢,第一件事情就是要確定對象是否存活
1.1 引用計數法
算法:給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器都為0的對象就是不可能再被使用的。但JAVA沒有選用引用計數算法來管理內存,主要的原因是很難解決對象之間的相互循環引用的問題
1.2 根搜索算法
算法:通過一系列的名為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots 沒有任何引用鏈相連(即GC Roots到這個對象不可達),則證明此對象是不可用的
在java中,可作為GC Roots的對象包括:虛擬機棧(棧幀中的本地變量表)中引用的對象;方法區中的類靜態屬性引用的對象;方法區中的常量引用的對象;本地方法棧中的JNI(nateive方法)的引用的對象
1.3 再談引用
JDK1.2后引用分為強引用、軟引用、弱引用、虛引用
- 強引用,類似
Object obj = new Object()
這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象 - 軟引用(soft reference),描述一些還有用,但非必須的對象。對于軟引用關聯著的對象,在系統將要發生內存溢出之前,將會把這些對象列進回收范圍之中并進行第二次回收
- 弱引用(weak reference),也是用來描述非必須對象,但是它的強度比軟引用更弱一下,被弱引用關聯的對象只能生存到下一次垃圾回收發生之前。當垃圾回收器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象
- 虛引用,最弱的引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是希望能在這個對象被收集器回收時收到一個系統通知
1.4 生存還是死亡
即使算法已經判定這個對象“非死不可”,但是他們也只是出于死緩階段,要真正判定他死亡,至少要經歷兩次標記過程;如果對象在第一個進行可達性分析后發現沒有與GC ROOTS相連接的引用鏈,那么它將會被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。如果這個對象沒有覆蓋finalize()方法,或者finalize已經被調用過,那么這個對象才會被正在的執行死刑。
首先第一次這個對象被判定為有必要執行finalize()方法,那么這個對象會被放置在一個叫做F-Queue的隊列之后,之后會被一個低優先級的小線程執行。finalize()方法是對象逃脫死亡命運的最后一次機會,稍后GC將會對F-queue重的對象進行第二次標記,如果對象要在finalize()中拯救自己需要重新與引用鏈上的任何一個對象建立關聯,如果在第二次標記前,這個對象還沒有移除即將回收的集合,那么它基本上就要被回收了。
2. 垃圾收集算法
2.1 標記-清除算法(Mark-Sweep)
算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后統一回收掉所有標記的對象
缺點:一個是效率問題,標記和清除過程的效率都不高;另一個是空間問題,內存碎片化嚴重,后續可能發生大對象不能找到可利用空間
2.2 復制算法(Copying)
為了解決Mark-Sweep算法內存碎片化的缺陷而被提出的算法。按內存容量將內存劃分為等大小的兩塊。每次只使用其中一塊,當這一塊內存滿后將尚存活的對象復制到另一塊上去,把已使用的內存清掉
缺點:內存空間的使用做出了高昂的代價,因為能夠使用的內存縮減到原來的一半。很顯然,Copying算法的效率跟存活對象的數目多少有很大的關系,如果存活對象很多,那么Copying算法的效率將會大大降低。
2.3 標記 - 整理算法(Mark-Compact)
標記階段和Mark-Sweep算法相同,標記后不是清理對象,而是將存活對象移向內存的一端。然后清除端邊界外的對象
2.4 分代收集算法(Generational Collection)
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據對象存活的生命周期將內存劃分為若干個不同的區域。一般情況下將堆區劃分為老年代(Tenured Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那么就可以根據不同代的特點采取最適合的收集算法。
目前大部分垃圾收集器對于新生代都采取復制算法,因為新生代中每次垃圾回收都要回收大部分對象,也就是說需要復制的操作次數較少,但是實際中并不是按照1:1的比例來劃分新生代的空間的,一般來說是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將Eden和Survivor中還存活的對象復制到另一塊Survivor空間中,然后清理掉Eden和剛才使用過的Survivor空間。
而由于老年代的特點是每次回收都只回收少量對象,一般使用的是標記-整理算法(壓縮法)
3. 垃圾回收器
[圖片上傳失敗...(image-8713c1-1583046941026)]
3.1 Serial收集器------復制算法
特點:
針對新生代;采用復制算法;單線程收集;進行垃圾收集時,必須暫停所有工作線程,直到完成;
應用場景:
依然是HotSpot在Client模式下默認的新生代收集器;
也有優于其他收集器的地方:簡單高效(與其他收集器的單線程相比);
對于限定單個CPU的環境來說,Serial收集器沒有線程交互(切換)開銷,可以獲得最高的單線程收集效率;在用
戶的桌面應用場景中,可用內存一般不大(幾十M至一兩百M),可以在較短時間內完成垃圾收集(幾十MS至一
百多MS),只要不頻繁發生,這是可以接受的
3.2 ParNew收集器------復制算法
特點:
除了多線程外,其余的行為、特點和Serial收集器一樣;
如Serial收集器可用控制參數、收集算法、Stop The World、內存分配規則、回收策略等
應用場景:
在Server模式下,ParNew收集器是一個非常重要的收集器,因為除Serial外,目前只有它能與CMS收集器配合工
作(因為Parallel Scavenge(以及G1)都沒有使用傳統的GC收集器代碼框架,而另外獨立實現,而其余幾種收集
器則共用了部分的框架代碼); 但在單個CPU環境中,不會比Serail收集器有更好的效果,因為存在線程交互開銷
3.3 Parallel Scavenge收集器
Parallel Scavenge垃圾收集器因為與吞吐量關系密切,也稱為吞吐量收集器(Throughput Collector),通過設置參數進行控制吞吐量。參數分別是控制最大垃圾收集停頓時間的 -XX:MaxGCPauseMillis參數和直接設置吞吐量大小的-XX:GCTimeRatio參數。
MaxGCPauseMillis參數是大于0的毫秒數。收集器保證內存回收耗費的時間不超過設定值,不過GC停頓個時間是以犧牲吞吐量和新生代空間來換取的。
GCTimeRatio參數的值是大于0小于100的整數,也就是垃圾時間站總時間的比率,比如參數設為19,最大GC時間戰總時間的5%(1/(1+19))。默認值是99,就是允許最大時間的1%作為垃圾收集時間
還有一個參數-XX:+UseAdaptiveSizePolicy,這是開關參數,打開以后不需要指定新生代大小(-Xnn)、Eden與Survivor區的比例 (-XX:SurvivorRatio) 、晉升到老年代的年齡(-XX:PretenureSizeThreshold)等細節參數,虛擬機會根據實際情況動態調節這些參數以提供最合理的停頓時間以及最大的吞吐量。
特點:
新生代收集器; 采用復制算法;多線程收集;
主要特點是:它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓
時間;而Parallel Scavenge收集器的目標則是達一個可控制的吞吐量(Throughput);有自適應調節策略;
應用場景:
高吞吐量為目標,即減少垃圾收集時間,讓用戶代碼獲得更長的運行時間;當應用程序運行在具有多個CPU上,對
暫停時間沒有特別高的要求 時,即程序主要在后臺進行計算,而不需要與用戶進行太多交互;例如,那些執行批
量處理、訂單處理、工資支付、科學計算的應用程序;
3.4 Serial Old收集器
Serial Old是 Serial收集器的老年代版本
特點:
針對老年代;采用"標記-整理"算法(還有壓縮,Mark-Sweep-Compact);單線程收集;
應用場景:
主要用于Client模式;
而在Server模式有兩大用途:(A)、在JDK1.5及之前,與Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);(B)、作為CMS收集器的后備預案,在并發收集發生Concurrent Mode Failure時使用(后面詳解)
3.5 Parallel Old收集器
Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本;JDK1.6中才開始提供;
特點:
針對老年代; 采用"標記-整理"算法; 多線程收集;
應用場景:
JDK1.6及之后用來代替老年代的Serial Old收集器;
特別是在Server模式,多CPU的情況下;
這樣在注重吞吐量以及CPU資源敏感的場景,就有了Parallel Scavenge加Parallel Old收集器的"給力"應用組合
3.6 CMS收集器
CMS(Concurrent Mark Sweep,CMS)收集器是一種以獲取最短回收停頓時間為目標的收集器,也稱為并發低
停頓收集器(Concurrent Low Pause Collector)或低延遲(low-latency)垃圾收集器
特點:
針對老年代;基于"標記-清除"算法(不進行壓縮操作,產生內存碎片);
以獲取最短回收停頓時間為目標;
并發收集、低停頓; 需要更多的內存(看后面的缺點);
是HotSpot在JDK1.5推出的第一款真正意義上的并發(Concurrent)收集器;
第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作;
應用場景:
與用戶交互較多的場景;希望系統停頓時間最短,注重服務的響應速度;以給用戶帶來較好的體驗; 如常見WEB、B/S系統的服務器上的應用;
運作過程:
(A)、初始標記(CMS initial mark)----- 僅標記一下GC Roots能直接關聯到的對象; 速度很快; 但需要"Stop The World";
(B)、并發標記(CMS concurrent mark)------ 進行GC Roots Tracing的過程;剛才產生的集合中標記出存活對象;應用程序也在運行;并不能保證可以標記出所有的存活對象;
(C)、重新標記(CMS remark)------為了修正并發標記期間因用戶程序繼續運作而導致標記變動的那一部分對象的標記記錄;需要"Stop The World",且停頓時間比初始標記稍長,但遠比并發標記短; 采用多線程并行執行來提升效率;
(D)、并發清除(CMS concurrent sweep)-----回收所有的垃圾對象;整個過程中耗時最長的并發標記和并發清除都可以與用戶線程一起工作; 所以總體上說,CMS收集器的內存回收過程與用戶線程一起并發執行;
缺點:
(A)CMS收集器對CPU資源非常敏感。并發收集雖然不會暫停用戶線程,但因為占用一部分CPU資源,還是會導致應用程序變慢,總吞吐量降低;CMS的默認收集線程數量是=(CPU數量+3)/4,當CPU數量多于4個,收集線程占用的CPU資源最多不超過25%,但不足4個時,影響就很大
(B)CMS收集器無法處理浮動垃圾,可能出現"Concurrent Mode Failure"失敗導致另一次“Full GC”的產生。在并發清除時,用戶線程新產生的垃圾,稱為浮動垃圾; 由于垃圾收集階段用戶線程還需要運行,這使得并發清除時需要預留一定的內存空間(默認情況下,老年代空間使用了68%的空間后會被激活,若老年代增長不是太快,可以調高參數 -XX:CMSInititatingOccupancyFraction來提高觸發百分比),不能 像其他收集器在老年代幾乎填 滿再進行收 集;如果CMS預留內存空間無法滿足程序需要,就會出現一次"Concurrent Mode Failure"失敗;這時JVM啟用后備預案:臨時啟用Serail Old收集 器
(C)垃圾收集結束后產生大量空間碎片,往往會出現老年代還有很大的空間剩余,但無法找到足夠大的連續空間分配當前對象,而不得不觸發Full GC,所以CMS收集器提供了 -XX:+UseCMSCompactAtFullCollection開關參數,用于在執行完Full GC后提供一個碎片整理過程。
3.7 G1收集器
G1是一款面向服務端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是(在比較長期的)未來可以替換掉JDK 1.5中發布的CMS收集器。與其他GC收集器相比,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毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特征了。
在G1之前的其他收集器進行收集的范圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存布局就與其他收集器很很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的,它們都是一部分Region(不需要連續)的集合。
G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內可以獲取盡可能高的收集效率。
G1收集器的運作大致可劃分為以下幾個步驟:
- 初始標記(Initial Marking)
- 并發標記(Concurrent Marking)
- 最終標記(Final Marking)
- 篩選回收(Live Data Counting and Evacuation)
G1收集器的運作步驟中并發和需要停頓的階段:
3.8 垃圾收集器常用參數
UseSerialGC | 虛擬機運行在Client 模式下的默認值,打開此開關后,使用Serial +Serial Old 的收集器組合進行內存回收 |
---|---|
UseParNewGC | 打開此開關后,使用ParNew + Serial Old 的收集器組合進行內存回收 |
UseConcMarkSweepGC | 打開此開關后,使用ParNew + CMS + Serial Old 的收集器組合進行內存回收。Serial Old 收集器將作為CMS 收集器出現Concurrent Mode Failure失敗后的后備收集器使用 |
UseParallelGC | 虛擬機運行在Server 模式下的默認值,打開此開關后,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器組合進行內存回收 |
UseParallelOldGC | 打開此開關后,使用Parallel Scavenge + Parallel Old 的收集器組合進行內存回收,新生代中Eden 區域與Survivor 區域的容量比值, 默認為8, 代表Eden :Survivor=8∶1 |
PretenureSizeThreshold | 直接晉升到老年代的對象大小,設置這個參數后,大于這個參數的對象將直接在老年代分配 |
MaxTenuringThreshold | 晉升到老年代的對象年齡。每個對象在堅持過一次Minor GC 之后,年齡就加1,當超過這個參數值時就進入老年代 |
UseAdaptiveSizePolicy | 動態調整Java 堆中各個區域的大小以及進入老年代的年齡 |
HandlePromotionFailure | 是否允許分配擔保失敗,即老年代的剩余空間不足以應付新生代的整個Eden 和Survivor 區的所有對象都存活的極端情況 |
ParallelGCThreads | 設置并行GC 時進行內存回收的線程數 |
GCTimeRatio | GC 時間占總時間的比率,默認值為99,即允許1% 的GC 時間。僅在使用Parallel Scavenge 收集器時生效 |
MaxGCPauseMillis | 設置GC 的最大停頓時間。僅在使用Parallel Scavenge 收集器時生效 |
CMSInitiatingOccupancyFraction | 設置CMS 收集器在老年代空間被使用多少后觸發垃圾收集。默認值為68%,僅在使用CMS 收集器時生效 |
UseCMSCompactAtFullCollection | 設置CMS 收集器在完成垃圾收集后是否要進行一次內存碎片整理。僅在使用CMS 收集器時生效 |
CMSFullGCsBeforeCompaction | 設置CMS 收集器在進行若干次垃圾收集后再啟動一次內存碎片整理。僅在使用CMS 收集器時生效 |
4. 內存分配與回收策略
-
對象優先分配在Eden區
大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠的空間進行分配時,虛擬機將發起一次Minor GC
注意:
新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為java對象大多都具備朝生夕滅的特性,所以Minor GC 非常頻繁
老年代GC(Major GC / Full GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(但非絕對,在 ParallelScavenge收集器的收集策略里,就有直接進行Major GC的策略選擇過程),Major GC的速度一般會比Minor GC慢10倍以上
-
大對象直接進入老年代
所謂大對象,是指需要大量連續的內存空間的JAVA對象,比如很長的字符串或數組。虛擬機提供了一個 -XX:PretenureSizeThreshold 參數,令大于這個大小的對象直接進入老年代。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的內存拷貝
注意:
PretenureSizeThreshold 參數只對Serial和ParNew兩個收集器生效,ParallelScavenge 不認識該參數,ParallelScavenge 一般不需要設置,如果遇到必須使用此參數的場合,可以考慮使用ParNew和CMS的收集器組合
-
長期存活的對象將進入老年代
虛擬機采用了分代收集的思想,為了做到這點,虛擬機給每個對象定義了對象年齡(Age)計數器。如果對象在Eden出生并經過第一次Minor GC后仍然存活,并且能被Survivor容納的話,將被移動到Survivor空間中,并且對象年齡設為1,當它的年齡增加到一定程度(默認為15歲),就將會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數-XX:MaxTenuringThreshold設置。
-
動態對象年齡判定
為了能更好的的適應不同程序的內存狀況,虛擬機并不是永遠得要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。
-
空間分配擔保
在發生Minor GC時,虛擬機就會檢測之前每次晉升到老年代的平均大小是否大于老年代的剩余空間,如果大于,則改為直接進行一次Full GC.如果小于,則查看HandlePromotionFailure設置是否允許擔保失敗;如果允許,那只會進行Minor GC:如果不允許,則也要改為進行一次Full GC.
新生代使用復制收集算法,但為了內存的利用率,只使用其中一個Survivor空間作為輪換備份,因此當出現大量對象在Minor GC 后仍然存活的情況,就需要老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代。如果老年代剩余空間不足就需要進行Full GC。
取平均值進行比較其實仍然是一種動態概率的手段,也就是說,如果某次Minor GC存活后的對象突增,遠遠高于平均值的話,依然會導致擔保失敗(Handle Promotion Failure)。 如果出現了HandlePromotionFailure失敗,那就只好在失敗后重新發起一次Full GG。雖然擔保失敗時繞的圈子是最大的,但大部分情況下還是會將HandlePromotionFailure開關打開,避免Full GC過于頻繁