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


背景

工程中用到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)證明


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM