23. java虛擬機總結-和OOM相關的 (六)

垃圾回收算法

可達性分析法(根搜索算法,GC ROOTS)

從 GC Roots 向下追溯、搜索,會產生一個叫作 Reference Chain 的鏈條。當一個對象不能和任何一個 GC Root 產生關係時,就會被無情的誅殺掉。
如圖所示,Obj5、Obj6、Obj7,由於不能和 GC Root 產生關聯,發生 GC 時,就會被摧毀。


GC Roots 有哪些

GC Roots 是一組必須活躍的引用。用通俗的話來說,就是程序接下來通過直接引用或者間接引用,能夠訪問到的潛在被使用的對象。
GC Roots 包括:

    Java 線程中,當前所有正在被調用的方法的引用類型參數、局部變量、臨時值等。也就是與我們棧幀相關的各種引用。
    所有當前被加載的 Java 類。
    Java 類的引用類型靜態變量。
    運行時常量池裏的引用類型常量(String 或 Class 類型)。
    JVM 內部數據結構的一些引用,比如 sun.jvm.hotspot.memory.Universe 類。
    用於同步的監控對象,比如調用了對象的 wait() 方法。
    JNI handles,包括 global handles 和 local handles。

這些 GC Roots 大體可以分爲三大類,下面這種說法更加好記一些:

    1  活動線程相關的各種引用。(虛擬機棧(棧幀中的本地變量表)中引用的對象。)
    2  類的靜態變量的引用。(方法區中類靜態屬性引用的對象。方法區中常量引用的對象。)
    3  JNI 引用。(本地方法棧中JNI(即一般說的Native方法)引用的對象。)

有兩個注意點:

1. 我們這裏說的是活躍的引用,而不是對象,對象是不能作爲 GC Roots 的。
2. GC 過程是找出所有活對象,並把其餘空間認定爲“無用”;而不是找出所有死掉的對象,並回收它們佔用的空間。所以,哪怕 JVM 的堆非常的大,基於 tracing 的 GC 方式,回收速度也會非常快。

引用計數法

因爲有循環依賴的硬傷,現在主流的 JVM,沒有一個是採用引用計數法來實現 GC 的,所以我們大體瞭解一下就可以。引用計數法是在對象頭裏維護一個 counter 計數器,被引用一次數量 +1,引用失效記數 -1。計數器爲 0 時,就被認爲無效。你現在可以忘掉引用計數的方式了

引用類型

面試題:能夠找到 Reference Chain 的對象,就一定會存活麼?
不一定,看引用類型

強引用 Strong references

即使程序會異常終止,這種對象也不會被回收的。比如下邊,這種情況我們需要手動將其引用關係斷開纔會被回收

Object obj = new Object()
軟引用 Soft references

軟引用用於維護一些可有可無的對象。在內存足夠的時候,軟引用對象不會被回收,只有在內存不足時,系統則會回收軟引用對象,這種特性非常適合用在緩存技術上。比如網頁緩存、圖片緩存等。
有一個相關的 JVM 參數。它的意思是:每 MB 堆空閒空間中 SoftReference 的存活時間。這個值的默認時間是1秒(1000)。

-XX:SoftRefLRUPolicyMSPerMB=<N>

這裏要特別說明的是,網絡上一些流傳的優化方法,即把這個值設置成 0,其實是錯誤的,這樣容易引發故障,感興趣的話你可以自行搜索一下。

這種比較偏門的優化手段,除非在你對其原理相當瞭解的情況下,才能設置一些比較特殊的值。比如 0 值,無限大等,這種值在 JVM 的設置中,最好不要發生。

弱引用 Weak references

當 JVM 進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。弱引用擁有更短的生命週期
它的應用場景和軟引用類似,可以在一些對內存更加敏感的系統裏採用

虛引用 Phantom References

形同虛設的引用,在現實場景中用的不是很多。虛引用必須和引用隊列(ReferenceQueue)聯合使用。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。

Object  object = new Object();
ReferenceQueue queue = new ReferenceQueue();
// 虛引用,必須與一個引用隊列關聯
PhantomReference pr = new PhantomReference(object, queue);

虛引用主要用來跟蹤對象被垃圾回收的活動。
當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象之前,把這個虛引用加入到與之關聯的引用隊列中。

程序如果發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的內存被回收之前採取必要的行動。

下面的方法,就是一個用於監控 GC 發生的例子。

private static void startMonitoring(ReferenceQueue<MyObject> referenceQueue, Reference<MyObject> ref) {
     ExecutorService ex = Executors.newSingleThreadExecutor();
     ex.execute(() -> {
         while (referenceQueue.poll()!=ref) {
             //don't hang forever
             if(finishFlag){
                 break;
            }
        }
         System.out.println("-- ref gc'ed --");

    });
     ex.shutdown();
}

基於虛引用,有一個更加優雅的實現方式,那就是 Java 9 以後新加入的 Cleaner,用來替代 Object 類的 finalizer 方法。

典型 OOM 場景

內存區域有哪些會發生 OOM 呢?我們可以從內存區域劃分圖上,看一下彩色部分。


除了程序計數器,其他區域都有OOM溢出的可能。但是最常見的還是發生在堆上。


OOM 到底是什麼引起的呢?有幾個原因:

內存的容量太小了,需要擴容,或者需要調整堆的空間。
錯誤的引用方式,發生了內存泄漏。沒有及時的切斷與 GC Roots 的關係。比如線程池裏的線程,在複用的情況下忘記清理 ThreadLocal 的內容。
接口沒有進行範圍校驗,外部傳參超出範圍。比如數據庫查詢時的每頁條數等。
對堆外內存無限制的使用。這種情況一旦發生更加嚴重,會造成操作系統內存耗盡。

典型的內存泄漏場景,原因在於對象沒有及時的釋放自己的引用。比如一個局部變量,被外部的靜態集合引用。


你在平常寫代碼時,一定要注意這種情況,千萬不要爲了方便把對象到處引用。即使引用了,也要在合適時機進行手動清理。關於這部分的問題根源排查,我們將在實踐課程中詳細介紹。

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