【JVM系列5】深刻分析Java垃圾收集算法和經常使用垃圾收集器

前言

上一篇咱們介紹了對象在堆內的內存佈局已經佔用空間的大小,同時分析了堆內能夠分爲Young區和Old區,並且Young區能夠分爲Eden區和Survivor區,Survivor區又拆分紅了兩個大小同樣的區S0和S1區域,其實這麼拆分的理由和GC是密切相關的,那麼這一篇文章就讓咱們深刻了解一下Java中的垃圾收集機制。java

如何肯定無效對象

在垃圾收集的時候第一件事就是怎麼肯定一個對象是垃圾,那麼該如何肯定一個對象已經能夠被回收了呢?主流的算法有兩種:引用計數法可達性分析算法算法

引用計數法(Reference Counting)

這個算法很簡單,效率也很是高。就是給每一個對象添加一個引用計數器,每當有一個地方引用它時,計數器的值就加1,當引用失效時,計數器的值就減1,當計數器的值減爲0時就表名這個對象不會再被使用,成爲了無用對象,能夠被回收。編程

這種算法雖然實現簡單,效率也高,可是存在一個問題,咱們看下面一個場景:多線程

在這裏插入圖片描述
上圖中4個對象相互引用,可是並無其餘對象去引用他們,這種對象實際上也是無效對象,可是他們的引用計數器都是1而不是0,因此引用計數法沒辦法解決這種「一坨垃圾」的場景。併發

可達性分析算法(Reachability Analysis)

可達性分析算法就是選擇一些對象做爲起始點,這些對象稱之爲:GC Root。而後從GC Root開始向下搜索,搜索路徑稱之爲引用鏈(Reference Chain),當一個對象不在任何一條引用鏈上時,就說明此對象是無效對象,能夠被回收。
好比說下面這幅圖,右邊那一串互相引用的對象由於沒有不在GC Root的引用鏈上,因此就是無效對象,可達性分析算法有效的解決了互相引用對象沒法回收問題。
在這裏插入圖片描述jvm

GC Root

在Java中,能夠做爲GC Root對象的包括下面幾種:工具

  • Java虛擬機棧內棧幀中的局部變量表中的變量
  • 方法區中類靜態屬性
  • 方法區中常量
  • 本地方法棧中JNI(即Native方法)中的變量

注意:在分析對象的過程當中,爲了確保結果的準確性,須要保證分析過程當中對象引用關係不會發生變化,而爲了達到這個目的,就須要暫停用戶線程,這種操做也叫:Stop The World(STW)。佈局

引用的分類

上面兩種算法其實都是一個目的,判斷對象有沒有被引用,而引用也不只僅都是同樣的引用,JDK1.2開始,Java中將引用進行了分類,劃分紅了四種引用,分別是:強引用,軟引用,弱引用,虛引用。這四種引用關係的強度爲:強引用>軟引用>弱引用>虛引用。性能

強引用(Strong Reference)

咱們寫的代碼中通常都是用的強引用,如:Object obj = new Object()這種就屬於強引用,強引用只要還存在,必定不會被回收,空間不夠就直接拋出OOM異常spa

軟引用(Soft Reference)

軟引用是經過SoftReference類來實現的。軟引用能夠用來表示一些還有用但又是非必需的對象,系統在即將溢出以前,若是發現有軟引用的對象存在,會對其進行二次回收,回收以後內存仍是不夠,就會拋出OOM異常。

弱引用(Weak Reference)

弱引用是經過WeakReference類來實現的。弱引用也是用來表示非必需對象的,可是相比較軟引用,弱引用的對象會在第一次垃圾回收的時候就被回收掉。

虛引用(Phantom Reference)

虛引用是經過PhantomReference類來實現的,也被成爲幽靈引用或者幻影引用。這是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不對其生存時間構成影響。也沒法經過虛引用來取得一個對象實例。設置爲虛引用的惟一用處可能就是當這個對象被回收的時候能夠收到一個系統通知。

垃圾收集算法

上面分析瞭如何肯定一個對象屬於可回收對象的兩種算法,那麼當一個對象被肯定爲垃圾以後,就須要對其進行回收,回收也有不一樣的算法,下面就來看一下經常使用的垃圾收集算法

標記-清除(Mark-Sweep)算法

標記-清除算法主要分爲兩步,標記(Mark)和清除(Sweep)。
好比說有下面一塊內存區域(白色-未使用,灰色-無引用,藍色-有引用):
在這裏插入圖片描述
而後標記-清除算法會進行以下兩個步驟:

  • 一、將堆內存掃描一遍,而後會把灰色的區域(無引用對象,可悲回收)對象標記一下。
  • 二、繼續掃描,掃描的同時將被標記的對象進行統一回收。

標記清除以後獲得以下圖所示:
在這裏插入圖片描述
能夠很明顯看到,回收以後內存空間是不連續的,產生了大量的內存空間碎片。過多內存碎片最直接的就是能夠致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。

標記-清除算法的缺點

一、標記和清除兩個過程都比較耗時,效率不高
二、會產生大量不連續的內存碎片。

爲了解決這兩個問題,因此就有了複製算法。

複製(Copying)算法

複製算法的思想就是把內存區域一分爲二,兩塊內存保持同樣的大小,每次只使用其中的一塊,當其中一塊內存使用完了以後,將仍然存活的對象複製到另外一塊內存區域,而後把已使用的一半內存所有一次性清理掉。
以下圖(綠色表示暫時不放對象的一半空間):
在這裏插入圖片描述
回收以後:
在這裏插入圖片描述

複製算法的缺點

複製算法的缺點就是犧牲了一半的內存空間,有點過於浪費。

複製算法在Java虛擬機的落地形式

Java堆內存中作了好幾回劃分,最後是將Survivor區分紅了2個區域S0和S1來進行復制算法,這種作法就是爲了彌補原始複製算法直接將一半的空間做爲空閒空間方式的彌補。

IBM公司的研究代表,Young區(新生代)中98%的對象都是「朝生夕死」的,生命週期極短,因此說在一次GC以後能存活下來的對象不多,徹底不必劃分一半的區間來進行復制算法。Hot Spot虛擬機中Eden區和Survivor區域的比例爲:Eden:S0:S1=8:1:1,也就是說其實只有10%的空間被浪費掉,徹底是能夠接受的。

標記-整理(Mark-Compact)算法

咱們想一下,假如Young區(新生代)的對象在一次GC以後,基本全部對象都存活下來了,那就須要複製大量的對象,效率也會變低。而堆中的old區(老年代)的特色就是對象生命週期極爲頑強,由於默認要進行第16次垃圾回收的時候還能存活下來的對象纔會放到老年代,因此對老年代中對象的回收通常不會選擇標記-複製算法。

標記-整理算法就是爲了老年代而設計的一種算法,標記-整理算法和標記清除算法的區別就是在最後一步,標記-整理算法不會對對象進行清理,而是進行移動,將存活的對象所有向一端移動,而後清理掉端邊界之外的對象。以下圖所示:
回收前:
在這裏插入圖片描述
回收後:
在這裏插入圖片描述

分代收集算法(Generational Collection)

目前主流的商業虛擬機都是採用的分代收集算法,這種算法本質上就是上面介紹的算法的結合體。新生代採用標記-清除算法,老年代採用標記-清除或者標記-整理算法。

垃圾收集器

上面介紹了肯定對象的算法以及回收對象的算法,而後具體要怎麼落地卻並無一個規定,而垃圾收集器就是實現了對算法的落地,而由於落地形式不一樣,天然也產生了不少不一樣的收集器。下面是一張收集器的彙總圖:
在這裏插入圖片描述
上面一半表示新生代收集器,下面一半表示老年代收集器,橫跨中間的表示均可以用。

根據這個圖形有了總體認知以後,咱們再來一個個看看這些垃圾收集器的工做原理吧。

Serial和Serial Old收集器

Serial收集器是基本、發展歷史悠久的收集器,在JDK1.3.1以前是虛擬機新生代收集的惟 一選擇。
Serial收集器是一種單線程收集器,並且是在進行垃圾收集的時候須要暫停全部其餘線程,也就是說觸發了GC的時候,用戶線程是暫停的,若是GC時間過長,用戶是能夠明顯感知到卡頓的。
Serial Old是Serial的一個老年代版本,也是一種單線程收集器。
能夠用下面一個圖形來表示一下Serial和Serial Old收集器的工做原理:
在這裏插入圖片描述
優勢:簡單高效,擁有很高的單線程收集效率
缺點:收集過程須要暫停全部線程
算法:Serial採用複製算法,Serial Old採用標記-整理算法
適用範圍:Serial用於新生代,Serial Old用於老年代
應用:Client模式下的默認的收集器

ParNew收集器

ParNew收集器是Serial收集器的多線程版本,實現了並行執行,其他工做原理都和Serial一致。可使用參數:-XX:+UseParNewGC來指定使用。

注意:這裏的並行指的是多個GC線程並行,可是其餘線程仍是暫停,而併發指的是用戶線程和GC線程同時執行。

ParNew收集器默認開啓和CPU個數相同的線程數來進行回收,可使用參數:-XX:ParallelGCThreads來限制線程數
ParNew收集器工做原理以下圖:
在這裏插入圖片描述
優勢:在多CPU時,比Serial效率高。
缺點:收集過程暫停全部應用程序線程,單CPU時比Serial效率差
算法:複製算法
適用範圍:新生代
應用:運行在Server模式下的虛擬機中首選的新生代收集器

Parallel Scavenge收集器

Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,和ParNew同樣也是一個並行的多線程收集器,Parallel Scanvenge收集器相比較於ParNew收集器,更關注與提高系統的吞吐量。

吞吐量指的是CPU用於運行用戶代碼的而時間於CPU總消耗時間的比值。
即:吞吐量=運行用戶代碼時間/(運行用戶代碼時間+GC時間)

Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量:

`-XX:MaxGCPauseMillis//GC最大停頓毫秒數,必須大於0
-XX:GCTimeRatio//設置吞吐量大小,大於0小於100,默認值爲99` 

*   1
*   2

咱們思考一個問題,假如咱們經過參數把容許最大停頓毫秒數設置的相對較小會怎麼樣?是否是GC速度就會變快了

答案是否認的。若是設置的時間太短,Parallel Scavenge收集器會犧牲吞吐量和新生代空間來交換。
好比新生代400Mb須要GC時間爲100ms,而後手動設置爲50ms,那麼就會把新生代調小爲200Mb,這樣確定時間就降下來了,然而這種操做可能會下降吞吐量,假如說原先是10s觸發一次GC,每次100ms,修改時間後編程5s觸發一次GC,每次70ms,那麼10s觸發兩次GC時間就變成了140ms,吞吐量反而下降。

若是不知道如何設置,那麼還能夠經過參數:-XX:+UseAdaptiveSizePolicy開啓自適應策略(GC Ergonomics),這樣咱們就不須要手動設置吞吐量和GC停頓時間了,虛擬機會根據運行狀況手機監控信息來動態調整。

Paralled Old收集器

Paralled Old收集器是Parallel Scavenge收集器的老年代版本,可是這個收集器是jdk1.6以後纔出現的,因此致使了在Paralled Old收集器出現以前Parallel Scavenge收集器一直找不到合適的「搭檔」。由於Parallel Scavenge收集器沒辦法和CMS收集器配合使用(後面會介紹緣由),因此在Paralled Old收集器出現以前,若是新生代選擇了Parallel Scavenge收集器,那麼老年代就只能選擇Serial Old收集器,而Serial Old收集器是單線程的,因此單單只是新生代替換成了多線程的吞吐量收集器Parallel Scavenge,在性能上並不必定有多少提高。

在注重吞吐量的業務系統中,能夠考慮Parallel Scavenge+Paralled Old收集器配合使用,結合使用後的工做原理以下圖所示:
在這裏插入圖片描述
PS:在jdk1.8中,默認收集器就是Parallel Scavenge+Parallel Old組合

CMS(Concurrent Mark Sweep)收集器

這是一種以實現GC時最短停頓時間爲目標的收集器,也是一款真正實現了併發回收的收集器。固然,雖然是併發的,可是仍然須要Stop The World,只是儘量將這個時間縮到最短。

對於任何暫停時間要求較低的應用程序,都應該考慮使用此收集器。CMS收集器能夠經過參數:-XX:+UseConcMarkSweepGC啓用。

CMS收集器是基於算法標記-清除來實現的,整個過程分爲4步:

  • 一、初始標記(inital mark)
    須要Stop The World。標記GC Roots對象,由於GC Root對象並不會不少,因此這個過程很是快。
  • 二、併發標記(concurrent mark)
    這個階段能夠和用戶線程同時進行,也能夠分爲三步:
    (1)併發標記(CMS-concurrent-mark):主要是進行GC Roots Tracing。就是說根據第1步中找到的GC Root對象,開始搜索,這個過程相比階段1是比較慢的。
    (2)預清理(CMS-concurrent-preclean),這個階段是爲了處理併發標記以後發生了變化的對象
    (3)可被終止的預清理(CMS-concurrent-abortable-preclean),這個預清理差很少,可是是能夠被終止的,主要是分了儘量分擔下面第3步的工做,這個階段會有一個abort觸發條件,該階段存在的目的是但願能發生一次Young GC,這樣就能夠減小Young區對象的數量,下降從新標記的工做量,由於從新標記會掃描整個堆內空間。能夠經過參數-XX:+CMSScavengeBeforeRemark參數控制在從新標記前發生一次Young GC,默認爲false。這個階段發生的最大時間由-XX:CMSMaxAbortablePrecleanTime控制,默認5s
  • 三、從新標記(remark)
    須要Stop The World,這個階段是爲了修正在階段2標記以後產生了變化的對象
  • 四、併發清除(concurrent sweep)
    和用戶線程同時進行,開始正式清除垃圾,在此階段也會產生垃圾,產生垃圾後沒法清除,只能留待下一次GC。

CMS收集過程以下圖所示:
在這裏插入圖片描述

CMS優缺點

  • 優勢:併發收集、低停頓。
    其實最主要的是CMS把收集過程當中步驟拆分了,而最耗時的操做都是併發執行,天然就會低停頓了。
  • 缺點:產生大量空間碎片、併發階段會下降吞吐量。
    CMS採用的是標記-清除算法,因此會產生大量的空間碎片。在階段2和階段4併發執行的時候,會佔用CPU資源,就會致使應用程序變慢,下降了吞吐量。

Floating Garbage(浮動垃圾)

上面的步驟中,步驟2是併發標記,因此在標記過程當中,可能會有新的垃圾產生而沒有被標記到。好比說對象A,剛掃描的時候是有效對象,而後繼續掃描的時候,對象A又變成不可用了,而後還有併發清除的階段,也可能會有新的垃圾產生,這種就稱之爲浮動垃圾(Floating Garbage)。CMS並不能收集浮動垃圾,只能等到下一次GC時再回收。

Concurrent Mode Failure(併發模式失敗)

CMS收集器不能和其餘收集器同樣等到空間滿了纔開始觸發GC,由於CMS收集的時候是併發的,併發的過程確定會持續產生對象,若是由於在垃圾收集期間內存不足而致使了GC失敗,就稱之爲Concurrent Mode Failure。出現這種狀況以後,Java虛擬機就會啓動預備方案,啓用Serial Old收集器替換CMS收集器,這時候整個GC過程都會Stop The World。

CMS收集器的觸發閾值能夠經過參數:-XX:CMSInitiatingOccupancyFraction=來進行設置,N爲(0,100)之間,在jdk1.6中默認是92,即老年代空間使用率達到92%就會觸發CMS收集器開始進行垃圾回收。

G1(Garbage-First)收集器

G1也是以實現GC時最短停頓時間爲目標併發回收的收集器,它嘗試以高几率知足垃圾收集(GC)暫停時間目標,同時實現高吞吐量。

在G1以前的其餘收集器都是屬於分代收集器,也就是說一個收集器要否則用於新生代,要否則就是用於老年代,而G1中,將堆的整個內存佈局作了很大的修改,在G1中,將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然在邏輯上還保留了新生代和老年代的概念,可是物理上已經沒有隔離了。

G1收集器中堆內佈局以下圖所示:
在這裏插入圖片描述
上圖中堆被劃分爲一組大小相同的Region,每一個Region都是連續的虛擬內存範圍。
G1能夠知道哪一個Region區域內大部分都是空的,這樣就能夠在每次容許的收集時間內去優先回收價值最大的Region區域(根據回收所得到的空間大小以及回收所須要的時間綜合考慮),因此這也就是G1爲何叫作Garbage-First的緣由。

PS:G1是JDK1.9的默認垃圾收集器

G1特色

通過上面的簡單介紹,能夠得出G1主要有如下特色:

  • 一、實現了並行與併發,儘量的縮短了Stop The World時間。
  • 二、分代收集:邏輯上依然保留了分代概念
  • 三、空間整合:總體來看是基於「標記-整理」算法來實現的(若是衝Region來看,是基於「複製」算法),因此不會產生大量內存空間碎片。
  • 四、支持可預測的停頓時間:能夠經過參數來設置每次GC最大時間
  • 五、非實時收集:由於能夠人爲設置停頓時間,因此在指定時間範圍內會進行優先選擇收集,而不會收集全部被標記好的垃圾。

G1工做流程

G1收集器在工做流程上和CMS比較類似,只是在最後的步驟有所區別,主要通過了以下4個步驟:

  • 一、初始標記(Initial Marking):須要Stop The World。標記一下GC Roots可以關聯的對象,而且修改TAMS(Next Top at Mark Start)的值,使得下一階段併發運行時,能在正確可用的Region中建立對象。
  • 二、併發標記(Concurrent Marking):和CMS同樣,主要是進行GC Roots Tracing,找出存活對象進行標記。
  • 三、最終標記(Final Marking):須要Stop The World。和CMS同樣,這個階段主要是爲了修正在併發標記期間因用戶程序繼續運行而致使產生變更的對象。
  • 四、篩選回收(Live Data Counting and Evacuation):對各個Region的回收價值和成本進行排序,根據 用戶所指望的GC停頓時間制定回收計劃進行回收。

工做流程圖以下所示:
在這裏插入圖片描述

G1應用場景

G1的第一個重點是爲運行須要大堆且GC延遲有限的應用程序的用戶提供解決方案。這意味着堆大小大約爲6GB或更大,而且穩定且可預測的暫停時間低於0.5秒。

若是咱們的應用程序具備如下一個或多個特性,那麼能夠考慮切換到G1收集器。

  • 一、超過50%的Java堆被實時數據佔用。
  • 二、對象分配率或提高率差別很大。
  • 三、當前應用程序GC停頓時間超過0.5到1秒,而又想縮短停頓時間的應用。

其餘收集器

  • ZGC收集器:是Java11中提供的一種垃圾收集器。
  • Shenandoah:OpenJDK中包含的收集器,最開始是由RedHat公司開發,後來貢獻給了OpenJDK。
  • Epsilon(A No-Op Garbage Collector):一款控制內存分配,可是不執行任何垃圾回收工做的收集器。一旦java的堆被耗盡,jvm就直接關閉。

如何選擇垃圾回收器

垃圾收集器主要能夠分爲以下三大類:

  • 串行收集器:Serial和Serial Old
    只能有一個垃圾回收線程執行,用戶線程暫停。 適用於內存比較小的嵌入式設備 。
  • 並行收集器[吞吐量優先]:Parallel Scanvenge和Parallel Old
    多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態。 適用於科學計算、後臺處理等若交互場景 。
  • 併發收集器[停頓時間優先]:CMS和G1。
    用戶線程和垃圾收集線程同時執行(但並不必定是並行的,多是交替執行的),垃圾收集線程在執行的時候不會停頓用戶線程的運行。 適用於對時間有要求的場景,好比Web應用。

總結

本文主要介紹了肯定無效對象的兩種算法,而且結合垃圾收集算法介紹了不一樣類型的落地形式而產生的不一樣垃圾收集器,本文將對比較偏向於理論,下一篇開始,JVM系列文章將會結合JVM系列前5篇文章來進一步結合實際場景以及相關監控工具的使用來進行實際場景分析。

相關文章
相關標籤/搜索