作者 陳彩華
文章轉載交流請聯絡 caison@aliyun.com
複製程式碼
本文通過探析Java中的引用模型,分析比較強引用、軟引用、弱引用、虛引用的概念及使用場景,知其然且知其所以然,希望給大家在實際開發實踐、學習開源專案提供參考。
1 Java的引用
對於Java中的垃圾回收機制來說,物件是否被應該回收的取決於該物件是否被引用。因此,引用也是JVM進行記憶體管理的一個重要概念。Java中是JVM負責記憶體的分配和回收,這是它的優點(使用方便,程式不用再像使用C語言那樣擔心記憶體),但同時也是它的缺點(不夠靈活)。由此,Java提供了引用分級模型,可以定義Java物件重要性和優先順序,提高JVM記憶體回收的執行效率。
關於引用的定義,在JDK1.2之前,如果reference型別的資料中儲存的數值代表的是另一塊記憶體的起始地址,就稱為這塊記憶體代表著一個引用;JDK1.2之後,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種。
軟引用物件和弱應用物件主要用於:當記憶體空間還足夠,則能儲存在記憶體之中;如果記憶體空間在垃圾收集之後還是非常緊張,則可以拋棄這些物件。很多系統的快取功能都符合這樣的使用場景。
而虛引用物件用於替代不靠譜的finalize方法,可以獲取物件的回收事件,來做資源清理工作。
2 物件生命週期
2.1 無分級引用物件生命週期
前面提到,分層引用的模型是用於記憶體回收,沒有分級引用物件下,一個物件從建立到回收的生命週期可以簡單地用下圖概括:物件被建立,被使用,有資格被收集,最終被收集,陰影區域表示物件“強可達”時間:
2.2 有分級引用物件生命週期
JDK1.2引入java.lang.ref程式包之後,物件的生命週期多了3個階段,軟可達,弱可達,虛可達,這些狀態僅適用於符合垃圾回收條件的物件,這些物件處於非強引用階段,而且需要基於java.lang.ref包中的相關的引用物件類來指示標明。
-
軟可達 軟可達物件用SoftReference來指示標明,並沒有強引用,垃圾回收器會盡可能長時間地保留物件,但是會在丟擲OutOfMemoryError異常之前收集它。
-
弱可達 弱可達物件用WeakReference來指示標明,並沒有強引用或軟引用,垃圾回收器會隨時回收物件,並不會嘗試保留它,但是會在丟擲OutOfMemoryError異常之前收集它。
假設垃圾收集器在某個時間點確定物件是弱可達的。 那時它將原子地清除該弱可達引用物件關聯的物件。
- 虛可達 虛可達物件用PhantomReference來指示標明,它已經被標記選中進行垃圾回收並且它的finalizer(如果有)已經執行。在這種情況下,術語“可達”實際上是用詞不當,因為您無法訪問實際物件。
物件生命週期圖中出現三個新的可選狀態會造成一些困惑。邏輯順序上是從強可達到軟,弱和虛,最終到回收,但實際的情況取決於程式建立的參考物件。但如果建立WeakReference但不建立SoftReference,則物件直接從強可達到弱到達最終到收集。
3 強引用
強引用就是指在程式程式碼之中普遍存在的,比如下面這段程式碼中的obj和str都是強引用:
Object obj = new Object();
String str = "hello world";
複製程式碼
只要強引用還存在,垃圾收集器永遠不會回收被引用的物件,即使在記憶體不足的情況下,JVM即使丟擲OutOfMemoryError異常也不會回收這種物件。
實際使用上,可以通過把引用顯示賦值為null來中斷物件與強引用之前的關聯,如果沒有任何引用執行物件,垃圾收集器將在合適的時間回收物件。
例如ArrayList類的remove方法中就是通過將引用賦值為null來實現清理工作的:
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
複製程式碼
4 引用物件
介紹軟引用、弱引用和虛引用之前,有必要介紹一下引用物件, 引用物件是程式程式碼和其他物件之間的間接層,稱為引用物件。每個引用物件都圍繞物件的引用構造,並且不能更改引用值。
引用物件提供get()來獲得其引用值的一個強引用,垃圾收集器可能隨時回收引用值所指的物件。 一旦物件被回收,get()方法將返回null,要正確使用引用物件,下面使用SoftReference(軟引用物件)作為參考示例:
/**
* 簡單使用demo
*/
private static void simpleUseDemo(){
List<String> myList = new ArrayList<>();
SoftReference<List<String>> refObj = new SoftReference<>(myList);
List<String> list = refObj.get();
if (null != list) {
list.add("hello");
} else {
// 整個列表已經被垃圾回收了,做其他處理
}
}
複製程式碼
也就是說,使用時:
- 1、必須經常檢查引用值是否為null 垃圾收集器可能隨時回收引用物件,如果輕率地使用引用值,遲早會得到一個NullPointerException。
- 2、必須使用強引用來指向引用物件返回的值 垃圾收集器可能在任何時間回收引用物件,即使在一個表示式中間。
/**
* 正確使用引用物件demo
*/
private static void trueUseRefObjDemo(){
List<String> myList = new ArrayList<>();
SoftReference<List<String>> refObj = new SoftReference<>(myList);
// 正確的使用,使用強引用指向物件保證獲得物件之後不會被回收
List<String> list = refObj.get();
if (null != list) {
list.add("hello");
} else {
// 整個列表已經被垃圾回收了,做其他處理
}
}
/**
* 錯誤使用引用物件demo
*/
private static void falseUseRefObjDemo(){
List<String> myList = new ArrayList<>();
SoftReference<List<String>> refObj = new SoftReference<>(myList);
// XXX 錯誤的使用,在檢查物件非空到使用物件期間,物件可能已經被回收
// 可能出現空指標異常
if (null != refObj.get()) {
refObj.get().add("hello");
}
}
複製程式碼
- 3、必須持有引用物件的強引用 如果建立引用物件,沒有持有物件的強引用,那麼引用物件本身將被垃圾收集器回收。
- 4、當引用值沒有被其他強引用指向時,軟引用、弱引用和虛引用才會發揮作用,引用物件的存在就是為了方便追蹤並高效垃圾回收。
5 軟引用、弱引用和虛引用
引用物件的3個重要實現類位於java.lang.ref包下,分別是軟引用SoftReference、弱引用WeakReference和虛引用PhantomReference。
5.1 軟引用
軟引用用來描述一些還有用但非必需的物件。對於軟引用關聯著的物件,在系統將要發生丟擲OutOfMemoryError異常之前,將會把這些物件列入回收範圍之內進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲OutOfMemoryError異常。在JDK1.2之後,提供了SoftReference類來實現軟引用。
下面是一個使用示例:
import java.lang.ref.SoftReference;
public class SoftRefDemo {
public static void main(String[] args) {
SoftReference<String> sr = new SoftReference<>( new String("hello world "));
// hello world
System.out.println(sr.get());
}
}
複製程式碼
JDK文件中提到:軟引用適用於對記憶體敏感的快取:每個快取物件都是通過訪問的 SoftReference,如果JVM決定需要記憶體空間,那麼它將清除回收部分或全部軟引用對應的物件。如果它不需要空間,則SoftReference指示物件保留在堆中,並且可以通過程式程式碼訪問。在這種情況下,當它們被積極使用時,它們被強引用,否則會被軟引用。如果清除了軟引用,則需要重新整理快取。
實際使用上,要除非快取的物件非常大,每個數量級為幾千位元組,才值得考慮使用軟引用物件。例如:實現一個檔案伺服器,它需要定期檢索相同的檔案,或者需要快取大型物件圖。如果物件很小,必須清除很多物件才能產生影響,那麼不建議使用,因為清除軟引用物件會增加整個過程的開銷。
5.2 弱引用
弱引用也是用來描述非必需物件,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集傳送之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。
在JDK1.2之後,提供了WeakReference類來實現弱引用。
/**
* 簡單使用弱引用demo
*/
private static void simpleUseWeakRefDemo(){
WeakReference<String> sr = new WeakReference<>(new String("hello world " ));
// before gc -> hello world
System.out.println("before gc -> " + sr.get());
// 通知JVM的gc進行垃圾回收
System.gc();
// after gc -> null
System.out.println("after gc -> " + sr.get());
}
複製程式碼
可以看到被弱引用關聯的物件,在gc之後被回收掉。 有意思的地方是,如果把上面程式碼中的:
WeakReference<String> sr = new WeakReference<>(new String("hello world "));
複製程式碼
改為
WeakReference<String> sr = new WeakReference<>("hello world ");
複製程式碼
程式將輸出
before gc -> hello world
after gc -> hello world
複製程式碼
這是因為使用Java的String直接賦值和使用new區別在於:
- new 會在堆區建立一個可以被正常回收的物件。
- String直接賦值,例如:String str = String( "Hello"); JVM首先在string池內裡面看找不找到字串 "Hello",找到,不做任何事情; 否則,建立新的String物件,放到String常量池裡面(常量池Hotspot1.7之前存於永生代,Hotspot1.7和1.7之後的版本存於堆區,通常不會被gc回收)。同時,由於遇到了new,還會在記憶體上(不是String常量池裡面)建立String物件儲存 "Hello",並將記憶體上的(不是String池內的)String物件返回給str。
WeakHashMap 為了更方便使用弱引用,Java還提供了WeakHashMap,功能類似HashMap,內部實現是用弱引用對key進行包裝,當某個key物件沒有任何強引用指向,gc會自動回收key和value物件。
/**
* weakHashMap使用demo
*/
private static void weakHashMapDemo(){
WeakHashMap<String,String> weakHashMap = new WeakHashMap<>();
String key1 = new String("key1");
String key2 = new String("key2");
String key3 = new String("key3");
weakHashMap.put(key1, "value1");
weakHashMap.put(key2, "value2");
weakHashMap.put(key3, "value3");
// 使沒有任何強引用指向key1
key1 = null;
System.out.println("before gc weakHashMap = " + weakHashMap + " , size=" + weakHashMap.size());
// 通知JVM的gc進行垃圾回收
System.gc();
System.out.println("after gc weakHashMap = " + weakHashMap + " , size="+ weakHashMap.size());
}
複製程式碼
程式輸出:
before: gc weakHashMap = {key1=value1, key2=value2, key3=value3} , size=3
after: gc weakHashMap = {key2=value2, key3=value3} , size=2
複製程式碼
WeakHashMap比較適用於快取的場景,例如Tomcat的快取就用到。
5.3 引用佇列
介紹虛引用之前,先介紹引用佇列: 在使用引用物件時,通過判斷get()方法返回的值是否為null來判斷物件是否已經被回收,當這樣做並不是非常高效,特別是當我們有很多引用物件,如果想找出哪些物件已經被回收,需要遍歷所有所有物件。
更好的方案是使用引用佇列,在構造引用物件時與佇列關聯,當gc(垃圾回收執行緒)準備回收一個物件時,如果發現它還僅有軟引用(或弱引用,或虛引用)指向它,就會在回收該物件之前,把這個軟引用(或弱引用,或虛引用)加入到與之關聯的引用佇列(ReferenceQueue)中。
如果一個軟引用(或弱引用,或虛引用)物件本身在引用佇列中,就說明該引用物件所指向的物件被回收了,所以要找出所有被回收的物件,只需要遍歷引用佇列。
當軟引用(或弱引用,或虛引用)物件所指向的物件被回收了,那麼這個引用物件本身就沒有價值了,如果程式中存在大量的這類物件(注意,我們建立的軟引用、弱引用、虛引用物件本身是個強引用,不會自動被gc回收),就會浪費記憶體。因此我們這就可以手動回收位於引用佇列中的引用物件本身。
/**
* 引用佇列demo
*/
private static void refQueueDemo() {
ReferenceQueue<String> refQueue = new ReferenceQueue<>();
// 用於檢查引用佇列中的引用值被回收
Thread checkRefQueueThread = new Thread(() -> {
while (true) {
Reference<? extends String> clearRef = refQueue.poll();
if (null != clearRef) {
System.out
.println("引用物件被回收, ref = " + clearRef + ", value = " + clearRef.get());
}
}
});
checkRefQueueThread.start();
WeakReference<String> weakRef1 = new WeakReference<>(new String("value1"), refQueue);
WeakReference<String> weakRef2 = new WeakReference<>(new String("value2"), refQueue);
WeakReference<String> weakRef3 = new WeakReference<>(new String("value3"), refQueue);
System.out.println("ref1 value = " + weakRef1.get() + ", ref2 value = " + weakRef2.get()
+ ", ref3 value = " + weakRef3.get());
System.out.println("開始通知JVM的gc進行垃圾回收");
// 通知JVM的gc進行垃圾回收
System.gc();
}
複製程式碼
程式輸出:
ref1 value = value1, ref2 value = value2, ref3 value = value3
開始通知JVM的gc進行垃圾回收
引用物件被回收, ref = java.lang.ref.WeakReference@48c6cd96, value=null
引用物件被回收, ref = java.lang.ref.WeakReference@46013afe, value=null
引用物件被回收, ref = java.lang.ref.WeakReference@423ea6e6, value=null
複製程式碼
5.4 虛引用
虛引用也稱為幽靈引用或者幻影引用,不同於軟引用和弱引用,虛引用不用於訪問引用物件所指示的物件,相反,通過不斷輪詢虛引用物件關聯的引用佇列,可以得到物件回收事件。一個物件是否有虛引用的存在,完全不會對其生產時間構成影響,也無法通過虛引用來取得一個物件例項。雖然這看起來毫無意義,但它實際上可以用來做物件回收時資源清理、釋放,它比finalize更靈活,我們可以基於虛引用做更安全可靠的物件關聯的資源回收。
- finalize的問題
- Java語言規範並不保證finalize方法會被及時地執行、而且根本不會保證它們會被執行 如果可用記憶體沒有被耗盡,垃圾收集器不會執行,finalize方法也不會被執行。
- 效能問題 JVM通常在單獨的低優先順序執行緒中完成finalize的執行。
- 物件再生問題 finalize方法中,可將待回收物件賦值給GC Roots可達的物件引用,從而達到物件再生的目的。
針對不靠譜finalize方法,完全可以使用虛引用來實現。在JDK1.2之後,提供了PhantomReference類來實現虛引用。
下面是簡單的使用例子,通過訪問引用佇列可以得到物件的回收事件:
/**
* 簡單使用虛引用demo
* 虛引用在實現一個物件被回收之前必須做清理操作是很有用的,比finalize()方法更靈活
*/
private static void simpleUsePhantomRefDemo() throws InterruptedException {
Object obj = new Object();
ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, refQueue);
// null
System.out.println(phantomRef.get());
// null
System.out.println(refQueue.poll());
obj = null;
// 通知JVM的gc進行垃圾回收
System.gc();
// null, 呼叫phantomRef.get()不管在什麼情況下會一直返回null
System.out.println(phantomRef.get());
// 當GC發現了虛引用,GC會將phantomRef插入進我們之前建立時傳入的refQueue佇列
// 注意,此時phantomRef物件,並沒有被GC回收,在我們顯式地呼叫refQueue.poll返回phantomRef之後
// 當GC第二次發現虛引用,而此時JVM將phantomRef插入到refQueue會插入失敗,此時GC才會對phantomRef物件進行回收
Thread.sleep(200);
Reference<?> pollObj = refQueue.poll();
// java.lang.ref.PhantomReference@1540e19d
System.out.println(pollObj);
if (null != pollObj) {
// 進行資源回收的操作
}
}
複製程式碼
比較常見的,可以基於虛引用實現JDBC連線池,鎖的釋放等場景。 以連線池為例,呼叫方正常情況下使用完連線,需要把連線釋放回池中,但是不可避免有可能程式有bug,造成連線沒有正常釋放回池中。基於虛引用對Connection物件進行包裝,並關聯引用佇列,就可以通過輪詢引用佇列檢查哪些連線物件已經被GC回收,釋放相關連線資源。具體實現已上傳github的caison-blog-demo倉庫。
6 總結
對比一下幾種引用物件的不同:
引用型別 | GC回收時間 | 常見用途 | 生存時間 |
---|---|---|---|
強引用 | 永不 | 物件的一般狀態 | JVM停止執行時 |
軟引用 | 記憶體不足時 | 物件快取 | 記憶體不足時終止 |
弱引用 | GC時 | 物件快取 | GC後終止 |
虛引用,配合引用佇列使用,通過不斷輪詢引用佇列獲取物件回收事件。
雖然引用物件是一個非常有用的工具來管理你的記憶體消耗,但有時它們是不夠的,或者是過度設計的 。例如,使用一個Map來快取從資料庫中讀取的資料。雖然可以使用弱引用來作為快取,但最終程式需要執行一定量的記憶體。如果不能給它足夠實際足夠的資源完成任何工作,那麼錯誤恢復機制有多強大也沒有用。
當遇到OutOfMemoryError錯誤,第一反應是要弄清楚它為什麼會發生,也許真的是程式有bug,也許是可用記憶體設定的太低。
在開發過程中,應該制定程式具體的使用記憶體大小,而已要關注實際使用中用了多少記憶體。大多數應用程式在實際執行負載下,程式的記憶體佔用會達到穩定狀態,可以用此來作為參考來設定合理的堆大小。如果程式的記憶體使用量隨著時間的推移而上升,很有可能是因為當物件不再使用時仍然擁有對物件的強引用。引用物件在這裡可能會有所幫助,但更有可能是把它當做一個bug來進行修復。
文章所有涉及原始碼已經上傳github,地址:github.com/caison/cais…
更多精彩,歡迎關注作者公眾號【分散式系統架構】
參考
《深入理解Java虛擬機器——JVM高階特性與最佳實踐(第2版)》