一文帶你瞭解 JVM 的垃圾回收機制

Howie_Y發表於2019-02-17

垃圾收集是一項自動化的技術。可是當我們排查各種記憶體問題,或者當垃圾收整合為系統達到更高併發量的瓶頸時,我們需要對這些原本自動化的技術進行必要的監控和調節,所有我們很有必要學習 JVM 的垃圾收集機制。

一. 什麼區域需要回收?為什麼需要回收?

垃圾回收也稱為 GC (Garbage Collection),或者可以稱為垃圾收集。

對於執行緒私有的三個部分(程式計數器,虛擬機器棧和本地方法棧),不怎麼需要考慮回收問題,原因:

  • 在方法結束或執行緒結束時,記憶體便跟著回收走了,他們隨執行緒而生,執行緒而滅
  • 而且對於棧來說,每個棧幀中分配多少記憶體基本在類結構確定下來的時候就已經確定了。

對於執行緒共享的兩個部分(堆和方法區,主要是堆),需要考慮回收,原因:

  • 程式只有處於執行的時候才能知道會建立哪些物件
  • 記憶體的分配和回收都是動態的

對於堆來說,如果不進行垃圾回收,記憶體遲早都會被消耗空,因為我們在不斷的分配記憶體空間而不進行回收。除非記憶體無限大,我們可以任性的分配而不回收,但是事實並非如此。所以,垃圾回收是必須的。

二. 如何判斷物件是否存活?

在對堆進行垃圾回收前,必須確定每個物件是否還存活著;而這個判斷過程主要是以下兩種演算法

1. 引用計數演算法

給物件新增一個引用計數器,每當有一個地方引用它,計數器值加 1;每當引用失效,計數器減 1;當某個物件任何時候計數器值都是 0 時,這個物件就“死”了

缺點:很難解決物件之間迴圈引用的問題,也因此主流的 JVM 都沒有使用該演算法來管理記憶體

public class GCTest {
    Object object;

    private static void test() {
        GCTest test1 = new GCTest();
        GCTest test2 = new GCTest();
        
        test1.object = test2;
        test2.object = test1;  
        
        test1 = null;
        test2 = null;
    }
}
複製程式碼

類似這樣的例子,由於 test1 和 test2 相互引用對方,即使這兩個物件已經不可能再被訪問到(兩個變數都已經指向 null),引用計數演算法也無法讓垃圾收集器對它們進行回收

2. 可達性分析演算法

這是主流 JVM 使用的回收演算法

通過一系列稱為 GC Roots 的物件作為起始點,從這些節點向下搜尋,如果一個物件與 GC Roots 物件有引用鏈相連,說明物件可用;反之,物件不可用

一文帶你瞭解 JVM 的垃圾回收機制

如上圖的物件 4,5,6 則需要被回收

而可作為 GC Roots 的物件包括下面幾種:

  • 虛擬機器棧中引用的物件
  • 方法區中類靜態屬性引用的物件以及常量引用的物件
  • 本地方法棧中 native 方法引用的物件

三. 方法區的回收

方法區很少進行垃圾回收,甚至可以不要求虛擬機器對方法區進行回收,因為能回收的東西很少,因此也叫做永久代

在永久代,主要回收兩個內容:廢棄常量,無用的類。如果在永久代發生垃圾回收,那麼這兩個內容就會被清理出去(當然大多數情況下不會去對永久代進行垃圾回收操作)

四. 引用的型別

不論使用什麼演算法判斷物件的存活情況,這都和“引用”息息相關

1. 強引用

  • 簡單來說就是類似 Object o = new Objrct 這樣的引用
  • 只要這樣的關係還存在,就永不會被回收

2. 軟引用

  • 還有用但非必需的物件
  • 如果將要發生記憶體溢位,則進行第二次回收,將這些軟引用物件回收;之後如果還是沒有足夠的記憶體,再丟擲記憶體溢位異常

可以通過 SoftReference 實現,這是 sf 對 obj 有軟應用

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
sf.get(); //如果 obj 被標記為需要被回收,則會返回null
複製程式碼

SoftReference 可以用來實現類似快取的功能

3. 弱引用

  • 非必需物件,比軟引用強度更弱
  • 當垃圾收集器工作,它們就會被回收

可以通過 WeakReference 實現,通常用於監控物件是否已經被垃圾回收器標記為即將回收的垃圾

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
wf.get();
wf.isEnQueued();//返回是否被垃圾回收器標記為即將回收的垃圾
複製程式碼

4. 虛引用

  • 最弱的引用關係
  • 無法通過虛引用取得一個物件的例項

可以通過 PhantomReference 實現,主要用於檢測物件是否已經從記憶體中刪除

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
pf.get();//永遠返回null
pf.isEnQueued();//返回是否從記憶體中已經刪除
複製程式碼

五. 垃圾回收演算法

1. 標記——清除演算法

先標記出要回收的物件,標記完成後統一清除這些物件。

缺點:

  • 效率太低,標記和清除兩個操作的效率都不高
  • 清除後會產生大量不連續的記憶體空間,或者稱為記憶體碎片。而如果我們需要分配一些較大的物件的時候,無法找到足夠的連續空間是一件很麻煩的事情。

2. 複製演算法

將記憶體劃分為等大的兩塊,一次只使用一塊。當其中一塊用完了,就把裡面的存活的物件全部複製到另一塊去,然後將已經使用的那一大塊一次性全部清理掉

  • 優點:實現簡單,執行高效,也不用擔心碎片問題
  • 缺點:將記憶體縮小了一半,代價有點高

3. 標記——整理演算法

標記——清除演算法的改進,在完成標記之後,讓所有存活的物件都向一端移動,然後直接清理掉邊界以外的記憶體,故名叫“整理”。

4. 分代收集演算法

當前商業虛擬機器的垃圾收集都採用「分代收集」演算法

分代收集演算法將記憶體劃分為新生代和老年代:

  • 在新生代中,每次垃圾收集都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集

  • 在老年代中因為物件存活率高、沒有額外空間對它進行擔保,就必須採用「標記 — 清理」或者「標記 — 整理」演算法來回收。

相關文章