Java中的Reference類使用

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
}

參考

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