看完這篇,我再也不怕面試官問垃圾收集了
說在前面:本文的篇幅較長,看本文的時候最好先去上個廁所,先準備好一杯枸杞茶,慢慢品,本文將會講解三種垃圾收集算法:標記-清除、複製、標記-整理算法,以及各種成熟度較高的垃圾收集器:Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old、CMS以及G1(Garbage First)
在討論垃圾收集算法之前,需要先了解針對不同區域進行收集的名詞:
Minor GC
(新生代收集)、Major GC
(老年代收集)、Full GC
(整個Java堆和方法區的收集)、Mixed GC
(新生代收集和部分老年代的收集,目前只有G1收集器有這種行爲)
垃圾收集算法
前一篇文章介紹了可達性分析算法後,瞭解了虛擬機會利用可達性分析來識別哪些對象是垃圾,可以回收,那麼是如何進行回收的呢?下面就會介紹這三種垃圾收集算法
標記-清除算法(Mark-Sweep)
可以看到,在對象可以被回收的區域上,JVM會直接把這些垃圾對象佔用的內存直接清除掉。
這個算法的優點很明顯:簡單
這個算法也有許多缺點:
執行效率不穩定,如果Java堆中包含大量對象,某一次回收時無用的對象非常多,這時候會花費很多時間進行內存的清除。
有可能造成內存空間碎片,上圖只是一個理想的刪除過程,正好沒有內存碎片產生,而實際上在內存中待清除的內存有可能不是連續的,導致會產生許多內存碎片,如果某個大對象無法找到一塊連續的內存進行存放時,會誤以爲堆內存不足,提前觸發
Full GC
所以爲了解決內存碎片問題,科學家們研製出了一種新的算法:標記-複製算法
標記-複製算法(Mark-Copying)
由上面的動圖可以看出,標記-複製算法將原本的堆內存劃分了兩個區域,採用了“半區複製”算法,將一半的內存省出來,當發生垃圾收集行爲時,將存活的對象複製到另外一半保留區域中連續存放。
標記-複製算法的優點是解決了大對象分配內存的內存碎片問題
,也解決了標記-清除算法中大量垃圾對象導致的清除效率問題
。
缺點也非常的明顯,那就是可分配的內存空間少了整整一半,而且如果某次存活的對象較多,甚至全部存活,那麼複製的效率將會非常低。
標記-整理算法(Mark-Compact)
爲了提升內存的利用率,科學家提出了標記-整理算法,該算法的起始過程和標記-清除
算法相同,先標記處待回收對象的內存區域,但是在清除時不是對所有可回收對象清除,而是讓所有存活對象往內存空間的一邊移動,把存活對象邊界外的內存直接清空掉。
標記-整理算法提高了內存的利用率、解決了大對象分配時的內存碎片問題,看似完美的垃圾收集算法,也有它的弊端
在移動存活對象的過程中,需要全程暫停用戶程序的執行,被設計者稱爲“Stop The World”。
分代收集
新生代垃圾收集及內存分配
分代收集算法本質上標記-複製算法,它把堆內存中較大的一塊區域作爲新生代區域,新生代區域中分爲一個Eden區域和兩個Survivor區域,Eden和Survivor的比例默認是8:1,因爲在Eden區域,絕大數對象都熬不過第一輪GC(98%),所以每個Survivor區域只需要10%的空間就足矣了,每一次觸發Minor GC
時,就會將Eden區和Survivor區存活的對象複製到另外一個Survivor區域中,然後清除掉被回收的對象,每次都依據這樣的步驟進行垃圾收集。
不知道你有沒有注意到每個對象有一個數字的標記,這個標記是對象的年齡,當對象到了15歲以後(默認情況)就會被晉升爲老年代
晉升老年代
如圖所示,當對象在Survivor區存活了15次以後,就會晉升爲老年代對象。
還有以下情況會晉升爲老年代對象:
大對象。當對象所佔連續內存非常大時,不會分配在Eden區,如果分配在Eden區,那麼對象存活時產生的複製操作將導致效率大大降低。
如果在Survivor區,相同年齡的對象總大小大於Survivor區空間的一半時,也會將這些年齡相同的對象直接晉升到老年代,原因也是防止對象的複製操作導致的效率問題。
空間分配擔保
在對象無法分配到Eden區時,會觸發一次Minor GC
,JVM會首先檢查老年代最大的可用連續空間是否大於新生代所有對象的總和,如果大於,那麼這次Minor GC
是安全的,如果不大於的話,JVM就需要判斷HandlePromotionFailure
是否允許空間分配擔保。
如果允許擔保,則證明老年代的連續可用內存空間大於歷次晉升到老年代對象的平均大小,此時觸發一次Minor GC
,如果小於,那麼證明老年代並沒有把握放得下Survivor區有可能晉升的對象,此時發生一次Full GC
。
Stop The World
發生GC
(MinorGC或者FullGC)時,都會將用戶線程停頓並進行垃圾收集,在Minor GC
中,STW
的時間較短,只涉及Eden
和survivor
區域的對象清除和複製操作,而Full GC
則是對整個堆內存進行垃圾收集,對象的掃描、標記和清除操作工作量大大提高,所以Full GC
會導致用戶線程停頓較長時間,如果頻繁地發生Full GC
,那麼用戶線程將無法正常執行。
或者通俗的理解:
你給你媽媽打掃房間時,你是希望她坐在一旁靜靜等你掃完地再繼續活動,還是想你一邊掃地,她一邊丟垃圾呢?
Safe Points
既然要用戶線程停頓下來,那麼要在什麼地方停頓呢?JVM採用主動式中斷方式告訴Java線程需要停頓了,JVM在特定的位置設置了這些安全點(Safe point),讓線程可以在這些安全點主動掛起。
方法調用、循環跳轉、異常跳轉
這些安全點的特徵是令程序有可能進行某一段長時間執行的特徵。
在這些安全點上存有對象引用信息的OopMap
數據結構,這種數據結構你可以理解爲HashMap
這種數據結構,它內部存儲了什麼位置上存儲了對象引用信息,這些信息在類加載完成時就確定下來了。所以JVM在垃圾收集時不需要從一個個方法的GC Roots
去掃描,從OopMap
中可以快速準確地定位到這些GC Roots
。
如果用戶線程本身處於停頓狀態,例如阻塞(Blocked)、睡覺(Sleep),那麼此時觸發GC時,用戶線程無法響應JVM的中斷(我聽不見你喊我,我睡着了~),用戶線程無法主動地跑去安全點中斷掛起,此時該怎麼辦呢?
對於這種情況,必須引入Safe Region來解決。
Safe Region
安全區域是指,用戶線程進入某一段代碼區域中時,引用關係不會發生變化,那麼在這片代碼區域的任何地方開始GC都不會受到影響。實現的方式是,用戶線程進入安全區域時會標識自己已經進入安全區域,在JVM發起GC時不必理會那些已經標識爲進入安全區域的線程,當用戶線程需要離開安全區域時,會主動檢查JVM是否已經完成了需要停頓線程的工作,如果已完成則可以離開,如果未完成則必須一直等待,直到JVM發送可以離開安全區域的信號爲止。
垃圾收集器
垃圾收集器分爲新生代收集器與老年代收集器,各種不同的收集器之間如果符合標準則可以相互搭配使用
新生代收集器
Serial收集器
Serial收集器是一款單線程的垃圾收集器,“單線程”的意義不僅僅是指它只能用一條線程或佔用一個處理器去完成垃圾收集操作,更重要的是它進行垃圾收集時,**需要暫停其它所有線程,直到垃圾收集結束。**它身爲最古老的一款垃圾收集器,在當今依舊廣泛受用,它有以下優點:
對於內存受限的環境,它是所有收集器裏額外內存消耗最小的
沒有線程交互的開銷,Serial收集器可以很好地專注於收集垃圾,把用戶線程都停掉
在用戶桌面的應用場景和近年來流行的部分微服務應用中,分配給虛擬機管理的內存一般不會特別大,收集幾十兆、一兩百兆的新生代(桌面應用的新生代甚至少於這個容量),垃圾收集完全可以控制在十幾、幾十毫秒,最多一百毫秒,這點停頓時間對用戶來說是十分友好的。
ParNew收集器
ParNew是一款並行新生代收集器,parNew收集器除了支持多線程並行收集以外,其餘的行爲與Serial收集器完全一致,包括收集算法、STW(Stop The World)、對象分配規則、回收策略等等。
parNew是不少運行在服務器端模式下的HotSpot虛擬機中首選的新生代收集器,其中一個與性能、功能無關但很重要的原因是:除了Serial收集器,只有ParNew能夠與CMS收集器配合工作。
CMS收集器與Parallel Scavenge收集器不能配合工作的一個原因是:Parallel Scavenge收集器內部並沒有按照分代收集的框架進行設計垃圾回收,在之後的G1收集器也同樣沒有按照分代回收的框架設計。
Parallel Scavenge收集器
Parallel Scavenge收集器同樣是基於標記-複製算法實現的收集器,也是能夠並行收集的一款新生代收集器,那它與ParNew收集器的差別在哪裏呢?
Parallel Scavenge收集器的特別之處在於它與其它收集器的關注點不一樣,其它垃圾收集器關注如何最大限度地減少STW的時間,而Parrel Scavenge關注的是如何達到一個可控制的吞吐量(Throughput),由於與吞吐量關係密切,所以也被稱作“吞吐量優先收集器”。
Parallel Scavenge收集器可以實現自適應策略,這是另外一個與ParNew收集器的差別,可以通過指定-XX:UseAdaptiveSizePolicy
參數,虛擬機就會根據系統當前的運行情況收集監控信息,並且自動調整系統的相關JVM參數以提供最高的吞吐量和最合適的停頓時間。
老年代收集器
Serial Old收集器
使用標記-整理
算法,是一個單線程收集器,它有另外兩個用途:
它作爲CMS收集器發生失敗後的後備預案,在CMS收集器併發收集發生Concurrent Mode Failure使用
作爲Parallel Scavenge的老年代收集器
這個時候就有疑惑了,Parallel Scavenge
收集器不是沒有按分代收集框架實現嗎,爲什麼能夠搭配Serial Old
收集器使用
《深入理解Java虛擬機》:Parallel Scavenge
收集器架構中含有PS MarkSweep
收集器進行老年代收集,並非直接調用Serial Old
收集器,但是PS MarkSweep
與Serial Old
的實現幾乎是一樣的,所以官方很多地方用Serial Old
代替它進行講解。
Parallel Old收集器
Parallel Old
是Parallel Scavenge
的老年代版本,支持多線程併發收集,基於標記-整理
算法設計,自從JDK6以後,Parallel Old
和Parallel Scavenge
成爲了最好的搭檔,在注重吞吐量或者處理器資源比較緊缺的情況下,都可以採用這個組合。
CMS收集器
CMS收集器是基於獲取最短回收停頓時間爲目標的收集器,CMS收集器適合追求服務的響應速度的應用,例如基於瀏覽器的B/S系統的服務端上。
CMS是基於標記-清除
算法設計的,它支持用戶線程與GC線程併發執行,如下圖所示
運作過程分爲4個階段:
初始標記、併發標記、重新標記、併發清除
初始標記的過程就是掃描GC Roots;
併發標記是掃描GC Roots鏈上所有的對象,此時會出現一些對象標記的變動,因爲用戶線程仍然在執行;
重新標記的過程是修正併發標記期間產生引用變動的那一部分對象的標記記錄
併發清除是刪除掉標記階段判斷已經死亡的對象,由於不用移動存活對象,此時也是可以併發執行的。
CMS收集器有三個缺點:
-
對處理器資源特別敏感,由於是併發執行,所以CMS收集器工作時會佔用一部分CPU資源而導致用戶程序變慢,降低總吞吐量,建議具有四核處理器以上的服務器使用CMS收集器
-
CMS無法清除浮動垃圾,有可能出現
Concurrent Mode Failure
失敗而導致另一次STW
的Full GC
產生。由於併發清理過程中用戶線程與GC線程併發執行,就一定會產生新的垃圾對象,但是無法在本次GC中處理這些垃圾對象,不得不推遲到下一次GC中處理,這些垃圾對象就稱爲“浮動垃圾”,到JDK6的時候,CMS收集器啓動閾值達到92%
,也就是老年代佔了92%
的空間後會觸發GC,但是如果剩餘的內存8%
不足以分配新對象時,就會發生“併發失敗”,進而凍結用戶線程,使用Serial Old
收集器進行一次Full GC
,所以觸發CMS收集器的閾值還是根據實際場景來設置,參數爲-XX:CMSInitiatingOccu-pancyFraction
。 -
基於
標記-清除
算法會導致內存碎片不斷增多,在分配大對象時有可能會提前觸發一次Full GC
。所以CMS提供兩個參數可供開發者指定在每次Full GC
時進行碎片整理,由於碎片整理需要移動對象,所以是無法併發收集的,-XX:+UseCMSCompactAtFullCollection
(JDK9開始廢棄),-XX:CMSFullGCsBeforeCompaction
(JDK9開始廢棄,默認值是0,每次Full GC都進行碎片整理)。
Garbage First收集器
這是一個在垃圾收集器技術發展歷史上的里程碑式的成果,它取代了Parallel Scavenge + Parallel Old
的組合,並取代了CMS
,作爲它們的繼承者和替代者,G1到底有什麼魔力呢?
G1是一種“停頓時間模型”收集器,也就是說可以指定在時間片段爲
M
毫秒時,垃圾收集所佔用的時間不會超過N
毫秒。G1顛覆了之前的所有垃圾收集器的垃圾收集行爲:要麼新生代收集(Minor GC)、要麼老年代收集(Major GC)、要麼整堆收集(Full GC),而G1可以面向堆內存任何部分組成回收集(Collection Set , CSet),衡量標準不再是它屬於哪個分代,而是哪塊內存存放的垃圾數量較多,這就是G1所特有的Mixed GC模式。
可以看到上圖中每一個方塊就是一個Region,每個Region可以存放1~32MB大小的對象,使用參數-XX:G1HeapRegionSize
指定,Region中可以存放Eden
/Survivor
/Humongous
/Old
,G1中新生代和老年代並不是連續存放的,而是一個動態的集合。
注意在G1中專門用Region
存放一個Humongous
大對象,當對象容量大於Region的一半時就認爲它是大對象,按照“大對象優先在老年代中分配”,Humongous
也是老年代的一部分對象。
G1收集器將Region
單元看出是最小的內存回收單元,每次發生GC時,G1收集器都會評估各個Region
的價值大小,根據用戶所指定的收集停頓時間來優先處理那些回收價值最大的Region
,這也是Garbage First
的由來。
G1收集器的運作過程可以分爲4個步驟:
初始標記:僅記錄GC Roots對象,需要停頓用戶線程,但時間很短,藉助
Minor GC
同步完成。併發標記:從GC Roots開始遍歷掃描所有的對象進行可達性分析,找出要回收的對象,由於是併發標記,有可能在掃描過程中出現引用變動。
最終標記:將併發標記過程中出現變動的對象引用給糾正過來。
篩選回收:對各個Region的回收價值和成本進行排序,根據用戶所希望的停頓時間來制定回收計劃,選取任意多個Region區域進行回收,把回收的Region區域中的存活對象複製到空的Region區域中,然後清空掉原來的Region區域,涉及對象的移動,所以需要暫停用戶線程,由多條GC線程並行完成。
如何設置G1的停頓時間?
G1的停頓時間不能過短,如果停頓時間過短,那麼每次GC收集都只會回收佔用Region內存區域很小的一部分,而隨着內存不斷分配,堆上的垃圾越來越多,GC的速度低於分配的速度,就會觸發Full GC
,所以,只要我們把停頓時間設置後的效果爲垃圾回收的速度與內存分配的速度大致相同,那麼在理論上來說就永遠不會發生Full GC
,這也是G1被稱爲很牛逼的一個地方。
G1和CMS的比較
G1從整體上看是“標記-整理”算法,從局部(兩個Region之間)上看是“標記-複製”算法,不會產生內存碎片,而CMS基於“標記-清除”算法會產生內存碎片。
G1在垃圾收集時產生的內存佔用和程勳運行時的額外負載都比CMS高
G1支持動態指定停頓時間,而CMS無法指定
兩者都利用了併發標記這個技術
總結
本文主要介紹了各種垃圾收集算法以及當前較爲成熟的垃圾收集器,其中G1和CMS這兩款垃圾收集器是最受關注的,解釋了爲什麼在垃圾收集時需要Stop The World
,本文篇幅較長,能讀到這裏是非常不容易的,之後也要多加複習!
參考資料:
《深入理解Java虛擬機》
https://www.infoq.com/articles/G1-One-Garbage-Collector-To-Rule-Them-All/
https://www.cnblogs.com/yangchunchun/p/7405502.html
https://blog.csdn.net/ladymorgana/article/details/82352100