如何有效的避免OOM,溫故Java中的引用

佔小狼發表於2019-02-20

佔小狼 轉載請註明原創出處,謝謝!

如何有效的避免OOM,溫故Java中的引用

背景

前段時間,看到群裡在討論Java中的各種引用,不禁的細想了下,發現自己對它們的瞭解僅僅停留在了表面,對它們的實現原理、回收機制,並不熟悉,心裡發毛。

大雪過後的魔都,顯得更冷了,但Java的大道,越挫越勇。

一個簡單的HashMap,相當於一個簡陋版的快取,如果不斷的往裡面新增資料,JVM遲早會發生OOM,那麼有何有效的避免這種OOM,今天一起溫故Java中的各種引用。

四種引用

在Java層面,一共有四種引用:強引用、軟引用、弱引用、虛引用,從名字也可以發現,這幾種引用的生命週期由強到弱。

這裡為什麼要說在Java層面呢,因為除了這四種引用,還有其它兩種。
只是這四種在程式碼中觸手可及,而其它兩個,幾乎碰不到。

強引用

強引用(Strong Reference)是使用最普遍的引用,99%的程式碼可能都是強引用,很多人平時接觸的也都是強引用相關的程式碼,比如下面這種:

Object o = new Object()

這種情況是普遍存在的,在寫中介軟體框架程式碼時,可能才需要其它引用。

如果一個物件,和GC Root有強引用的關係,當記憶體不足發生GC時,寧可丟擲OOM異常,終止程式,也不會回收這些物件。

相反,當一個物件,和GC Root沒有強引用關係時,可能會被回收(因為可能還有其它引用),如果沒有任何引用關係,GC之後,該物件就被回收了。

軟引用

軟引用(Soft Reference),主要用來描述一些不那麼重要的物件,它具有一個特性,會保證儘可能長時間的駐留在JVM中,只在GC多次之後實在不足記憶體才會被回收,這種特別適合用來實現快取。

Object reference = new MyObject();
System.out.println(reference);
Reference root = new SoftReference(reference);
reference = null; // MyObject物件只有軟引用
System.gc();
System.out.println(root.get());
複製程式碼

強制GC之後,SoftReference所引用的物件並沒有被回收,那麼軟引用的物件到底在哪個時間點會被回收呢?感興趣的同學可以腦洞一下。

弱引用

弱引用(Weak Reference),相對於軟引用,它的生命週期更短,當發生GC時,如果掃描到一個物件只有弱引用,不管當前記憶體是否足夠,都會對它進行回收。

感覺說的有點幹,來段程式碼解解渴。

Object reference = new MyObject();
System.out.println(reference);
Reference root = new WeakReference(reference);
reference = null; // MyObject物件只有弱引用
System.gc(); 
System.out.println(root.get());
複製程式碼

發生GC之後,root.get()返回了null,說明物件已經被回收。

在JDK的WeakHashMap中,很好的應用了弱引用,其中Entry繼承了WeakReference,如果一個entry物件,一旦沒有指向key的強引用,WeakHashMap在GC後會自動刪除相關的entry。

虛引用

虛引用(Phantom Reference),和之前兩種引用的最大不同是:它的get方法一直返回null。

很奇怪,一個返回null的引用有什麼用?

虛引用的使用場景很窄,在JDK中,目前只知道在申請堆外記憶體時有它的身影。
申請堆外記憶體時,在JVM堆中會建立一個對應的Cleaner物件,這個Cleaner類繼承了PhantomReference,當DirectByteBuffer物件被回收時,可以執行對應的Cleaner物件的clean方法,做一些後續工作,這裡是釋放之前申請的堆外記憶體。

由於虛引用的get方法無法拿到真實物件,所以當你不想讓真實物件被訪問時,可以選擇使用虛引用,它唯一能做的是在物件被GC時,收到通知,並執行一些後續工作。

看到有些文章說虛引用可以清理已經執行finalize方法,但是還沒被回收的物件,這簡直就是誤導人嘛,與finalize方法有關的引用是FinalReference,這個引用就是之前說的其它兩種中的一個。

實現原理

上述引用中,除了強引用,其它幾種都有對應的實現類,都繼承了Reference,所有的精華也都在這個類中。

Reference有幾個重要的引數,有些和GC密切相關:
1、referent: 就是所引用的物件,會被GC特別對待。
2、queue:RererenceQueue,看名字也知道它是一個Reference佇列,用來儲存Reference物件,當新建一個Reference時,可以選擇性的傳入第二個引數。
3、discovered:該物件被JVM使用,表示下一個要被處理的Reference物件(1.8的實現)
4、next:當Reference物件被放入RererenceQueue時,使用next變數形成連結串列結構。
5、pending:該物件會被JVM使用,當前被處理的Reference物件。

Reference中有一個重要的執行緒 Reference Handler,執行優先順序極高,啟動之後負責輪詢pending變數是否有資料,如果pending被JVM設定了一個值,就把它拿出來放到queue中,這裡有個例外,就是之前說的堆外記憶體申請時的Cleaner物件,只會執行它的clean方法,並不會放到queue中。

當Reference物件被放進queue之後,就可以使用一個執行緒,依次拿出來進行處理。

文字太乾,還是需要上程式碼:

final ReferenceQueue queue = new ReferenceQueue();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Reference reference = queue.remove();
System.out.println(reference + "回收了");
} catch (InterruptedException e) {

}
}
}
}).start();

Object o = new Object();
Reference root = new WeakReference(o, queue);
System.out.println(root);
o = null;
System.gc();
System.in.read();
複製程式碼

上述程式碼中,先初始化了一個ReferenceQueue,隨後又初始化了一個執行緒,迴圈的從queue中撈資料,因為當一個軟引用、弱引用或虛引用的物件被GC回收時,這個引用會被放到對應的ReferenceQueue中,這裡會被拿出來進行列印,更多的是做一些清理工作。

執行上述程式碼的結果:

java.lang.ref.WeakReference@34374ed5
0.174: [Full GC (System) 0.175: [CMS: 0K->437K(12288K), 0.0146570 secs] 1231K->437K(19712K), [CMS Perm : 2692K->2690K(21248K)], 0.0147430 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
java.lang.ref.WeakReference@34374ed5 回收了
複製程式碼

通過這種方式,有興趣的同學可以驗證一下其它引用。

總結

型別 生命週期 用途
軟引用 直到記憶體不足時 快取
弱引用 下次GC 快取(WeakHashMap)
虛引用 下次GC 堆外記憶體管理

相關文章