Java中的Reference類使用

程式設計師自由之路發表於2020-12-10

Java 2 平臺引入了 java.lang.ref 包,這個包下面包含了幾個Reference相關的類,Reference相關類將Java中的引用也對映成一個物件,這些類還提供了與垃圾收集器(garbage collector)之間有限的互動。

Reference引用類的幾種型別

在jvm中,一個物件如果不再被使用就會被當做垃圾給回收掉,判斷一個物件是否是垃圾,通常有兩種方法:引用計數法和可達性分析法。不管是哪一種方法判斷一個物件是否是垃圾的條件總是一個物件的引用是都沒有了。

JDK.1.2 之後,Java 對引用的概念進行了擴充,將引用分為了:強引用、軟引用、弱引用、虛引用4 種。下面就介紹下這些引用型別的區別。

強引用

如果一個物件具有強引用,它就不會被垃圾回收器回收。即使當前記憶體空間不足,JVM也不會回收它,而是丟擲 OutOfMemoryError 錯誤,使程式異常終止。下面的程式碼中str就是一個強引用。

public void test1(){
    String str = new String("程式設計師自由之路");
}

軟引用(SoftReference)

記憶體足夠的時候,軟引用物件不會被回收,只有在記憶體不足時,系統則會回收軟引用物件,如果回收了軟引用物件之後仍然沒有足夠的記憶體,才會丟擲記憶體溢位異常。

上面只是很簡單的說了下:當系統沒有足夠的記憶體時會回收軟引用物件。但是具體什麼才是記憶體不夠?具體的回收具體是什麼?如果想要了解具體的情況,大家可以參考這篇文章。我簡單總結了下,軟引用物件具體的回收策略如下:

如果已經沒有引用指向軟引用物件,那麼這個物件會被JVM回收;

如果還有軟引用指向這個軟引用物件,就判斷在某段之間之內(_max_interval),有沒有呼叫過SoftReference的get方法,如果在_max_interval時間內沒呼叫過get方法,那麼即使還有軟引用指向這個物件,JVM也會回收這個物件,如果在_max_interval時間內呼叫過get方法,那麼就不會回收這個物件。

_max_interval具體的時間是根據JVM的可用記憶體動態計算出來的,如果JVM的可用記憶體比較大,那麼_max_interval的值也比較大,如果JVM的可用記憶體比較小,那麼max_interval也會比較小。

我自己寫了一段程式碼來展示軟引用物件回收的過程。為了讓堆記憶體迅速耗盡,我將最大記憶體設定為-Xmx5m。

public static void main(String[] args) throws InterruptedException {
    SoftReference<String> reference = new SoftReference<>(new String("自由之路..."));
    List<String> list = new ArrayList<>();
    while (true) {
        for (int i = 0; i < 10000; i++) {
            // 這邊的物件都是強引用,不會被回收
            list.add(new String("自由之路"));
        }
        // 暫停一段時間,為了讓_max_interval時間段檢測生效
        // 沒有這段暫停的話,JVM不會回收軟引用物件,因為一直有執行緒在快速地呼叫軟引用的get方法
        TimeUnit.MILLISECONDS.sleep(10);
        String s = reference.get();
        if (s == null) {
            logger.info("OMG, reference is gone...");
        }else {
            logger.info(s);
        }
    }
}

程式碼的執行效果,如下:

13:36:52.322 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - 自由之路...
13:36:52.372 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - 自由之路...
13:36:52.385 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - 自由之路...
13:36:52.397 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - 自由之路...
13:36:52.412 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - 自由之路...
13:36:52.423 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - 自由之路...
13:36:52.435 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - 自由之路...
13:36:52.488 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - 自由之路...
13:36:52.499 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - 自由之路...
13:36:52.555 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - 自由之路...
// 從下面開始,軟引用物件已經被虛擬機器回收了。
13:36:52.666 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - OMG, reference is gone... 
13:36:54.750 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - OMG, reference is gone...
13:36:58.686 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - OMG, reference is gone...
// 系統已經不能再分配出記憶體空間,直接報OutOfMemoryError
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
	at com.csx.demo.spring.boot.dao.UserMapperTest.main(UserMapperTest.java:54)

弱引用(WeakReference)

如果一個物件具有弱引用,在垃圾回收時候,一旦發現弱引用物件,無論當前記憶體空間是否充足,都會將弱引用回收。

關於弱引用,我也寫了個Bug程式碼,展示弱引用物件的回收過程。

public static void main(String[] args) throws InterruptedException {
        WeakReference<String> reference = new WeakReference<>(new String("自由之路..."));
        List<String> list = new ArrayList<>();
        while (true) {
            list.add(new String("自由之路"));
            String s = reference.get();
            if (s == null) {
                logger.info("OMG, reference is gone...");
            } else {
                logger.info(s);
            }
        }
    }

程式碼的執行結果如下:

13:50:54.015 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - 自由之路...
13:50:54.015 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - 自由之路...
13:50:54.015 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - 自由之路...
13:50:54.015 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - 自由之路...
// 這邊GC已經將弱引用物件回收
13:50:54.051 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - OMG, reference is gone...
13:50:54.051 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - OMG, reference is gone...
13:50:54.051 [main] INFO com.csx.demo.spring.boot.dao.UserMapperTest - OMG, reference is gone...

關於WeakReference,Java中一個比較典型的應用就是:WeakHashMap。關於這個類的使用情況大家可以參考這篇文章

虛引用(PhantomReference)

虛引用和前面的軟引用、弱引用不同,它並不影響物件的生命週期。如果一個物件與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收。虛引用是使用PhantomReference建立的引用,虛引用也稱為幽靈引用或者幻影引用,是所有引用型別中最弱的一個。一個物件是否有虛引用的存在,完全不會對其生命週期構成影響,也無法通過虛引用獲得一個物件例項。

使用虛引用的目的就是為了得知物件被GC的時機,所以可以利用虛引用來進行銷燬前的一些操作,比如說資源釋放等。這個虛引用對於物件而言完全是無感知的,有沒有完全一樣,但是對於虛引用的使用者而言,就像是待觀察的物件的把脈線,可以通過它來觀察物件是否已經被回收,從而進行相應的處理。

在<<深入理解Java虛擬機器>>3.2.3中有這麼一句話

為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。

要注意的是,虛引用必須和引用佇列關聯使用,當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會把這個虛引用加入到與之關聯的引用佇列中。程式可以通過判斷引用佇列中是否已經加入了虛引用,來了解被引用的物件是否將要被垃圾回收。如果程式發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體被回收之前採取必要的行動

public class Test {
    public static boolean isRun = true;
 
    @SuppressWarnings("static-access")
    public static void main(String[] args) throws Exception {
        String abc = new String("abc");
        System.out.println(abc.getClass() + "@" + abc.hashCode());
        final ReferenceQueue<String> referenceQueue = new ReferenceQueue<String>();
        new Thread() {
            public void run() {
                while (isRun) {
                    Object obj = referenceQueue.poll();
                    if (obj != null) {
                        try {
                            Field rereferent = Reference.class
                                    .getDeclaredField("referent");
                            rereferent.setAccessible(true);
                            Object result = rereferent.get(obj);
                            System.out.println("gc will collect:"
                                    + result.getClass() + "@"
                                    + result.hashCode() + "\t"
                                    + (String) result);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }.start();
        PhantomReference<String> abcWeakRef = new PhantomReference<String>(abc,
                referenceQueue);
        abc = null;
        Thread.currentThread().sleep(3000);
        System.gc();
        Thread.currentThread().sleep(3000);
        isRun = false;
    }
}

一個執行緒一直再檢測回收佇列中有沒有被回收的引用。如果有被回收的引用,進行一些操作。

引用佇列(ReferenceQueue)

作為一個Java物件,SoftReference物件除了具有儲存軟引用的特殊性之外,也具有Java物件的一般性。所以,當軟可及物件被回收之後,雖然這個SoftReference物件的get()方法返回null,但這個SoftReference物件已經不再具有存在的價值,需要一個適當的清除機制,避免大量SoftReference物件帶來的記憶體洩漏。在java.lang.ref包裡還提供了ReferenceQueue。如果在建立SoftReference物件的時候,使用了一個ReferenceQueue物件作為引數提供給SoftReference的構造方法:

ReferenceQueue queue = new ReferenceQueue();  
SoftReference  ref = new SoftReference(object, queue);  

那麼當這個SoftReference所軟引用的物件被垃圾收集器回收的同時,ref所強引用的SoftReference物件被列入ReferenceQueue。也就是說,ReferenceQueue中儲存的物件是Reference物件,而且是已經失去了它所軟引用的物件的Reference物件。另外從ReferenceQueue這個名字也可以看出,它是一個佇列,當我們呼叫它的poll()方法的時候,如果這個佇列中不是空佇列,那麼將返回佇列前面的那個Reference物件。

在任何時候,我們都可以呼叫ReferenceQueue的poll()方法來檢查是否有它所關心的非強可及物件被回收。如果佇列為空,將返回一個null,否則該方法返回佇列中前面的一個Reference物件。利用這個方法,我們可以檢查哪個SoftReference所軟引用的物件已經被回收,於是我們可以把這些失去所軟引用的物件的SoftReference物件清除掉。


SoftReference ref = null;
while ((ref = (EmployeeRef) q.poll()) != null) {
    // 清除ref
}

參考

相關文章