背景
工程中用到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後就會被回收;虛引用隨時會回收,好像沒有存在過,但是會有一個佇列來跟蹤它的垃圾回收情況。
相關閱讀