Java 引用類型

Java引用概述

StrongReference(強引用) 不存在這個類 默認實現

Java.lang.ref提供了與 Java垃圾回收器密切相關的引用類。SoftReference(軟引用),WeakReference(弱引用),PhantomReference(虛引用)。這四種引用的強度按照上面的順序依次減弱.

一、強引用(StrongReference)

-就是指在程序代碼中普遍存在的,類似Object obj = new Object()這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。 我們一般都是使用強引用來對對象進行引用。如:

String tag = new String("T");

此處的 tag 引用就稱之為強引用。而強引用有以下特征:

  • 強引用可以直接訪問目標對象。
  • 強引用所指向的對象在任何時候都不會被系統回收。
  • 強引用可能導致內存泄漏

只有顯式地設置o為null,或超出對象的生命周期范圍,則gc認為該對象不存在引用,這時就可以回收這個對象。具體什么時候收集這要取決于gc的算法。

二、軟引用(SoftReference)

是用來描述一些還有用但并非必須的對象。對于軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。
對于軟引用關聯著的對象,如果內存充足,則垃圾回收器不會回收該對象,如果內存不夠了,就會回收這些對象的內存。在 JDK 1.2 之后,提供了 SoftReference 類來實現軟引用。軟引用可用來實現內存敏感的高速緩存。軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。

demo
public class SoftRefTest {
    private static ReferenceQueue<MyObject> softQueue = new ReferenceQueue<>();

    public static class MyObject {

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("MyObject's finalize called");
        }

        @Override
        public String toString() {
            return "I am MyObject";
        }
    }

    public static class CheckRefQueue implements Runnable {
        Reference<MyObject> obj = null;

        @Override
        public void run() {
            try {
                obj = (Reference<MyObject>) softQueue.remove();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (obj != null) {
                System.out.println("Object for SoftReference is " + obj.get());
            }
        }
    }

//    -Xmx5M -XX:+PrintGCDetails
    public static void main(String[] args) {
        MyObject object = new MyObject();
        SoftReference<MyObject> softRef = new SoftReference<>(object, softQueue);
        new Thread(new CheckRefQueue()).start();

        object = null;    //刪除強引用
        System.gc();
        System.out.println("After GC: Soft Get= " + softRef.get());
        System.out.println("分配大塊內存");
        byte[] b = new byte[5 * 1024 * 928];
        System.out.println("After new byte[]:Soft Get= " + softRef.get());
        System.gc();
    }
}

out:

[GC (Allocation Failure) [PSYoungGen: 1024K->481K(1536K)] 1024K->513K(5632K), 0.0038919 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (System.gc()) [PSYoungGen: 682K->481K(1536K)] 714K->537K(5632K), 0.0035611 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 481K->0K(1536K)] [ParOldGen: 56K->498K(4096K)] 537K->498K(5632K), [Metaspace: 2717K->2717K(1056768K)], 0.0068592 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
After GC: Soft Get= I am MyObject
分配大塊內存
[GC (Allocation Failure) [PSYoungGen: 40K->32K(1536K)] 539K->530K(5632K), 0.0006437 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 32K->64K(1536K)] 530K->562K(5632K), 0.0004163 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 64K->0K(1536K)] [ParOldGen: 498K->488K(4096K)] 562K->488K(5632K), [Metaspace: 2718K->2718K(1056768K)], 0.0070447 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 488K->488K(5632K), 0.0006157 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
[PSYoungGen: 0K->0K(1536K)] [ParOldGen: 488K->476K(4096K)] 488K->476K(5632K), [Metaspace: 2718K->2718K(1056768K)], 0.0057286 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
MyObject's finalize called
Object for SoftReference is null
Heap
 PSYoungGen      total 1536K, used 61K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 1024K, 6% used [0x00000007bfe00000,0x00000007bfe0f740,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
  to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
 ParOldGen       total 4096K, used 476K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
  object space 4096K, 11% used [0x00000007bfa00000,0x00000007bfa77338,0x00000007bfe00000)
 Metaspace       used 2749K, capacity 4490K, committed 4864K, reserved 1056768K
  class space    used 294K, capacity 386K, committed 512K, reserved 1048576K
    at com.gxgeek.javabasic.ref.SoftRefTest.main(SoftRefTest.java:55)

構造MyObject對象,并將其賦值給object變量,構成強引用。然后使用SoftReference構造這個MyObject對象的軟引用softRef,并注冊到softQueue引用隊列。當softRef被回收時,會被加入softQueue隊列。設置obj=null,刪除這個強引用,因此,系統內對MyObject對象的引用只剩下軟引用。此時,顯示調用GC,通過軟引用的get()方法,取得MyObject對象的引用,發現對象并未被回收,這說明GC在內存充足的情況下,不會回收軟引用對象。
接著,請求一塊大的堆空間,這個操作會使系統堆內存使用緊張,從而產生新一輪的GC。在這次GC后,softRef.get()不再返回MyObject對象,而是返回null,說明在系統內存緊張的情況下,軟引用被回收。軟引用被回收時,會被加入注冊的引用隊列。

二、弱引用(SoftReference)

WeakReference 是弱于 SoftReference 的引用類型。弱引用的特性和基本與軟引用相似,區別就在于弱引用所指向的對象只要進行系統垃圾回收,不管內存使用情況如何,永遠對其進行回收(get() 方法返回 null)。
弱引用有以下特征:

  • 弱引用使用 get()方法取得對象的強引用從而訪問目標對象。
  • 一旦系統內存回收,無論內存是否緊張,弱引用指向的對象都會被回收。
  • 弱引用也可以避免 Heap 內存不足所導致的異常。

public class WeakRefTest {
    private static ReferenceQueue<MyObject> weakQueue = new ReferenceQueue<>();

    public static class MyObject {

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("MyObject's finalize called");
        }

        @Override
        public String toString() {
            return "I am MyObject";
        }
    }

    public static class CheckRefQueue implements Runnable {
        Reference<MyObject> obj = null;

        @Override
        public void run() {
            try {
                obj = (Reference<MyObject>) weakQueue.remove();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (obj != null) {
                System.out.println("刪除的弱引用為:" + obj + "  but獲取弱引用的對象obj.get()=" + obj.get());
            }
        }
    }

    public static void main(String[] args) {
        MyObject object = new MyObject();
        Reference<MyObject> weakRef = new WeakReference<>(object, weakQueue);
        System.out.println("創建的弱引用為:" + weakRef);
        new Thread(new CheckRefQueue()).start();

        object = null;
        System.out.println("Before GC: Weak Get= " + weakRef.get());
        System.gc();
        System.out.println("After GC: Weak Get= " + weakRef.get());
    }
}

out:

創建的弱引用為:java.lang.ref.WeakReference@2503dbd3
Before GC: Weak Get= I am MyObject
After GC: Weak Get= null
MyObject's finalize called
刪除的弱引用為:java.lang.ref.WeakReference@2503dbd3  but獲取弱引用的對象obj.get()=null

可以看到,在GC之前,弱引用對象并未被垃圾回收器發現,因此通過 weakRef.get()可以獲取對應的對象引用。但是只要進行垃圾回收,弱引用一旦被發現,便會立即被回收,并加入注冊引用隊列中。此時再試圖通過weakRef.get()獲取對象的引用就會失敗。

Java 虛引用


public class PhantomRefTest {
    private static ReferenceQueue<MyObject> phanQueue = new ReferenceQueue<>();

    public static class MyObject {

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("MyObject's finalize called");
        }

        @Override
        public String toString() {
            return "I am MyObject";
        }
    }

    public static class CheckRefQueue implements Runnable {
        Reference<MyObject> obj = null;

        @Override
        public void run() {
            try {
                obj = (Reference<MyObject>) phanQueue.remove();
                System.out.println("刪除的虛引用為:" + obj + "  but獲取虛引用的對象obj.get()=" + obj.get());
                System.exit(0);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyObject object = new MyObject();
        Reference<MyObject> phanRef = new PhantomReference<>(object, phanQueue);
        System.out.println("創建的虛引用為:" + phanRef);
        new Thread(new CheckRefQueue()).start();

        object = null;
        TimeUnit.SECONDS.sleep(1);
        int i = 1;
        while (true) {
            System.out.println("第" + i++ + "次gc");
            System.gc();
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

out:

創建的虛引用為:java.lang.ref.PhantomReference@2503dbd3
第1次gc
MyObject's finalize called
第2次gc
刪除的虛引用為:java.lang.ref.PhantomReference@2503dbd3  but獲取虛引用的對象obj.get()=null

PhantomReference是所有“弱引用”中最弱的引用類型。不同于軟引用和弱引用,虛引用無法通過 get() 方法來取得目標對象的強引用從而使用目標對象,觀察源碼可以發現 get() 被重寫為永遠返回 null。

那虛引用到底有什么作用?其實虛引用主要被用來 跟蹤對象被垃圾回收的狀態,通過查看引用隊列中是否包含對象所對應的虛引用來判斷它是否 即將被垃圾回收,從而采取行動。它并不被期待用來取得目標對象的引用,而目標對象被回收前,它的引用會被放入一個 ReferenceQueue 對象中,從而達到跟蹤對象垃圾回收的作用。

引用類型 取得目標對象方式 垃圾回收條件 是否可能內存泄漏
強引用 直接調用 不回收 可能
軟引用 通過 get() 方法 視內存情況回收 不可能
弱引用 通過 get() 方法 永遠回收 不可能
虛引用 無法取得 不回收 可能

FinalReference 以及 Finzlizer

st=>start: Reference
e=>end: Finalizer
op=>operation: FinalReference

st->op->e

FinalReference 作為 java.lang.ref 里的一個不能被公開訪問的類,又起到了一個什么樣的作用呢?作為他的子類, Finalizer 又在垃圾回收機制里扮演了怎么樣的角色呢?

實際上,FinalReference 代表的正是 Java 中的強引用,如這樣的代碼 :

Bean bean = new Bean();

在虛擬機的實現過程中,實際采用了 FinalReference 類對其進行引用。而 Finalizer,除了作為一個實現類外,更是在虛擬機中實現一個 FinalizerThread,以使虛擬機能夠在所有的強引用被解除后實現內存清理。

讓我們來看看 Finalizer 是如何工作的。首先,通過聲明 FinalizerThread,并將該線程實例化,設置為守護線程后,加入系統線程中去。

    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread finalizer = new FinalizerThread(tg);
        finalizer.setPriority(Thread.MAX_PRIORITY - 2);
        finalizer.setDaemon(true);
        finalizer.start();
    }

在 GC 的過程中,當一個強引用被釋放,由系統垃圾收集器標記后的對象,會被加入 Finalizer 對象中的 ReferenceQueue 中去,并調用 Finalizer.runFinalizer() 來執行對象的 finalize 方法。

    private static class FinalizerThread extends Thread {
        private volatile boolean running;
        FinalizerThread(ThreadGroup g) {
            super(g, "Finalizer");
        }
        public void run() {
            if (running)
                return;

            // Finalizer thread starts before System.initializeSystemClass
            // is called.  Wait until JavaLangAccess is available
            while (!VM.isBooted()) {
                // delay until VM completes initialization
                try {
                    VM.awaitBooted();
                } catch (InterruptedException x) {
                    // ignore and continue
                }
            }
            final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
            running = true;
            for (;;) {
                try {
                    Finalizer f = (Finalizer)queue.remove();
                    f.runFinalizer(jla);
                } catch (InterruptedException x) {
                    // ignore and continue
                }
            }
        }
    }
    
    
    private void runFinalizer(JavaLangAccess jla) {
        synchronized (this) {
            if (hasBeenFinalized()) return;
            remove();
        }
        try {
            Object finalizee = this.get();
            if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
                jla.invokeFinalize(finalizee);

                /* Clear stack slot containing this variable, to decrease
                   the chances of false retention with a conservative GC */
                finalizee = null;
            }
        } catch (Throwable x) { }
        super.clear();
    }
    

注意,標記處所調用的 invokeFinalizeMethod 為 native 方法,由于 finalize 方法在 Object 類中被聲明為 protected,這里必須采用 native 方法才能調用。隨后通過將本地強引用設置為空,以便使垃圾回收器清理內存。
可以看到,通過這樣的方法,Java 將四種引用對象類型:軟引用 (SoftReference),弱引用 (WeakReference),強引用 (FinalReference),虛引用 (PhantomReference) 平等地對待,并在垃圾回收器中進行統一調度和管理。

何時注冊(實例化FinalReference)

JVM在類加載的時候會遍歷當前類的所有方法,包括父類的方法,只要有一個參數為空且返回void的非空finalize方法就認為這個類在創建對象的時候需要進行注冊。

對象的創建其實是被拆分成多個步驟,注冊的時機可以在為對象分配好內存空間后,也可以在構造函數返回之前,這個點由-XX:-RegisterFinalizersAtInit控制,這個參數默認為true,即:在構造函數返回之前調用。注冊入口是Finalizer的register()方法。

GC回收問題
  • 對象因為Finalizer的引用而變成了一個臨時的強引用,即使沒有其他的強引用,還是無法立即被回收;

  • 對象至少經歷兩次GC才能被回收,因為只有在FinalizerThread執行完了f對象的finalize方法的情況下才有可能被下次GC回收,而有可能期間已經經歷過多次GC了,但是一直還沒執行對象的finalize方法;

  • CPU資源比較稀缺的情況下FinalizerThread線程有可能因為優先級比較低而延遲執行對象的finalize方法;

  • 因為對象的finalize方法遲遲沒有執行,有可能會導致大部分f對象進入到old分代,此時容易引發old分代的GC,甚至Full GC,GC暫停時間明顯變長,甚至導致OOM;

  • 對象的finalize方法被調用后,這個對象其實還并沒有被回收,雖然可能在不久的將來會被回收。

finalizer的生存周期
  • 在創建對象時,如果對象override了finalize()方法,jvm會同時創建一個Finalizer對象
  • 所有Finalizer對象組成了一個雙向鏈表
  • 所有Finalizer對象都有一個名為queue的成員變量,指向的都是Finalizer類的靜態Queue。
  • cms gc執行到mark階段的最后時,會把需要gc的對象加入到Reference的pending list中。
  • 有一個專門的高級別線程Reference Handler處理pending list,把pending list中的對象取出來,放到這個對象所指的Reference Queue中,對于Finalizer對象來說,這個queue指向Finalizer類的靜態Queue。
  • Finalizer類有一個專門的線程負責從queue中取對象,并且執行finalizer引用的對象的finalize函數。

參考資料

《Java引用Reference學習》

《Java中的四種引用類型》

《Java引用類型》

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容