詳解JVM垃圾收集算法&垃圾收集器(圖解)

垃圾收集算法:內存回收的方法論

垃圾收集器:內存回收的具體實現


在正式討論垃圾回收算法和垃圾收集器以前,咱們應該瞭解一下JVM是如何判斷一個對象已經死亡的?
JVM主要使用了兩種方法判斷對象是否已經死亡:
  1. 引用計數法:這種方法實現起來簡單,並且也好理解,就是給對象維護了一個計數器,當有一個地方引用它的時候,它就加一,當引用失效的時候計數器就減一。當這個計數器的值爲0的時候,那麼就能夠判斷該對象能夠被清理。
  2. 可達性分析算法:該方法的基本思想就是經過一些」GC Roots」 對象做爲起始點,從這些節點開始向下搜索。就像一棵樹結構同樣,從它的根節點開始搜索,若某些對象不屬於這些樹的子節點,那麼就能夠斷定這些對象爲不可達。能夠被清理。(在咱們下面介紹的垃圾回收算法中,主要是使用了這種方法判斷對象是否須要被回收)
    注:固然,實際的判斷方法沒有這麼簡單,這裏只是簡單的介紹一下。

OK,接下來先說垃圾回收算法

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

    該算法就和他的名字同樣,分爲標記和清除兩個階段。首先標記出全部須要回收的對象,在標記完成以後統一回收全部被標記的對象。算法

圖解:

回收前 (黑色:可回收 | 灰色:存活對象 | 白色:未使用 )
這裏寫圖片描述多線程

回收後:
這裏寫圖片描述併發

從上面的圖解中咱們就能夠看出:
1. 這種算法在收集結束後,內存是亂七八糟的(也就是產生了不少內存碎片),這種內存碎片太多的話,會致使咱們在下一次分配較大對象的時候,沒法找到足夠的連續內存,不得不觸發另一次垃圾回收。
2. 還有就是其實,標記和清理的過程的效率都是不高的。jvm

該算法適合於那些對象存活率較高的內存區域的收集工做。
2. 複製算法(Copying)

    該算法的出現是爲了解決效率問題。它將內存劃分爲等大的兩份,每次只是使用其中的一塊。當一塊用完了,就將還活着的對象複製到另一塊上,完後再將已經使用過的那一塊一次性清除。佈局

圖解:

回收前 (黑色:可回收 | 淺灰色:存活對象 | 白色:未使用 | 深灰色:保留區域 )
這裏寫圖片描述
回收後:
這裏寫圖片描述優化

算法優勢:這種算法不存在出現垃圾碎片的狀況,再分配較大對象時候也不會有沒法找到足夠內存的狀況,實現簡單,運行高效。
算法缺點:使用該算法的代價就是直接將內存縮小一半,代價有點高。其次,再某些狀況下須要進行很大規模的複製動做,會直接影響到效率。線程

該算法適合於那些對象存活率較低的內存區域的收集工做。
3. 標記整理算法(Mark Compact)

    標記整理和標記清除的很是類似,可是標記整理的過程是這樣的,首先是標記要清理的對象,而後將剩下全部存活的對象都移動到一端,而後直接清理端邊界之外的內存。其實也就是標記-整理-清除算法,多了一個對內存的整理的過程。server

圖解:

回收前回收前 (黑色:可回收 | 灰色:存活對象 | 白色:未使用 )
這裏寫圖片描述
回收後:
這裏寫圖片描述對象

你們能夠在圖示中很清楚的看到,該種收集算法的好處就是首先沒有像複製算法那樣浪費掉一半的空間,而後收集後的內存也很是的整潔,沒有內存碎片。
相比標記清除,它能很好的整理內存,但同時也多了整理內存的代價。blog

該算法適合於那些對象存活率較高的內存區域的收集工做。
4. 分代收集算法(Generational Collection)

    這種算法其實並無什麼新的思想,只是根據對象存活週期的不一樣將內存劃分爲幾塊。就是將Java堆劃分爲新生代和老年代,新生代中每次收集都會有大量的對象死去,因此建議採用複製算法,只須要付出較少的複製成本就能完成收集。可是老年代中由於對象的存活率高且沒有額外的空間對它進行分配擔保。因此雖好,使用標記-清理或者標記整理算法來進行收集。


接下來就是垃圾收集器了

這裏寫圖片描述

    上圖所示的就是接下來要介紹的集中垃圾收集器,上圖中用線鏈接起來的兩個垃圾收集器之間是能夠搭配使用的。

1. Serial:基本、歷史悠久

    該收集器是一個單線程的收集器。這裏的單線程收集器並非說明它只會使用一個CPU或者使用一條線程去完成垃圾的收集工做,更爲重要的是在它進行垃圾收集的時候須要」Stop the World」直到它的工做結束。
優勢:簡單高效、在單個CPU環境來講,Serial沒有線程交互的開銷,專心作天然高效率。
缺點:Stop the World 影響用戶體驗!

運行圖示

這裏寫圖片描述

2. ParNew:Serial的多線程版本

    該線程除了使用多條線程進行垃圾收集以外,其他的東西和行爲和Serial是如出一轍的。雖說沒有多大的創新,可是倒是許多運行在Server模式下虛擬機目前首選的新生代垃圾收集器,其主要緣由是,它能夠於另一個吊炸天的老年代的垃圾收集器CMS配合使用。
優勢:一樣具有Serial的優勢。
缺點:Stop the World 影響用戶體驗。

運行圖示

這裏寫圖片描述

插入:在談論垃圾收集器色上下文語境中,併發並行的理解以下:

並行:多條垃圾收集線程並行工做,但此時的用戶線程仍處於等待狀態。
併發:用戶線程和垃圾收集線程同時處理,用戶線程在繼續運行、而垃圾收集程序運行在另外一個CPU。

3. Parallel Scavenge:做用於新生代、使用複製算法、多線程

    從表面看,該收集器和ParNew是很是類似的,但實際上是有所區別的。他們主要的區別在於他們的關注點不一樣,CMS或者ParNew等收集器的關注點主要在於縮短垃圾收集時用戶線程的停頓時間(縮短 Stop The World的時間),可是Parallel Scavenge的關注點是去達到一個能夠控制的吞吐量。(吞吐量=運行用戶代碼的時間/(運行用戶代碼的時間+垃圾收集時間))。
    分析:停頓的時間越短就越適合須要與用戶交互的程序,可是吞吐量高則能夠高效的利用CPU時間,儘快完成程序的任務,主要適合在後臺運算而不須要太多交互的任務。

4. Serial Old:Serial的老年代版本、單線程、標記-整理算法
    工做原理與Serial基本一致。主要做用於Client端。在server端主要有兩種用途:

    1. JDK1.5以前,與Parallel Scavenge搭配使用
    2. 做爲CMS收集器的後備預案,併發收集發生Concurrent Mode Failure時使用。

運行圖示

這裏寫圖片描述

5. Parallel Old收集器:Parallel Scavenge的老年版、多線程、標記-整理、JDK1.6中開始加入
    主要是搭配Parallel Scavenge使用,可使得整個系統的吞吐量達到最大化的優化,在注重吞吐量和CPU資源敏感的場合,能夠優先考慮Parallel套件。
運行圖示

這裏寫圖片描述

6. CMS收集器(Concurrent Mark Sweap):併發收集、低停頓、標記-清除、JDK1.6中加入

該收集器意在獲取最短的停留時間,多應用在B/S結構的服務端上,該收集器的運做過程。

  1. 初始標記(CMS initial mark)
  2. 併發標記(CMS concurrent mark)
  3. 從新標記(CMS remark)
  4. 併發清除(CMS concurrent sweap)

    在上述的四個過程當中:併發標記和從新標記兩個步驟仍是須要」Stop The World」。初始標記只是標記一下 GCRoots 不能直接關聯到的對象,速度很快,併發標記階段就是進行 GC Roots Tracing 的過程。併發標記就至關於再次去判斷該對象是否已經死亡的過程,由於對象是很是可能」復活」的。
    其中的併發標記和併發清除步驟:從整體上來講是和用戶一塊兒來工做的。因此,這也就是它最大的優勢。

    可是它並不完美,它主要有如下三個缺點:
1.對CPU資源敏感

    CMS默認啓動的回收線程數是 (CPU數量+3)/4,也就是CPU在4個以上時,併發回收時垃圾線程很多於25%的CPU資源,而且隨着CPU數的上升而降低。可是,當CPU數不足4個時,好比2個,CMS對用戶程序的影響就比較大了,CPU須要分出一半以上的資源供給垃圾回收,用戶的程序執行速度會下降50%以上。爲了應對這種狀況發生,虛擬機提供了一種稱爲」增量式併發收集器」(Incremental Concurrent Mark Sweap / i-CMS)的變種CMS收集器,但在實際應用中效果不理想,如今已經不提倡使用。

2.沒法處理浮動垃圾,可能會出現(Concurrent Mode Failure)失敗而致使的另外一次Full GC產生。

    因爲CMS在併發清理階段用戶的線程仍在運行。伴隨着程序的運行指定會產生新的垃圾,這種垃圾出如今標記過程以後,CMS沒法在當此處理中處理掉他們,這些就是浮動垃圾。一樣由於,在清理過程當中用戶線程也得跑,就須要預留有足夠的內存空間給用戶使用。若是運行的過程當中的預留的空間不夠使用,那麼就會發生(Concurrent Mode Failure),觸發一次Full GC,臨時啓用 Serial Old收集器從新對老年代進行收集。這樣的停頓時間過長。

3.會產生大量的內存碎片。

    因爲該收集器使用的標記-清除收集算法,就會不可避免的產生大量的內存碎片。給較大的對象分配帶來了很大的麻煩。

運行圖示:

這裏寫圖片描述

7. G1收集器:當今收集器技術發展最前沿的成果之一
它具備以下特色:

    1.併發與並行:G1充分利用多CPU,多核環境下的硬件優點,使用多個CPU來縮短 Stop-The-World停頓的時間,其它收集器須要停頓Java線程執行的GC動做,G1仍然能夠經過併發的方式讓Java程序繼續執行。
    2.分代收集:分代的概念在G1中仍然得以保留。雖然G1能夠不須要其它收集器的配合就能獨立管理整個GC堆。
    3.空間整合:與CMS的標記清除不一樣,G1從總體上看是基於「標記清理」算法實現的收集器。從局部(兩個Region)之間上來看是基於「複製」算法實現的。不管如何,結論就是G1運行期間不會產生內存碎片。
    4.可預測的停頓:G1除了追求低停頓以外,還能創建可預測的停頓時間模型,可讓使用者明確指定一個長度爲M毫秒的時間片斷內,消耗在垃圾收集上的時間不能超過N毫秒。

內存佈局的變化:

    G1以前的其它收集器進行收集的範圍都是整個新生代或者老年代,可是G1再也不這樣。—->他將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還有新生代和老年代的概念,可是新生代和老年代再也不是物理隔離,他們都是一部分Region的集合。

G1作了什麼?讓它具備了能創建可預測的停頓時間模型?

    首先,G2會跟蹤各個Region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需的時間的經驗值),在後臺維護一個優先列表,每側根據容許的收集時間,優先收集經驗值較大的Region。這種具備必定的優先級的回收策略,使得G1在有效的時間裏能夠得到儘量大的回收效率。
    Java堆分爲多個Region以後,那麼在處理對象間依賴這個問題就天然而然的出現了。在G1中,Region之間的對象引用還有其它版本收集器中的新生代和老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。

具體使用 Remembered Set 怎麼實現避免全堆掃描?

    G1中每一個Region中都有一個與之對應的Remembered Set ,虛擬機發現程序在對 Reference類型的數據進行寫操做時。會產生一個 Write Barrier 暫時中斷操做,檢查 Reference 引用的對象是否處於不一樣的Region中,若是是,便經過 CardTable 吧相關的引用信息記錄到被引用對象的Region的 Remembered Set 中。當有GC操做時,在GC Roots 的枚舉範圍中加入 Remembered Set 便可保證不對全堆掃描,且不會有遺漏。

G1收集器運行的大概過程(忽略維護 Remembered Set)
  1. 初始標記(Initial Marking)
  2. 併發標記(Concurrent Marking)
  3. 最終標記(Final Marking)
  4. 篩選回收(Live Data Counting and Evacuation)

· 初始標記僅僅只能標記一下 GC Roots節點能直接關聯到的對象。
· 併發標記是從 GC Roots 開始對堆中的對象進行可達性分析,找出存活的對象,這階段耗時較長,可是能夠用戶程序併發執行。
· 最終標記是爲了修正在併發標記過程當中由於用戶程序繼續運做而致使標記產生變更的那一部分記錄。JVM將變更記錄在 Remembered Set Logs中,最終將 Remembered Set LogsRemembered Set 合併。雖需停頓,可是可並行執行。
· 篩選回收,首先對各個Rgion的回收價值和成本進行排序,根據用戶所指望的GC停頓時間制定回收計劃。

運行圖示

這裏寫圖片描述

終:上面就是我要說的垃圾收集器和垃圾回收算法,若是有問題還需大神指出。
相關文章
相關標籤/搜索