這篇垃圾回收和收集器,我寫到感動本身了!

看完這篇,我不再怕面試官問垃圾收集了

「說在前面」:本文的篇幅較長,看本文的時候最好先去上個廁所,先準備好一杯枸杞茶,慢慢品,本文將會講解三種垃圾收集算法:「標記-清除、複製、標記-整理算法」,以及各類成熟度較高的垃圾收集器:「Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old、CMS以及G1(Garbage First)」,若是有任何錯誤的地方,歡迎指出!html

在討論垃圾收集算法以前,須要先了解針對不一樣區域進行收集的名詞:Minor GC(新生代收集)、Major GC(老年代收集)、Full GC(整個Java堆和方法區的收集)、Mixed GC(新生代收集和部分老年代的收集,目前只有G1收集器有這種行爲)web

垃圾收集算法

前一篇文章介紹了可達性分析算法後,瞭解了虛擬機會利用可達性分析來識別哪些對象是垃圾,能夠回收,那麼是如何進行回收的呢?下面就會介紹這三種垃圾收集算法面試

標記-清除算法(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的時間較短,只涉及Edensurvivor區域的對象清除和複製操做,而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 Old收集器

Serial收集器是一款單線程的垃圾收集器,「單線程」的「意義」不只僅是指它只能用一條線程或佔用一個處理器去完成垃圾收集操做,更重要的是它進行垃圾收集時,**須要暫停其它全部線程,直到垃圾收集結束。**它身爲最古老的一款垃圾收集器,在當今依舊普遍受用,它有如下優勢:

對於內存受限的環境,它是全部收集器裏額外內存消耗最小的

沒有線程交互的開銷,Serial收集器能夠很好地專一於收集垃圾,把用戶線程都停掉

在用戶桌面的應用場景和近年來流行的部分微服務應用中,分配給虛擬機管理的內存通常不會特別大,收集幾十兆、一兩百兆的新生代(桌面應用的新生代甚至少於這個容量),垃圾收集徹底能夠控制在十幾、幾十毫秒,最多一百毫秒,這點停頓時間對用戶來講是十分友好的。

ParNew收集器

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收集器

Serial/Serial Old收集器

使用標記-整理算法,是一個單線程收集器,它有另外兩個用途:

它做爲CMS收集器發生失敗後的後備預案,在CMS收集器併發收集發生Concurrent Mode Failure使用

做爲Parallel Scavenge的老年代收集器

這個時候就有疑惑了,Parallel Scavenge收集器不是沒有按分代收集框架實現嗎,爲何可以搭配Serial Old收集器使用

《深刻理解Java虛擬機》:Parallel Scavenge收集器架構中含有PS MarkSweep收集器進行老年代收集,並不是直接調用Serial Old收集器,可是PS MarkSweepSerial Old的實現幾乎是同樣的,因此官方不少地方用Serial Old代替它進行講解。

Parallel Old收集器

Parallel Scavenge/Parallel Old

Parallel OldParallel Scavenge的老年代版本,支持多線程併發收集,基於標記-整理算法設計,自從JDK6之後,Parallel OldParallel Scavenge成爲了最好的搭檔,在「注重吞吐量或者處理器資源比較緊缺」的狀況下,均可以採用這個組合。

CMS收集器

CMS收集器是基於獲取「最短回收停頓時間」爲目標的收集器,CMS收集器適合追求服務的響應速度的應用,例如基於瀏覽器的B/S系統的服務端上。

CMS是基於標記-清除算法設計的,它支持用戶線程與GC線程併發執行,以下圖所示

CMS收集器

運做過程分爲4個階段:

初始標記、併發標記、從新標記、併發清除

初始標記的過程就是掃描GC Roots;

併發標記是掃描GC Roots鏈上全部的對象,此時會出現一些對象標記的變更,由於用戶線程仍然在執行;

從新標記的過程是修正併發標記期間產生引用變更的那一部分對象的標記記錄

併發清除是刪除掉標記階段判斷已經死亡的對象,因爲不用移動存活對象,此時也是能夠併發執行的。

CMS收集器有三個缺點:

  1. 對處理器資源特別敏感,因爲是併發執行,因此CMS收集器工做時會佔用一部分CPU資源而致使用戶程序變慢,下降總吞吐量,建議具備四核處理器以上的服務器使用CMS收集器

  2. CMS沒法清除浮動垃圾,有可能出現Concurrent Mode Failure失敗而致使另外一次STWFull GC產生。因爲併發清理過程當中用戶線程與GC線程併發執行,就必定會產生新的垃圾對象,可是沒法在本次GC中處理這些垃圾對象,不得不推遲到下一次GC中處理,這些垃圾對象就稱爲「浮動垃圾」,到JDK6的時候,CMS收集器啓動閾值達到92%,也就是老年代佔了92%的空間後會觸發GC,可是若是剩餘的內存8%不足以分配新對象時,就會發生「併發失敗」,進而凍結用戶線程,使用Serial Old收集器進行一次Full GC,因此觸發CMS收集器的閾值仍是根據實際場景來設置,參數爲-XX:CMSInitiatingOccu-pancyFraction

  3. 基於標記-清除算法會致使內存碎片不斷增多,在分配大對象時有可能會提早觸發一次Full GC。因此CMS提供兩個參數可供開發者指定在每次Full GC時進行「碎片整理」,因爲碎片整理須要移動對象,因此是沒法併發收集的,-XX:+UseCMSCompactAtFullCollection(JDK9開始廢棄),-XX:CMSFullGCsBeforeCompaction(JDK9開始廢棄,默認值是0,每次Full GC都進行碎片整理)。

Garbage First收集器

G1堆內存佈局

這是一個在垃圾收集器技術發展歷史上的里程碑式的成果,它取代了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收集器垃圾收集

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,本文篇幅較長,能讀到這裏是很是不容易的,以後也要多加複習!CMS和G1是很是火的兩款收集器,可是礙於時間,本文尚未很詳細地介紹,以後應該還會出一篇針對目前最流行的垃圾收集器的新特性,還會詳細比較與CMS和G1收集器之間的異同!

參考資料:

《深刻理解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

https://mp.weixin.qq.com/s/_AKQs-xXDHlk84HbwKUzOw

結尾


感謝大家閱讀我編寫的推文!筆者的能力有限,太過於具體和細節的知識暫時還沒法掌握,若是我有任何寫得不正確和不許確的地方,歡迎你們向我提出來,咱們能夠一塊兒學習和交流!

好看 點這裏

本文分享自微信公衆號 - 小菠蘿的IT之旅(jie534838084)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索