JVM垃圾回收歷險

做個好人君發表於2019-01-31

JVM垃圾回收歷險

一句話介紹,垃圾回收就是由程式自動的回收已死物件。

已死物件就是程式中一定不會再被使用到的物件。

垃圾回收可以分為兩個部分,一是如何判斷物件已死,二是如何清理掉已死物件。

判斷物件已死的方法

引用計數法

引用計數法比較好理解,就是為每個物件分配一個計數器,當一個物件被另一個物件引用時,其對應的計數器+1,當引用關係被解除時,計數器-1。當一個物件的計數器值為0時,則代表該物件可以被回收了。

引用計數法的優點是實現簡單且回收效率高,而缺點就是無法解決迴圈引用的問題。

引用計數法在Python等一些語言中有使用到,但jvm並沒有採用,關鍵原因也是其無法解決迴圈引用的問題(那python的迴圈引用物件不能被回收?)。

可達性分析

可達性分析是商用jvm中採用的判斷物件已死演算法。

該演算法類似於圖遍歷,我們把所有物件描述為一張圖,節點是物件,邊是引用關係。

GC ROOT節點出發,遍歷所有節點,對於遍歷到的每個節點都做一個標識,遍歷完成後。沒有標識的節點說明是可回收的。

這裡的GC ROOT在JVM中指的是以下幾類物件:

  1. 被棧中的本地變數表引用的物件
  2. 被靜態變數引用的物件
  3. 被常量引用的物件
  4. 被JNI方法中引用的物件

回收演算法

上一節介紹了垃圾回收第一步:判斷物件是否可以被回收,這一小節則會闡述一些常用的回收演算法。

標記清除

標記清除演算法也比較簡單。通常使用一張(類似)來記錄哪些空間已被使用。首先通過可達性分析找到所有的垃圾,然後將其佔用的空間釋放掉。
該演算法的問題是可能會產生大量的記憶體碎片。

image

標記整理

為了解決記憶體碎片的問題,標記整理在標記清楚演算法上做了優化,在找到所有垃圾物件後,不是直接釋放掉其佔用的空間,而是將所有存活物件往記憶體一端移動。回收完成後,所有物件都是相鄰的。

image

複製演算法

複製演算法將記憶體區域劃分為兩個,同一時間只有一個區域有物件,每次垃圾回收時,通過可達性分析演算法,找出所有存活物件,將這些存活物件移動到另一區域。為新物件分配記憶體時,可以通過智慧指標的形式,高效簡單。
複製演算法的缺點是會浪費一部分空間以便存放下次回收後存活的物件且需要一塊額外的空間進行擔保(當一個區域存放不下存活的物件時)。

分代收集

在商用jvm中,大多使用的是分代收集演算法。
根據物件的特性,可以將記憶體劃分為3個代:年輕代,老年代,永久代(jvm8後稱為元空間)。年輕代存放新分配的物件,使用的是複製演算法,老年代使用標記清除or標記整理演算法。其中年輕代分為一個Eden區和兩個Survivor區,其比例預設為8:1:1(-XX:SurvivorRatio),新物件在Eden區分配,當Eden區存放不下時,觸發一次Young GC,將Eden區和一個Survivor區域的所有存活物件拷貝到另一個Survivor區域。如果物件大小超過一定大小(-XX:PretenureSizeThreshold),則會直接在老年代分配。老年代採用標記清除或標記整理演算法。

年輕代老年代比例預設為3:8(-XX:NewRatio,-Xmn),老年代一般來說要比年輕代要大,因為當年輕代空間不足以存放下新物件時,需要老年代來擔保。

年輕代使用複製演算法的原因是年輕代物件的建立和回收很頻繁,同時大部分物件很快都會死亡,所以複製演算法建立和回收物件的效率都比較高。

老年代不使用複製演算法的原因是老年代物件通常存活時間比較長,如果採用複製演算法,則複製存活物件的開銷會比較大,且複製演算法是需要其他區域擔保的。 所以老年代不使用複製演算法。

垃圾回收器

下文將介紹jvm中常用的垃圾回收器

Serial序列回收器(年輕代)

使用單執行緒,複製演算法實現。在回收的整個過程中需要Stop The World。在單核cpu 的機器上,使用單執行緒進行垃圾回收效率更高。

使用方法:-XX:+UseSerialGC

ps:在jdk client模式,不指定VM引數,預設是序列垃圾回收器

Serial Old序列回收器(老年代)

與Serial相似,但使用標記整理演算法實現。

ParNew並行回收器(年輕代)

Serial的多執行緒形式,
-XX:+UseParNewGC(新生代使用並行收集器,老年代使用序列回收收集器)或者-XX:+UseConcMarkSweepGC(新生代使用並行收集器,老年代使用CMS)。

Parallel Scavenge 基於吞吐量的並行回收器(年輕代)

多執行緒的回收器,高吞吐量(=程式執行時間/(程式執行時間+回收器執行時間)),可以高效率的利用CPU時間,儘快完成程式的運算任務,適合後臺應用等對響應時間要求不高的場景。

有一個自適應條件引數(-XX:+UseAdaptiveSizePolicy),當這個引數開啟後,無需手動指定新生代大小(-Xmn),Eden和Survivor比例(-XX:SurvivorRatio)等引數,虛擬機器會動態調節這些引數來選擇最適合的停頓時間(-XX:MaxGCPauseMillis)或吞吐量( -XX:GCTimeRatio)。

Parallel Scavenge是Server級別多CPU機器上的預設GC方式,也可以通過-XX:+UseParallelGC來指定,並且可以採用-XX:ParallelGCThread來指定執行緒數。

Parallel Scavenge對應的老年代收集器只有Serial Old和Parallel Old。不能與CMS搭配使用的原因是,其使用的框架不同,並不是技術原因。

Parallel Old 基於吞吐量的並行回收器(老年代)

使用多執行緒和“標記-整理”演算法。與Parallen Scavenge相似,只不過是運用於老年代。

CMS 關注暫停時間的回收器 (老年代)

基於標記清除演算法實現,關注GC的暫停時間,在注重響應時間的應用上使用。

在說CMS具體步驟前,我們先看下CMS使用的垃圾標記演算法:三色標記法

三色標記法

將堆中物件分為3個集合:白色、灰色和黑色

白色集合:需要被回收的物件

黑色集合:沒有引用白色集合中的物件,且從GC ROOT可達。該集合的物件是不會被回收的

灰色集合:從根可達但是還沒有掃描完其引用的所有物件,該集合的物件不會被回收,且當其引用的白色物件全部被掃描後,會將其加入到黑色集合中。

一般來說,會將被GC ROOT直接引用到的物件初始化到灰色集合,其餘所有物件初始化到白色集合,然後開始執行演算法:

1.將一個灰色物件加入到黑色集合

2.將其引用到的所有白色物件加入到灰色集合

3.重複上述兩步,直到灰色集合為空

該演算法保證從GC ROOT出發,所有沒有被引用到的物件都在白色集合中,所以最後白色集合中的所有物件就是要回收的物件

CMS回收過程

分為4個過程,初始標記,併發標記,重新標記,併發清理。

初始標記:
GC ROOT出發,找到所有被GC ROOT直接引用的節點。此過程需要Stop The World。

併發標記:
以上一步驟的節點為根節點,併發的遍歷所有節點。同時會開啟Write Barrier.如果在此過程中存在黑色物件增加對白色物件的引用,則會記錄下來。

在這裡,我們試想下如果三色標記法是先執行步驟2後執行步驟1(上面三色標記演算法的步驟),會發生什麼?

如下圖,在GC過程中,用三色標記法遍歷到A這個物件(圖1),將A引用到的BCD標記為灰色。之後,在應用程式執行緒中建立了一個物件E,A引用了它( 圖2這個階段GC是併發標記的)。然後將A標記為黑色(圖3)。在gc掃描結束後,E這個物件因為是白色的,所以將被回收掉。這顯然是不能接受的,併發垃圾回收器的底線是允許一部分垃圾暫時不回收(見下面的浮動垃圾),但絕不允許從根可達的存活物件被當作垃圾處理掉!

image

重新標記:
因為併發標記的過程中可能有引用關係的變化,所以該階段需要Stop The World。以GC ROOTWritter Barrier中記錄的物件為根節點,重新遍歷。
這裡為什麼還需要再遍歷GC ROOT?因為Writter Barrier是作用在堆上的,無法感知到GC ROOT上引用關係的變更。

併發清理:
併發的清理所有垃圾物件

CMS通過將步驟拆分,實現了降低STW時間的目的。但CMS也會有以下問題:

1.浮動垃圾,在併發標記的過程中(及之後階段),可能存在原來被引用的物件變成無人引用了,而在這次gc是發現不會清理這些物件的。

2.cpu敏感,因為使用者程式是和GC執行緒同時執行的,所以會導致GC的過程中程式執行變慢,gc執行時間增長,吞吐量降低。預設回收執行緒是(CPU數量+3)/4,也就是cpu不足4個時,會有一半的cpu資源給GC執行緒。

3.空間碎片,標記清除演算法都有的問題。當碎片過多時,為大物件分配記憶體空間就會很麻煩,有時候就是老年代空間有大量空間剩餘,但沒有連續的大空間來分配當前物件,不得不提前觸發full gc。CMS提供一個引數(-XX:+UseCMSCompactAtFullCollection),在Full Gc發生時開啟記憶體合併整理。這個過程是STW的。同時還可以通過引數(-XX:CMSFullGCsBeforeCom-paction)來這隻執行多少次不壓縮的Full GC後,來一次壓縮的。

4.需要更大的記憶體空間,因為是同時執行的GC和使用者程式,所以不能像其他老年代收集器一樣,等老年代滿了再觸發GC,而是要預留一定的空間。CMS可以配置當老年代使用率到達某個閾值時( -XX:CMSInitiatingOccupancyFraction=80 ),開始CMS GC。

在old GC執行的過程中,可能有大量物件從年輕代晉升,而出現老年代存放不下的問題(因為這個時候垃圾還沒被回收掉),該問題叫Concurrent Model Failure,這時候會啟用Serial Old收集器,重新回收整個老年代。Concurrent Model Failure一般伴隨著ParNew promotion failed(晉升擔保失敗),解決這個問題的辦法就是可以讓CMS在進行一定次數的Full GC(標記清除)的時候進行一次標記整理演算法,或者降低觸發cms gc的閾值

G1 新一代垃圾回收器 (整個堆)

講實話,我還沒太看懂,沒法寫自己的理解,
少年等我。。

相關文章