天天看點

垃圾回收之标記算法

掃描下方二維碼或者微信搜尋公衆号

菜鳥飛呀飛

,即可關注微信公衆号,閱讀更多

Spring源碼分析

Java并發程式設計

Netty源碼系列

MySQL工作原理

文章。
垃圾回收之标記算法

前言

作為 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 虛拟機》第三版。
垃圾回收之标記算法