您現在的位置是:首頁 > 音樂首頁音樂

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

由 阿風的架構筆記 發表于 音樂2021-09-08
簡介** * @author jack xu *public class ReferenceCount {public Object instance = null

你還不明白嗎

我們知道 JVM 調優主要調的是

垃圾收集器的選擇

引數的設定

,所以我們對垃圾回收的知識必須要掌握瞭解,不然怎麼調優呢,那麼什麼是垃圾呢,我們類比生活中的垃圾,就是不要的東西,需要清除的東西,那麼第一步就是要找到垃圾,在Java中我們有兩種方式。

怎麼定義垃圾

引用計數法

給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就+1。當引用失效時,計數器值就-1。任何時刻計數器為 0 的物件就是不可能再被使用,判斷為不可達,等待 gc 清理。這個方法幾乎報廢,因為如果AB相互持有引用,導致永遠不能被回收,大家看下面這段程式碼。。

/** * @author jack xu */public class ReferenceCount { public Object instance = null; public static void main(String[] args) { testGC(); } public static void testGC() { ReferenceCount a = new ReferenceCount(); ReferenceCount b = new ReferenceCount(); a。instance = b; b。instance = a; a = null; b = null; }}

最後在看下圖就明白了,最後這2個物件已經不可能再被訪問了,但由於他們相互引用著對方,導致它們的引用計數永遠都不會為0,透過引用計數演算法,也就永遠無法通知 GC 收集器回收它們。

這就導致了記憶體洩露,最後會導致記憶體溢位。

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

可達性分析

可達性分析演算法的基本思路是,透過一些被稱為引用鏈(GC Roots)的物件作為起點,從這些節點開始向下搜尋,搜尋走過的路徑被稱為(Reference Chain),當一個物件到 GC Roots 沒有任何引用鏈相連時(即從 GC Roots 節點到該節點不可達),則證明該物件是不可用的,如下圖所示。。

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

在Java中,可作為GC Roots物件的列表:

Java虛擬機器棧(棧幀中的本地變量表)中引用的物件

本地方法棧中JNI(即一般說的Native方法)引用的物件。

方法區中類靜態屬性引用的物件

方法區中常量的引用物件。

類載入器

執行緒 Thread 類

垃圾回收演算法

在確定了哪些垃圾可以被回收後,垃圾收集器要做的事情就是開始進行垃圾回收,但是這裡面涉及到一個問題是:

如何高效地進行垃圾回收?

這裡一共有三種演算法。

標記-清除(Mark-Sweep)

這是最基礎的垃圾回收演算法,標記-清除演算法分為兩個階段:標記階段和清除階段。標記階段是標記出所有需要被回收的物件,清除階段就是回收被標記的物件所佔用的空間。

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

缺點:

位置不連續,產生碎片

效率偏低,兩遍掃描,標記和清除都比較耗時

複製演算法 (Copying)

為了解決 Mark-Sweep 演算法的缺陷,Copying 演算法就被提了出來。它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用的記憶體空間一次清理掉,這樣一來就不容易出現記憶體碎片的問題。

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

優點:沒有碎片,空間連續

缺點:

導致50%的記憶體空間始終空閒浪費!

標記-整理(Mark-Compact)

為了解決 Copying 演算法的缺陷,充分利用記憶體空間,提出了 Mark-Compact 演算法。該演算法標記階段和 Mark-Sweep 一樣,但是在完成標記之後,它不是直接清理可回收物件,而是將存活物件都向一端移動,然後清理掉端邊界以外的記憶體。

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

優點:沒有碎片,空間連續

缺點:效率偏低,兩遍掃描,指標需要調整

分代收集演算法

分代收集演算法(Generational Collection)嚴格來說並不是一種思想或理論,而是融合上述3種基礎的演算法思想,而產生的針對不同情況所採用不同演算法的一套組合拳。

我們知道大多數物件都是朝生夕死的,所以我們把堆分為了新生代、老年代,以及永生代(JDK8 裡面叫做元空間),方便他們按照不同的代進行不同的垃圾回收。新生代又被進一步劃分為 Eden(伊甸園)和 Survivor(倖存者)區,他們的比例是8:1:1。

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

下面我用圖解來演示一下分代垃圾收集過程:

第一步:新分配的物件會放在伊甸園,伊甸園滿了就會觸發 minor gc,minor gc 會清除包括 s0 , s1 在內所有年輕代裡面不用的垃圾。

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

第二步:伊甸園裡面沒有被清除的物件就是倖存下來的,將他們年齡+1,放到 s0 區

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

第三步:伊甸園裡面滿了以後再次觸發 minor gc,伊甸園倖存的物件年齡+1放到s1區,s0區倖存的物件年齡+1放到 s1 區,這樣 s0 區就空出來了

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

第四步:如此反覆

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

第五步:倖存者區達到年齡後,進入到老年代,預設是15歲(CMS裡預設是6歲),這個年齡是可以自己調的

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

第六步:如果老年代記憶體滿了,就會觸發 major GC 或者 full GC。觸發 full GC 就會出現所謂的 STW(stop the world)現象。記憶體越大,STW 的時間也越長,所以記憶體也不僅僅是越大越好。

看了上面的圖解後我們知道,年輕代用的是複製演算法,因為物件大多數生命週期短,回收非常頻繁,用複製演算法效率高;而老年代用的是標記清除或標記整理演算法,因為在老年代物件存活時間比較長,複製來複制去沒必要。

垃圾收集器

垃圾收集器是垃圾回收演算法的具體實現,說白了就是落地,我們介紹下面幾個常用的收集器。

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

Serial/Serial Old

Serial/Serial Old 收集器是最基本最古老的收集器,它是一個單執行緒收集器,並且在它進行垃圾收集時,必須暫停所有使用者執行緒,會 Stop The World。Serial 收集器是針對新生代的收集器,採用的是

Copying

演算法。Serial Old 收集器是針對老年代的收集器,採用的是 Mark-Compact 演算法。它的優點是實現簡單高效,但是缺點是會給使用者帶來

停頓

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

引數控制:-XX:+UseSerialGC -XX:+UseSerialOldGC

ParNew

收集器其實就是 Serial 收集器的多執行緒版本,多執行緒並行進行垃圾收集。在多核 CPU 時,比 Serial 效率高,在單核 CPU 時和 Serial 是差不多的。作用在新生代,使用複製演算法,配合CMS使用。

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

引數控制:-XX:+UseParNewGC

Parallel Scavenge/Parallel Old

Parallel Scavenge 收集器是一個新生代的多執行緒收集器。他也是

並行收集

,看上去和 ParNew 一樣,但是 Parallel Scanvenge 更關注系統的

吞吐量

,其採用的是

Copying

演算法。

Parallel Old 是Parallel Scavenge 收集器的老年代版本,也是並行收集器,使用多執行緒和 Mark-Compact 演算法,也是更加關注吞吐量。

引數控制:-XX:+UseParallelGC -XX:+UseParallelOldGC

CMS

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收

停頓時間

為目標的收集器,它是一種

併發收集器

,採用的是

Mark-Sweep 演算法

小夥伴注意了:並行指的是垃圾回收執行緒之間並行執行,併發指的是使用者執行緒和垃圾回收執行緒一起執行。

它的運作過程相對於前面幾種收集器來說要更復雜一些,整個過程分為4個步驟,包括:

初始標記(CMS initial mark)

併發標記(CMS concurrent mark)

重新標記(CMS remark)

併發清除(CMS concurrent sweep)

其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”

。初始標記僅僅只是標記一下 GC Roots 能直接關聯到的物件,速度很快,併發標記階段就是進行 GC Roots Tracing 的過程,而重新標記階段則是為了修正併發標記期間,因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。

由於整個過程中耗時最長的併發標記和併發清除過程中,收集器執行緒都可以與使用者執行緒一起工作,所以總體上來說,CMS收集器的記憶體回收過程是與使用者執行緒一起併發地執行。

老生常談的GC垃圾回收,讓我來“重新定義”,不信你還不明白

引數控制:-XX:+UseConcMarkSweepGC

G1

G1 收集器是當今收集器技術發展最前沿的成果,它是一款面向服務端應用的收集器,它能充分利用多 CPU、多核環境。因此它是一款並行與併發收集器,並且它可以

設定停頓時間

。與 CMS 收集器相比G1收集器有以下特點:

1、

空間整合

,G1 收集器採用

標記-整理

演算法,不會產生記憶體空間碎片。分配大物件時不會因為無法找到連續空間而提前觸發下一次 GC。

2、

可預測停頓

,這是 G1 的另一大優勢,降低停頓時間是 G1 和 CMS 的共同關注點,但 G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為N毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時 Java(RTSJ)的垃圾收集器的特徵了。

上面提到的垃圾收集器,收集的範圍都是整個新生代或者老年代,而 G1 不再是這樣。使用G1收集器時,Java 堆的記憶體佈局與其他收集器有很大差別,它將整個 Java 堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔閡了,它們都是一部分(可以不連續)Region 的集合。

工作過程如下幾步:

初始標記(Initial Marking):標記一下GC Roots能夠關聯的物件,並且修改TAMS的值,需要暫停使用者執行緒

併發標記(Concurrent Marking):從GC Roots進行可達性分析,找出存活的物件,與使用者執行緒併發執行

最終標記(Final Marking):修正在併發標記階段因為使用者程式的併發執行導致變動的資料,需暫停使用者執行緒

篩選回收(Live Data Counting and Evacuation):對各個 Region 的回收價值和成本進行排序,根據使用者所期望的GC停頓時間制定回收計劃 引數控制:-XX:+UseG1GC

垃圾收集器的選擇

垃圾收集器的選擇主要看兩個關鍵指標,

停頓時間

吞吐量

停頓時間:垃圾收集器進行垃圾回收終端應用執行響應的時間

吞吐量:執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間)

停頓時間越短就越適合需要和使用者互動的程式,良好的響應速度能提升使用者體驗;高吞吐量則可以高效地利用CPU時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務。

我們把上面介紹的垃圾收集器再分下類

併發收集器[停頓時間優先]:CMS、G1 ——>適用於相對時間有要求的場景,比如Web

並行收集器[吞吐量優先]:Parallel Scanvent 和 Parallel Old ——> 適用於科學計算、後臺處理等弱互動場景

序列收集器:Serial 和 Serial Old ——>適合記憶體比較小,嵌入式的裝置

最後再說幾句,JamesGosling 在1995年設計 Java 這個時候並沒有意識到這個語言將來會有更多的 Web 開發,停頓時間要比較小的場景,所有一開始是序列化,需要 Stop The World,這個放到現在來說是不敢想象的,試想你上著淘寶正嗨的時候,突然網頁打不開了,這你能忍?後來慢慢有了 Java8 預設的 PS+Parallel Old,這個是吞吐量優先的,後來 Java8、Java9 對時間有了更高的要求,就有了 CMS、G1 以及本文沒有介紹的 ZGC,所以隨著時代的進步垃圾收集器也在不斷地改造升級。