Java的強引用、軟引用、弱引用、虛引用

程式設計一生發表於2020-11-18

背景

工程中用到guava的本地快取。它底層實現和API介面上使用了強引用、軟引用、弱引用。所以溫故知新下,也夯實下基礎。

 

預備知識

先來看下GC日誌每個欄位的含義

Young GC示例解釋

[GC (Allocation Failure) [PSYoungGen: 273405K->20968K(278016K)] 480289K->473619K(737792K), 0.1090103 secs] [Times: user=0.19 sys=0.27, real=0.11 secs] 

解釋

[GC(產生GC的原因,例子中是由於分配記憶體失敗)  [PSYoungGen: 年輕代回收前空間->年輕代回收後空間(年輕代總空間)] 堆區的回收前空間->堆區的回收後空間(堆區的總空間), GC耗時] [Times: 使用者空間耗時 系統空間耗時, 實際耗時]

Full GC示例解釋 

[Full GC (Ergonomics) [PSYoungGen: 20968K->20805K(278016K)] [ParOldGen: 452651K->451654K(864256K)] 473619K->472460K(1142272K), [Metaspace: 5793K->5793K(1056768K)], 0.1565987 secs] [Times: user=0.70 sys=0.00, real=0.16 secs] 

解釋

[Full GC (產生GC原因,例子中是由於要放入老年代的物件超過了老年代的剩餘空間) [PSYoungGen: ->年輕代回收前空間->年輕代回收後空間(年輕代總空間)] [ParOldGen: 老年代回收前空間->老年代回收後空間(老年代總空間)] 堆區的回收前空間->堆區的回收後空間(堆區的總空間), [Metaspace: 元空間的回收前空間->元空間的回收後空間(元空間的總空間)],  GC耗時] [Times: 使用者空間耗時 系統空間耗時, 實際耗時]

 

建立一個10M的大物件,重寫finalize方法。finalize()方法會在物件被回收前呼叫,一個物件只有一次被呼叫的機會。物件可以在這個方法裡進行自救,逃過被垃圾回收。Java設計這個方法可以被覆寫是為了讓有些物件在回收前做一些檢查,完成一些前置條件再被垃圾回收。正式程式碼不建議使用。因為是測試,所以為了驗證效果,這裡列印GC日誌資訊。

   byte[] bytes = new byte[10 * 1024 * 1024];
    int index;
    public Ref(int index) {
        this.index = index;
    }

    public byte[] getBytes() {
        return bytes;
    }

    @Override
    public void finalize() {
        System.out.println("index " + index + "'s " + bytes.length + "is going to be GG");
    }
}

為了測試,JVM引數統一為-Xms20M -XX:+PrintGCDetails。Xms20M表示堆記憶體設定最大為20M,-XX:+PrintGCDetails代表列印詳細的GC資訊。

強引用

先來做個實驗(程式碼已經上傳github:https://github.com/xiexiaojing/yuna)

@Test
public void  testRawStrong() {
    List<Ref> list = Lists.newArrayList();
    for(int i=0; i<100; i++) {
        list.add(new Ref(i));
        System.out.println(list.get(i));
    }
}

這段程式碼由上線的設定可知,由於最大設定20M堆空間,所以很快觸發了GC。

不過Xmx這個值是建議記憶體最大使用值。如果記憶體使用超過這個值,jvm認為還有記憶體可以使用,也會將物件一直往堆裡面放。所以2次GC之後JVM自動擴容了,之後就不再頻繁GC。最終用到了滿足程式需要的記憶體。

強引用是直接new出來呼叫的物件,大家都知道。由上面實驗可知,在系統記憶體很富裕的情況下,因為強引用記憶體不能被釋放,所以會多申請了很多記憶體。

 

軟引用

軟引用會在系統將要發生記憶體溢位異常之前,將會把這些軟引用物件列進回收範圍進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。

用實驗說明一下,為了防止JVM自動調整堆大小,我們把堆設定-Xmx200M。

@Test
public void  testRawSoft() {
    List<SoftReference<Ref>> list = Lists.newArrayList();
    for(int i=0; i<100; i++) {
        list.add(new SoftReference<>(new Ref(i)));
        System.out.println(list.get(i).get());
    }
}

從下面實驗結果可以看到數次的 GC之後,記憶體要撐不住的時候,Ref的軟引用物件觸發了finalize方法。這意味著它將要被記憶體回收了。說明GC會引發軟引用裡物件的記憶體回收,即使這個軟引用本身還被強引用(list呼叫)著。

最終回收了這些記憶體也不能避免OOM的結局:

因為軟引用通常情況下就是這樣,只有記憶體馬上要溢位了才觸發它的GC。就好像扁鵲見蔡桓公的時候,蔡桓公的病已經很深了,馬上就沒救了。所以有了下面弱引用的方法:有病早治。

 

弱引用

弱引用是發生了一次垃圾回收後,既存的弱引用物件就開始回收。通常,一個弱引用物件僅能生存到下一次垃圾回收前。

用實驗說明一下,為了防止JVM自動調整堆大小,我們把堆設定-Xmx200M。

@Test
public void  testRawWeak() {
    List<WeakReference<Ref>> list = Lists.newArrayList();
    for(int i=0; i<100; i++) {
        list.add(new WeakReference<>(new Ref(i)));
        System.out.println(list.get(i).get());
    }
}

從下面的實驗結果可知在發生了一次GC之後,已經生成的軟引用物件都都回收了。下一次GC,這中間產生的軟引用物件也都被回收了。

最終,由於GC及時,整個過程沒有爆發OOM,平安的結束了。

虛引用

虛引用也叫幻影引用。任何時候可能被GC回收,就像沒有引用一樣。

並且他必須和引用佇列一起使用,用於跟蹤垃圾回收過程,當垃圾回收器回收一個持有虛引用的物件時,在回收物件後,將這個虛引用物件加入到引用佇列中,用來通知應用程式垃圾的回收情況。

先來實驗一下,從下面結果可看到從一開始取出來就是空物件,基本上剛建立出來就被回收了。

一個像是從來沒有存在過的幻影有什麼用呢?Java的Unsafe類和NIO都可以直接訪問堆外記憶體。堆外記憶體GC管不了,這時候虛引用就排上用場了。我們可以通過引用佇列跟蹤垃圾回收,做好善後。

 

在Guava中使用強軟弱引用

@Test
public void testStrong() {
    Cache<Integer, Ref> cache = CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.DAYS).build();
    for(int i=0; i<100; i++) {
        cache.put(i, new Ref(i));
        System.out.println(cache.getIfPresent(i));
    }
    System.out.println(cache.stats().loadSuccessCount());
}

@Test
public void testSoft() {
    Cache<Integer, Ref> cache = CacheBuilder.newBuilder().softValues().build();
    for(int i=0; i<100; i++) {
        cache.put(i, new Ref(i));
        System.out.println(cache.getIfPresent(i));
    }
}

@Test
public void testWeak() {
    Cache<Integer, Ref> cache = CacheBuilder.newBuilder().weakKeys().weakValues().build();
    for(int i=0; i<100; i++) {
        cache.put(i, new Ref(i));
        System.out.println(cache.getIfPresent(i));
    }
}

 

Guava在沒有顯示設定強、軟、弱引用的情況下預設是強引用。這個結論我沒有看任何書,而是通過跟蹤原始碼,debug得到的結論。當顯示設定為軟引用或者弱引用時,執行時GC觸發和物件回收之間的關係和自己手動直接測試的結果是一樣的,大家可以動手實踐下。

 

總結

Java的強軟弱虛引用被回收的時機不同:強引用是引用被釋放才會回收;軟引用是沒釋放,但是快OOM了就會被回收;弱引用是引用沒釋放,但是發生了GC後就會被回收;虛引用隨時會回收,好像沒有存在過,但是會有一個佇列來跟蹤它的垃圾回收情況。

 

相關閱讀

Java非同步的2種方式分析

關於Java兩點需要更新的知識

阿里巴巴編碼規範(Java)證明

相關文章