垃圾回收之垃圾標記算法

封面

前言

作爲 Java 開發人員,其實是非常幸福的,因爲 JVM 的存在,使得 Java 開發人員不需要像 C 或者 C++開發人員那樣需要手動申請內存、釋放內存,這些資源申請、垃圾回收的操作,JVM 底層直接幫助我們全乾了。

這爲 Java 開發人員省去了不少事情,但同樣也使得像筆者這樣的菜鳥,對垃圾回收的概念越來越模糊,甚至壓根就不懂什麼是垃圾回收。然而現在的面試官越來越壞,逮着程序員的薄弱環節使勁懟,特別喜歡問 JVM 相關知識,尤其是 JVM 調優經驗、垃圾回收相關的知識。而作爲一名有理想的菜鳥,最近埋頭苦學了部分 JVM 知識,現在分享一波垃圾回收相關的知識。

垃圾回收

在 JVM 中,虛擬機規範將一大塊內存細分爲了很多不同的小區域,而 JVM 要想進行垃圾回收,首先得知道垃圾回收要回收的是哪些區域中的對象。下面這張圖相信大家已經見過很多次了,它是虛擬機規範中一張經典的 JVM 內存結構圖。圖中的運行時數據區包含 5 個部分:堆區、方法區、程序計數器、虛擬機棧、本地方法棧。其中程序計數器、虛擬機棧、本地方法棧是每個線程私有的區域,它們隨着線程的創建而生,隨着線程的死亡而消失,因此這部分區域不需要 JVM 單獨對它們進行垃圾回收。而堆區和方法區中存放的是對象、常量池、類信息等數據,這些數據是所有線程共享的,它們的生命週期不會伴隨着線程的生而生,死而死,它們需要 JVM 單獨進行垃圾回收。

JVM內存結構

知道了 JVM 中垃圾回收的目標區域,但是要對這些區域中的垃圾進行回收,JVM 首先得知道哪些對象是垃圾。而判斷一個對象是否是垃圾,通常有兩種算法:引用計數算法、可達性分析算法,下面將依次介紹這兩種算法。

引用計數算法

採用引用計數算法來判斷一個對象是否存活,其原理是:爲每一個對象分配一個計數器,當這個對象被另一個對象引用時,這個計數器就加一;當被另一個對象取消引用時,計數器就減一。當這個計數器的值爲零時,就表示當前對象沒有被任何對象所引用,那麼這個對象就可以被垃圾回收器進行回收了。

引用技術算法實現起來十分簡單,也十分高效。但是它有個致命的缺點,就是無法解決循環引用的問題。例如如下示例代碼:

public class ReferenceCountTest {

    private ReferenceCountTest reference;

    public static void main(String[] args) {
        ReferenceCountTest objA = new ReferenceCountTest();
        ReferenceCountTest objB = new ReferenceCountTest();
        objA.reference = objB;
        objB.reference = objA;

        objA = null;
        objB = null;
    }
}

示例代碼中,變量 objA 和 objB 相互之間循環引用,如果採用引用計數算法來判斷對象是否存活的話,即使我們將 objA 和 objB 設置爲 null 後,由於它們各自的引用計數器均爲 1,垃圾回收器會認爲 objA 和 objB 還有人在使用,因此不會回收 objA 和 objB。

正是因爲引用計數算法無法解決循環引用的問題,因此目前 Java 中的垃圾回收器均沒有使用引用計數算法來判斷一個對象是否存活,而是採用下面即將介紹的可達性分析算法。

可達性分析算法

可達性分析算法的實現思路是:將一系列被稱之爲“GC Roots”的根對象作爲起始節點,從這個根節點出發,通過引用關係向下尋找它可以到達的對象,尋找過程中經過的路線稱之爲引用鏈,一個系統中可以有多個根節點,也就是說 GC Roots 是一個節點的集合。如果一個對象無法通過任何一個 GC Roots 根節點找到,即 0 條引用鏈,那麼這個對象就不是存活對象了,後面在進行垃圾回收時,可以被垃圾收集器回收。

可達性分析算法

如果要使用可達性分析算法來進行垃圾標記,那麼就必須保證在整個可達性分析過程當中,系統必須處於一致性快照當中。什麼意思呢?就是在可達性分析過程中,不能有用戶線程更新對象間的引用關係,否則可達性分析算法的分析結果的準確性就無法保證了。因此在可達性分析算法的工作當中,會暫停所有的用戶線程,也就是”Stop The World“,簡稱 STW。

GC Roots

在可達性分析算法中提到了 GC Roots 這個概念,那麼在 Java 中,有哪些對象可以被作爲 GC Roots 呢?分別有如下幾種情況。

  1. 虛擬機棧中每個棧幀中局部變量表裏面的引用對象,如方法的入參,局部變量等。

  2. 本地方法棧中的引用對象。

  3. 方法區中類的靜態屬性引用的對象。

  4. 方法區中常量池引用的對象,如:字符串常量池引用的對象。

  5. 被關鍵字 synchronized 鎖住的對象。

  6. Java 虛擬機內部引用的對象,如:一些常駐的異常對象(NullPointerException、OutOfMemoryError),基本數據類型的 Class 對象,系統類加載器等。

  7. 反應 Java 虛擬機內部情況的 JMXBean、JVMTI 中註冊的回調、本地代碼緩存等。

  8. 除了這些固定的 GC Roots 外,根據用戶所選的垃圾收集器以及當前回收的內存區域不同,還可以有其他對象”臨時性“地加入,共同構成完整的 GC Roots 集合,比如:分代收集器和局部回收。

總結

本文主要介紹了 JVM 垃圾回收的作用區域,以及如何判斷一個對象是否是垃圾,通常可以通過引用計數法和可達性分析算法來判斷一個對象是否是垃圾,但是在目前 JVM 的垃圾收集器中,採用的都是可達性分析算法,因爲引用計數法無法解決循環依賴的問題。最後列舉了在可達性分析算法裏,Java 中哪些對象可以作爲 GC Roots。

垃圾回收通常會分爲兩個階段:垃圾標記階段和垃圾清除階段。而引用計數算法和可達性分析算法作用的是垃圾標記階段,後面的文章將會分享垃圾清除階段的相關算法。

參考

  • 周志明《深入理解 Java 虛擬機》第三版。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章