「進階」JVM 中 GC(垃圾回收器)的運行機制

前言

GC 即 Garbage Collection,中文 意思“垃圾回收”,在有GC之前,我們手動去管理內存,如果你忘記標記某一處已經不再使用的內存,那麼這塊內存將永遠不會被系統回收,也就是常說的 “內存洩露”。

以下所有的 GC 介紹,全部基於主流 JVM 虛擬機 Hotspot。

GC 是如何判斷一個對象是存亡?

GC判斷一個對象存活或死亡就是判斷這個對象還存不存在它的引用,常見的兩種方式如下

  • 引用計數法

每個對象從創建開始,都會維護一個引用計數器,每當引用一次,那麼計數器增加1,引用失效一次,那麼計數器減去1,這樣實現優點是高效、簡單,但是缺點也很明顯:無法解決循環依賴,比如下面的代碼,雖然 A引用B,B引用A,但是就沒有其他地方引用了,因此它們是無效引用,造成內存洩露。Java 自然不會選擇這種方式作為判斷方式。

<code>    A=B
    B=A/<code>
  • 引用鏈(可達性分析法)代表語言:Java、C#
    相比引用計數法,可達性分析法就複雜的多,也安全的多了,分為三步

1.可達性分析

將一系列的 GC Roots 對象作為起點,開始向下搜索。可作為 GC Root 的起點有

  • Java 虛擬機棧(棧楨本地變量表中)引用的對象
  • 本地方法棧中JNI(也就是常說的 Native 方法)
  • 方法中的常量、類靜態屬性引用的對象

注意向下搜索的路徑就是引用鏈

為了方便理解,我畫了下面的圖片

「進階」JVM 中 GC(垃圾回收器)的運行機制

特別注意: 可達性分析僅僅是判斷對象是否可達,但還不足以判斷對象是否存活或者死亡。可達性分析中判斷為不可達的對象,只是被判刑 ≠ 死亡。

不可達對象會存放在 「即將回收」集合中,要判斷一個對象是否真正的死亡,還需要經過下面的兩個步驟。

2.第一次標記 & 篩選

可達性分析中標記為不可達的對象,會經歷第一次篩選。

篩選標準:判斷對象是否需要執行 finalize() 方法,若有必要執行,則篩選進行下一階段分析,若沒必要執行,那麼該對象判定為死亡,不篩選,等待系統回收。

當對象無 finalize() 方法,或者 finalize() 方法已經被虛擬機調用過一次的情況下,那麼被標記為沒必要執行,等待回收。

3.第二次標記 & 篩選

當對象經過了第一次篩選後沒有被回收,將進行第二次篩選。

該對象會被放在一個 F-Queue 的隊列中,並由虛擬機自動創建一個名為 Finalizer 的低優先級的線程去執行隊列中所有對象的 finalize 方法,這裡需要注意的是,finalize 方法只會被執行一次,並且 JVM 並不承諾 finalize 會執行完畢,原因是為了防止 finalize 執行時間過長或者停止執行,導致的內存溢出。

篩選標準:在執行 finalize 方法的過程中,如果該對象依舊沒有和 GC Root 關聯起來,那麼該對象被判斷為死亡,留在即將回收集合,等待回收。

Full GC 和 Partial GC

簡單理解就是,全局GC(Full GC)和局部GC(Partial GC),分別看一下:

Partial GC(局部GC)

Young GC :只收集 Young Gen(年輕代)的 GC, Young GC 還有一個叫法 叫 Minor GC。

old GC : 只收集 Old Gen(年老代) 的GC 只有垃圾回收器 CMS 的 concament colletton 有這個模式。

mixed GC : 收集整個Young GC的GC和部分的old Gen的GC,只有垃圾回收器 G1 有這個模式。

Full GC

Full GC是對整個堆來說的,執行Full GC 的時候會回收所有代,包括永久代、年老代、年輕代等等所有的 GC。Full GC 的觸發條件有以下幾種

System.gc()方法的調用

此方法的調用是建議JVM進行Full GC,雖然只是建議而非一定,但很多情況下它會觸發 Full GC,從而增加Full GC的頻率,也即增加了間歇性停頓的次數。強烈建議能不使用此方法就別使用,讓虛擬機自己去管理它的內存,可通過通過-XX:+ DisableExplicitGC來禁止RMI(Java遠程方法調用)調用System.gc。

老年代空間不足

舊生代空間只有在新生代對象轉入及創建為大對象、大數組時才會出現不足的現象,當執行Full GC後空間仍然不足,則拋出如下錯誤: java.lang.OutOfMemoryError: Java heap space 為避免以上兩種狀況引起的FullGC,調優時應儘量做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要創建過大的對象及數組。

方法區空間不足

JVM規範中運行時數據區域中的方法區,在HotSpot虛擬機中又被習慣稱為永生代或者永生區,Permanet Generation中存放的為一些class的信息、常量、靜態變量等數據,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被佔滿,在未配置為採用CMS GC的情況下也會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會拋出如下錯誤信息:java.lang.OutOfMemoryError: PermGen space為避免Perm Gen佔滿造成Full GC現象,可採用的方法為增大Perm Gen空間或轉為使用CMS GC。

通過Minor GC後進入老年代的平均大小大於老年代的可用內存

如果發現統計數據說之前Minor GC的平均晉升大小比目前old gen剩餘的空間大,則不會觸發Minor GC而是轉為觸發full GC

為了方便理解上述枯燥又無味的理論,我寫了幾行代碼,實驗了一下

<code>/**
 * GCRoots 測試:虛擬機棧(棧幀中的局部變量)中引用的對象作為GCRoots
 * -Xms1024m -Xmx1024m -Xmn512m -XX:+PrintGCDetails
 * 


 * 擴展:虛擬機棧中存放了編譯器可知的八種基本數據類型,對象引用,returnAddress類型(指向了一條字節碼指令的地址)
 *
 * @author wangjie
 */
public class TestGCRoots01 {
    private int _10MB = 10 * 1024 * 1024;
    private byte[] memory = new byte[8 * _10MB];

    public static void main(String[] args) {
        method01();
        System.out.println("返回main方法");
        System.gc();
        System.out.println("第二次GC完成");
    }

    public static void method01() {
        TestGCRoots01 t = new TestGCRoots01();
        System.gc();
        System.out.println("第一次GC完成");


    }
}

/<code>

當運行上述代碼控制檯會輸出如下內容

「進階」JVM 中 GC(垃圾回收器)的運行機制

java private byte[] memory = new byte[8 * _10MB]; ¨G2G java method01(); TestGCRoots01 t = new TestGCRoots01(); ¨G3G java System.gc(); System.out.println("第一次GC完成"); ¨G4G java System.out.println("返回main方法"); System.gc(); System.out.println("第二次GC完成");

至此,GC 的基礎知識你應該瞭解了,但是這篇文章僅僅簡單分析了一下 GC 和 JVM 的關係,並不涉及到引用鏈,如果對你理解 GC 有幫助,點贊轉發是對我最大的支持。

另外,我為你找到了以下資料,並翻譯成了中文供你查閱

  • 關於 GC 的 Oracle給出的文檔


分享到:


相關文章: