JVM - 垃圾回收概述

不能放棄治療發表於2020-12-16

我是清都山水郎,天教懶慢帶疏狂。曾批給露支風券,累奏流雲借月章。 詩萬首,酒千觴,幾曾著眼看侯王。玉樓金闕慵歸去,且插梅花醉洛陽。

在進行垃圾回收的時候,對於 JVM 而言,什麼物件才算是垃圾呢?如何判斷某些物件是垃圾呢?
很明顯的,已經沒有被使用的物件,就是垃圾。

引用計數法

引用計數法是用於判斷物件是垃圾的一種方式。
如果被其他物件引用,那麼物件的引用計數就會+1。當引用失效時,引用計數就-1。當引用等於0時,即表示物件已無用。
引用計數雖然實現簡單,但是無法處理迴圈引用的問題,也因此沒有被採用

迴圈引用

物件A引用物件B,物件B的引用+1;物件B引用物件A,物件A的引用+1。但是物件A,物件B沒有被使用。從上帝視角來看,A、B已經是無用物件了。但是由於引用計數不等於0,因此不會被認為是垃圾。

GC Root 可達性分析

JVM 使用 GC Root 可達性分析判斷物件是否為垃圾。
物件是否應該被回收,判斷條件如下

  1. Root 物件出發,如果物件可被訪問到,那麼此物件就不應該被回收。
  2. 物件被回收前,可執行一次 finalize() 方法,如果在 finalize() 方法中復活,則不會被回收。需要注意的是:finalize() 方法只會被一個物件執行一次

根物件定義

想必,你一定很疑惑,what is root object?
官方文件定義:

Garbage Collection Roots
A garbage collection root is an object that is accessible from outside the heap. The following reasons make an object a GC root:

  1. System Class
    Class loaded by bootstrap/system class loader. For example, everything from the rt.jar like java.util.* .
  2. JNI Local
    Local variable in native code, such as user defined JNI code or JVM internal code.
  3. JNI Global
    Global variable in native code, such as user defined JNI code or JVM internal code.
  4. Thread Block
    Object referred to from a currently active thread block.
  5. Thread
    A started, but not stopped, thread.
  6. Busy Monitor
    Everything that has called wait() or notify() or that is synchronized. For example, by calling synchronized(Object) or by entering a synchronized method. Static method means class, non-static method means object.
  7. Java Local
    Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.
  8. Native Stack
    In or out parameters in native code, such as user defined JNI code or JVM internal code. This is often the case as many methods have native parts and the objects handled as method parameters become GC roots. For example, parameters used for file/network I/O methods or reflection.
  9. Finalizable
    An object which is in a queue awaiting its finalizer to be run.
  10. Unfinalized
    An object which has a finalize method, but has not been finalized and is not yet on the finalizer queue.
  11. Unreachable
    An object which is unreachable from any other root, but has been marked as a root by MAT to retain objects which otherwise would not be included in the analysis.
  12. Java Stack Frame
    A Java stack frame, holding local variables. Only generated when the dump is parsed with the preference set to treat Java stack frames as objects.
  13. Unknown
    An object of unknown root type. Some dumps, such as IBM Portable Heap Dump files, do not have root information. For these dumps the MAT parser marks objects which are have no inbound references or are unreachable from any other root as roots of this type. This ensures that MAT retains all the objects in the dump.

其他參考:知乎回答

通過 finalize() 復活

public class FinalizeTest {

    public static FinalizeTest obj;

    public static void main(String[] args) throws InterruptedException {
        obj = new FinalizeTest();
        obj = null;
        System.gc();
        Thread.sleep(1000);
        if (obj == null) {
            System.out.println("obj = null");
        } else {
            System.out.println("obj 可用");
        }
        System.out.println("第2次GC");
        obj = null;
        System.gc();
        Thread.sleep(1000);
        if (obj == null) {
            System.out.println("obj = null");
        } else {
            System.out.println("obj 可用");
        }
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("物件復活");
        obj = this;
    }
}

以上程式依次列印如下:

物件復活
obj 可用
第2次GC
obj = null

finalize() 對於一個物件而言,只會在要被回收時,才會呼叫一次。在第 1 次 GC 時,通過 finalize 復活,第二次 GC 沒有呼叫 finalize,順利被回收。

糟糕的 finalize

finalize() 方法由 FinalizerThread 執行緒執行。一個物件若重寫 finalize() 方法,那麼在被回收時,會被加入到 FinalizerThread 的執行佇列 FinalReference 中。
此時物件會被 Reference 中的 referent 欄位所指向,因此物件會重新變成可達。如果 finalize() 過於耗時,那麼會阻礙 GC 的正常回收。
下面就展示一段糟糕的 finalize 實現。
使用 -Xms10m -Xmx10m 執行 一段時間後,會報 OOM

public class FinalizeReferenceTest {

    public static void main(String[] args) {
        for (int i = 0; i < 50000; i++) {
            LF lf = new LF();
        }
    }

    public static class LF {

        private byte[] bs = new byte[1024];

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            try {
                Thread.sleep(1000);

            } catch (Exception e) {

            }
        }

    }
}

finalize()的實現中, Thread.sleep(1000) 表示在執行耗時的任務。FinalizerThread 在執行 finalize() 方法時,是從佇列中一項一項取出來執行的, finalize() 執行過久,導致大量物件一直被引用指向,無法正常回收。

如果將 LE#finalize() 方法去掉,程式會正常執行。

引用佇列

JAVA 中根據物件引用強弱,有 4 大引用物件:強引用,軟引用(SoftReference),弱引用(WeakReference),虛引用(PhantomReference)。
正常建立的物件為強引用物件。對於 軟引用,弱引用,虛引用,當物件被回收時,會被加入到引用佇列中,可以通過引用佇列來監聽到物件被回收。

下面的 demo 展示了,當物件被回收後,通過引用佇列進行監控

public class ReferenceQueueTest {

    static ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();

    public static void main(String[] args) throws InterruptedException {
        monitorSoftReference();
        addSoftReference();
    }

    private static void addSoftReference() throws InterruptedException {
        Object o = new Object();
        ObjectWeakReference reference = new ObjectWeakReference(o, referenceQueue);
        o = null;
        System.gc();
        Thread.sleep(1000);
    }

    private static void monitorSoftReference() throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                ObjectWeakReference reference = null;
                try {
                    reference = (ObjectWeakReference) referenceQueue.remove();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (reference != null) {
                    System.out.println("物件被回收");
                }
            }
        });

        thread.start();
    }

    public static class ObjectWeakReference extends WeakReference<Object> {

        public ObjectWeakReference(Object referent, ReferenceQueue<? super Object> q) {
            super(referent, q);
        }
    }
}

參考

Garbage Collection Roots
JAVA垃圾回收,執行緒棧中哪些東西作為GC RootS?
ReferenceQueue的使用
《實戰Java虛擬機器:JVM故障診斷與效能優化(第2版)》

既然選擇了遠方,即使天寒地凍,路遙馬亡,我本就一無所有,又有何懼。

‘深入交流’

在這裡插入圖片描述
在這裡插入圖片描述

相關文章