偏僻又熱門,引用與引用隊(duì)列
引用和引用指向的對(duì)象
A reference object(引用,我感覺(jué)這個(gè)不如直接寫成 reference 更容易理解)is a layer of indirection between your program code and some other object, called a referent(引用指向的對(duì)象). Each reference object is constructed around its referent, and the referent cannot be changed.
翻譯起來(lái)比較簡(jiǎn)單,引用(reference object)是一個(gè)間接層,我們的代碼通過(guò)引用訪問(wèn)引用指向的對(duì)象(referent)。
relationships between application code, soft/weak reference, and referent
所有的引用類型,都是抽象類 java.lang.ref.Reference 的子類:
這個(gè)抽象類提供了 get 方法用來(lái)獲取引用指向的對(duì)象(referent):
舉個(gè)例子:
SoftReference<List<Foo>> ref = new SoftReference<List<Foo>>(new LinkedList<Foo>());
// somewhere else in your code, you create a Foo that you want to add to the list
List<Foo> list = ref.get();
if (list != null) {
list.add(foo);
} else {
// list is gone; do whatever is appropriate
}
四種引用的定義
我想大部分人都可以很輕松的說(shuō)出引用的定義:如果棧中的變量存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱該變量代表了這塊內(nèi)存 or 這個(gè)對(duì)象的引用。
在 JDK 1.2 之前,沒(méi)有問(wèn)題,這個(gè)定義很正確。
不過(guò)現(xiàn)在來(lái)看有些過(guò)于狹隘了。
舉個(gè)例子,我們希望引用能夠描述這樣一類對(duì)象:當(dāng)內(nèi)存空間還足夠時(shí),就保留在內(nèi)存之中,如果垃圾收集后內(nèi)存空間比較緊張,那就拋棄這些對(duì)象釋放空間。
對(duì)于上述的定義來(lái)說(shuō),一個(gè)對(duì)象只有 “被引用” 和 “未被引用” 兩種狀態(tài),對(duì)這種情況顯然是無(wú)能無(wú)力的。
所以,JDK 1.2 之后,Java 對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為以下四種,這 4 種引用的強(qiáng)度依次逐漸減弱,所謂 “強(qiáng)度”,可以這樣簡(jiǎn)單理解,引用的強(qiáng)度越強(qiáng),那么這個(gè)被引用的對(duì)象就越不容易被垃圾回收器回收掉:
1)強(qiáng)引用,Strongly Re-ference
強(qiáng)引用隨處可見,就是最傳統(tǒng)的 “引用” 的定義,通過(guò) new 進(jìn)行的引用賦值,即類似User user = new User() 這種引用關(guān)系。
只要還有強(qiáng)引用指向一個(gè)對(duì)象,就能表明對(duì)象還 “活著”,垃圾收集器永遠(yuǎn)不會(huì)碰這種對(duì)象。
換句話說(shuō),當(dāng)內(nèi)存空間不足的時(shí)候,JVM 寧可拋出 OOM,使程序異常終止,也不會(huì)回收具有強(qiáng)引用的對(duì)象。
2)軟引用,Soft Reference
軟引用就對(duì)應(yīng)我們上面舉的那個(gè)例子,可以讓對(duì)象豁免一些垃圾收集,用來(lái)描述一些還有用、但非必須的對(duì)象。
如果內(nèi)存空間足夠,那么軟引用就不會(huì)被回收掉,但是如果快要發(fā)生 OOM 了,那么 JVM 就會(huì)對(duì)這些軟引用進(jìn)行回收釋放空間,如果對(duì)這些軟引用回收完了之后還是沒(méi)有足夠的內(nèi)存,才會(huì)拋出 OOM。
在 JDK 1.2 之后提供了 SoftReference 類來(lái)實(shí)現(xiàn)軟引用。
3)弱引用,Weak Reference
弱引用也是用來(lái)描述那些非必須對(duì)象,但是它的強(qiáng)度比軟引用更弱一些。
如果你創(chuàng)建了一個(gè)僅持有弱引用的對(duì)象,那么下一次垃圾收集發(fā)生的時(shí)候,無(wú)論當(dāng)前內(nèi)存是否足夠,這個(gè)對(duì)象都會(huì)被回收掉。
換句話說(shuō),被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生為止。
在 JDK 1.2 之后提供了 WeakReference 類來(lái)實(shí)現(xiàn)弱引用。
4)虛引用,Phantom Reference
虛引用也稱為 幽靈引用、幻影引用、幻象引用,它是最弱的一種引用關(guān)系。
如果一個(gè)對(duì)象僅持有幻像引用,那么它就和沒(méi)有任何引用一樣,對(duì)其生存時(shí)間沒(méi)有任何影響,我們也無(wú)法通過(guò)幻像引用來(lái)取得一個(gè)對(duì)象實(shí)例(看下圖,它的 get 方法永遠(yuǎn)返回 null)。
虛引用的 get 方法
滑稽了,那幻像引用有啥用?
事實(shí)上,我們可以通過(guò)為一個(gè)對(duì)象設(shè)置幻像引用關(guān)聯(lián)從而跟蹤這個(gè)對(duì)象被垃圾回收的活動(dòng)(詳細(xì)見下文解釋)。
在 JDK 1.2 之后提供了 PhantomReference 類來(lái)實(shí)現(xiàn)幻像引用。
對(duì)象的生命周期
在 JDK1.2 之前,一個(gè)對(duì)象的生命周期(object life cycle)可以簡(jiǎn)單的用下圖表示:
object life-cycle, without reference objects
而在 JDK1.2 中,引入了 java.lang.ref 包,一個(gè)對(duì)象的生命周期中新增了三個(gè)狀態(tài)(stage):
可以看到,除了強(qiáng)引用對(duì)應(yīng)的強(qiáng)可達(dá)狀態(tài)(strongly reachable)之外,額外添加了個(gè)三個(gè)狀態(tài),分別對(duì)應(yīng)軟引用、弱引用和虛引用(幻像引用):
- softly reachable,軟可達(dá):就是當(dāng)我們只能通過(guò)軟引用才能訪問(wèn)到對(duì)象的狀態(tài)。
- weakly reachable,弱可達(dá):就是無(wú)法通過(guò)強(qiáng)引用或者軟引用訪問(wèn),只能通過(guò)弱引用訪問(wèn)時(shí)的狀態(tài)。
- phantom reachable,幻象可達(dá):上面流程圖已經(jīng)很直觀了,就是沒(méi)有強(qiáng)、軟、弱引用關(guān)聯(lián),并且被回收掉了,只有幻像引用指向這個(gè)對(duì)象的時(shí)候。
除了幻像引用(因?yàn)?get 永遠(yuǎn)返回 null),如果對(duì)象還沒(méi)有被銷毀,都可以通過(guò) get 方法獲取原有對(duì)象。這意味著,利用軟引用和弱引用,我們可以將訪問(wèn)到的對(duì)象,重新指向強(qiáng)引用,也就是人為的改變了對(duì)象的可達(dá)性狀態(tài)!這也是為什么上面圖里有些地方畫了雙向箭頭。
引用隊(duì)列
引用隊(duì)列 ReferenceQueue 是用來(lái)配合引用工作的,最常與幻像引用一起使用,因?yàn)榛孟褚玫臉?gòu)造函數(shù)必須指定引用隊(duì)列,而其他引用類型沒(méi)有引用隊(duì)列一樣可以運(yùn)行。
當(dāng)某個(gè)被引用的對(duì)象(referent)被回收的時(shí)候,JVM 會(huì)將指向它的引用(reference)加入到引用隊(duì)列的隊(duì)列末尾,這相當(dāng)于是一種通知機(jī)制。這個(gè)操作其實(shí)是由 ReferenceHandler 守護(hù)線程來(lái)做的,這個(gè)守護(hù)線程是在 Reference 靜態(tài)代碼塊中建立并且運(yùn)行的線程,所以只要 Reference 這個(gè)父類被初始化,該線程就會(huì)創(chuàng)建和運(yùn)行,它的運(yùn)行方法中依賴了比較多的本地 (native) 方法:
由于 ReferenceHandler 是守護(hù)線程,除非 JVM 進(jìn)程終結(jié),否則它會(huì)一直在后臺(tái)運(yùn)行(注意它的 run() 方法里面使用了死循環(huán))。
實(shí)際上就是調(diào)用了引用隊(duì)列的 enqueue 方法來(lái)執(zhí)行入隊(duì)操作:
這樣,我們可以通過(guò) ReferenceQueue 中的元素(引用)來(lái)知道哪些對(duì)象(被引用的對(duì)象)被回收掉了,通過(guò)這種方式,我們就可以在對(duì)象被回收掉之后,做一些我們自己想做的事情。
這也就是為什么說(shuō)幻像引用存在的唯一作用就是跟蹤對(duì)象被垃圾回收的活動(dòng)。
另外,ReferenceQueue 提供了三種方法來(lái)彈出隊(duì)頭元素:
- poll():用于移除并返回該隊(duì)列中的下一個(gè)引用對(duì)象,如果隊(duì)列為空,則返回null;
- remove():用于移除并返回該隊(duì)列中的下一個(gè)引用對(duì)象,該方法會(huì)在隊(duì)列返回可用引用對(duì)象之前一直阻塞;
- remove (long timeout):用于移除并返回隊(duì)列中的下一個(gè)引用對(duì)象。該方法會(huì)在隊(duì)列返回可用引用對(duì)象之前一直阻塞,或者在超出指定超時(shí)后結(jié)束。如果超出指定超時(shí),則返回null。如果指定超時(shí)為0,意味著將無(wú)限期地等待。
不同引用類型的應(yīng)用場(chǎng)景
軟引用的應(yīng)用:斷路器
斷路器,Circuit Breaker
- A better use of soft references is to provide a "circuit breaker" for memory allocation: put a soft reference between your code and the memory it allocates, and you avoid the dreaded OutOfMemoryError.
舉個(gè)例子,下面這段 JDBC 代碼,邏輯是查詢數(shù)據(jù)庫(kù)的多行數(shù)據(jù)。
往比較極端的情況想,如果查詢到的數(shù)據(jù)有一百萬(wàn)行,但你的系統(tǒng)的可用內(nèi)存資源已經(jīng)不足以裝得下這一百萬(wàn)行數(shù)據(jù),此時(shí)程序肯定就拋錯(cuò)誤了。
這個(gè)時(shí)候軟引用的價(jià)值就體現(xiàn)出來(lái)了:如果在查詢數(shù)據(jù)期間 JVM 已經(jīng)耗盡了內(nèi)存,那么被軟引用指向的對(duì)象的內(nèi)存就會(huì)被釋放掉從而給新的數(shù)據(jù)挪出空間,同時(shí)在業(yè)務(wù)線程上我們可以拋出自定義異常以便我們進(jìn)行程序的后續(xù)處理:
弱引用的應(yīng)用:ThreadLocal 的 ThreadLocalMap 實(shí)現(xiàn)
大名鼎鼎,這個(gè)本文就不多說(shuō)了,后續(xù)會(huì)開文章詳細(xì)解釋。
虛引用的應(yīng)用:數(shù)據(jù)庫(kù)連接池
數(shù)據(jù)庫(kù)連接池 Connection Pool 應(yīng)該具備的一個(gè)優(yōu)點(diǎn)就是能夠有效的避免連接資源泄露,同時(shí)能夠?qū)B接資源進(jìn)行回收:
下面這個(gè)類可以不用怎么看,不過(guò)有一點(diǎn)值得注意,用戶使用該連接池時(shí)業(yè)務(wù)線程拿到的連接對(duì)象正是這個(gè)PooledConnection 對(duì)象,而不是真正的 Connection 對(duì)象。
重點(diǎn)看下下面這個(gè)類的實(shí)現(xiàn):
如果引用隊(duì)列中能夠拿到引用,說(shuō)明連接對(duì)象被 GC 掉了,此時(shí)我們就應(yīng)該對(duì)連接池執(zhí)行相應(yīng)的清理邏輯(重點(diǎn)注意下面的 releaseConnection 方法):
看起來(lái)挺復(fù)雜,其實(shí)本質(zhì)上就是圍繞著虛引用的特性:你不能通過(guò)它訪問(wèn)對(duì)象,但是它結(jié)合引用隊(duì)列提供了一種對(duì)象被回收以后做某些事情的機(jī)制。