深入理解Java的分級引用模型

1 Java的引用

對于Java中的垃圾回收機(jī)制來說,對象是否被應(yīng)該回收的取決于該對象是否被引用。因此,引用也是JVM進(jìn)行內(nèi)存管理的一個重要概念。Java中是JVM負(fù)責(zé)內(nèi)存的分配和回收,這是它的優(yōu)點(diǎn)(使用方便,程序不用再像使用C語言那樣擔(dān)心內(nèi)存),但同時也是它的缺點(diǎn)(不夠靈活)。由此,Java提供了引用分級模型,可以?定義Java對象重要性和優(yōu)先級,提高JVM內(nèi)存回收的執(zhí)行效率?。

關(guān)于引用的定義,在JDK1.2之前,如果reference類型的數(shù)據(jù)中存儲的數(shù)值代表的是另一塊內(nèi)存的起始地址,就稱為這塊內(nèi)存代表著一個引用;JDK1.2之后,Java對引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種。

軟引用對象和弱應(yīng)用對象主要用于:當(dāng)內(nèi)存空間還足夠,則能保存在內(nèi)存之中;如果內(nèi)存空間在垃圾收集之后還是非常緊張,則可以拋棄這些對象。很多系統(tǒng)的緩存功能都符合這樣的使用場景。

而虛引用對象用于替代不靠譜的finalize方法,可以獲取對象的回收事件,來做資源清理工作。

2 對象生命周期

2.1 無分級引用對象生命周期

前面提到,分層引用的模型是用于內(nèi)存回收,沒有分級引用對象下,一個對象從創(chuàng)建到回收的生命周期可以簡單地用下圖概括:對象被創(chuàng)建,被使用,有資格被收集,最終被收集,陰影區(qū)域表示對象“強(qiáng)可達(dá)”時間:

對象生命周期(無分級引用)

2.2 有分級引用對象生命周期

JDK1.2引入java.lang.ref程序包之后,對象的生命周期多了3個階段,軟可達(dá),弱可達(dá),虛可達(dá),這些狀態(tài)僅適用于符合垃圾回收條件的對象,這些對象處于非強(qiáng)引用階段,而且需要基于java.lang.ref包中的相關(guān)的引用對象類來指示標(biāo)明。

軟可達(dá)

軟可達(dá)對象用SoftReference來指示標(biāo)明,并沒有強(qiáng)引用,垃圾回收器會盡可能長時間地保留對象,但是會在拋出OutOfMemoryError異常之前收集它。

弱可達(dá)

弱可達(dá)對象用WeakReference來指示標(biāo)明,并沒有強(qiáng)引用或軟引用,垃圾回收器會隨時回收對象,并不會嘗試保留它,但是會在拋出OutOfMemoryError異常之前收集它。

在對象回收階段中,該對象在major collection期間被回收,但是可以在minor collection期間存活

虛可達(dá)

虛可達(dá)對象用PhantomReference來指示標(biāo)明,它已經(jīng)被標(biāo)記選中進(jìn)行垃圾回收并且它的finalizer(如果有)已經(jīng)運(yùn)行。在這種情況下,術(shù)語“可達(dá)”實(shí)際上是用詞不當(dāng),因?yàn)槟鸁o法訪問實(shí)際對象。

對象生命周期(有分級引用)

對象生命周期圖中添加三個新的可選狀態(tài)會造成一些困惑。邏輯順序上是從強(qiáng)可達(dá)到軟,弱和虛,最終到回收,但實(shí)際的情況取決于程序創(chuàng)建的參考對象。但如果創(chuàng)建WeakReference但不創(chuàng)建SoftReference,則對象直接從強(qiáng)可達(dá)到弱到達(dá)最終到收集。

3 強(qiáng)引用

強(qiáng)引用就是指在程序代碼之中普遍存在的,比如下面這段代碼中的obj和str都是強(qiáng)引用:

Object obj = new Object();

String str = "hello world";

只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會回收被引用的對象,即使在內(nèi)存不足的情況下,JVM即使拋出OutOfMemoryError異常也不會回收這種對象。

實(shí)際使用上,可以通過把引用顯示賦值為null來中斷對象與強(qiáng)引用之前的關(guān)聯(lián),如果沒有任何引用執(zhí)行對象,垃圾收集器將在合適的時間回收對象。

例如ArrayList類的remove方法中就是通過將引用賦值為null來實(shí)現(xiàn)清理工作的:

/**

? ? * 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 引用對象

介紹軟引用、弱引用和虛引用之前,有必要介紹一下引用對象,

引用對象是程序代碼和其他對象之間的間接層,稱為引用對象。每個引用對象都圍繞對象的引用構(gòu)造,并且不能更改引用值。

引用對象提供get()來獲得其引用值的一個強(qiáng)引用,垃圾收集器可能隨時回收引用值所指的對象。

一旦對象被回收,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 {

? ? ? ? ? ? // 整個列表已經(jīng)被垃圾回收了,做其他處理

? ? ? ? }

? ? }

也就是說,使用時:

1、必須經(jīng)常檢查引用值是否為null?

垃圾收集器可能隨時回收引用對象,如果輕率地使用引用值,遲早會得到一個NullPointerException。

2、必須使用強(qiáng)引用來指向引用對象返回的值?

垃圾收集器可能在任何時間回收引用對象,即使在一個表達(dá)式中間。

/**

? ? * 正確使用引用對象demo

? ? */

? ? private static void trueUseRefObjDemo(){

? ? ? ? List<String> myList = new ArrayList<>();

? ? ? ? SoftReference<List<String>> refObj = new SoftReference<>(myList);

? ? ? ? // 正確的使用,使用強(qiáng)引用指向?qū)ο蟊WC獲得對象之后不會被回收

? ? ? ? List<String> list = refObj.get();

? ? ? ? if (null != list) {

? ? ? ? ? ? list.add("hello");

? ? ? ? } else {

? ? ? ? ? ? // 整個列表已經(jīng)被垃圾回收了,做其他處理

? ? ? ? }

? ? }

? ? /**

? ? * 錯誤使用引用對象demo

? ? */

? ? private static void falseUseRefObjDemo(){

? ? ? ? List<String> myList = new ArrayList<>();

? ? ? ? SoftReference<List<String>> refObj = new SoftReference<>(myList);

? ? ? ? // XXX 錯誤的使用,在檢查對象非空到使用對象期間,對象可能已經(jīng)被回收

? ? ? ? // 可能出現(xiàn)空指針異常

? ? ? ? if (null != refObj.get()) {

? ? ? ? ? ? refObj.get().add("hello");

? ? ? ? }

? ? }

3、必須持有引用對象的強(qiáng)引用?

如果創(chuàng)建引用對象,沒有持有對象的強(qiáng)引用,那么引用對象本身將被垃圾收集器回收。

4、當(dāng)引用值沒有被其他強(qiáng)引用指向時,軟引用、弱引用和虛引用才會發(fā)揮作用,引用對象的存在就是為了方便追蹤并高效垃圾回收。

5 軟引用、弱引用和虛引用

引用對象的3個重要實(shí)現(xiàn)類位于java.lang.ref包下,分別是軟引用SoftReference、弱引用WeakReference和虛引用PhantomReference。

5.1 軟引用

軟引用用來描述一些還有用但非必需的對象。對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生拋出OutOfMemoryError異常之前,將會把這些對象列入回收范圍之內(nèi)進(jìn)行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會拋出OutOfMemoryError異常。在JDK1.2之后,提供了SoftReference類來實(shí)現(xiàn)軟引用。

下面是一個使用示例:

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文檔中提到:軟引用適用于對內(nèi)存敏感的緩存:每個緩存對象都是通過訪問的 SoftReference,如果JVM決定需要內(nèi)存空間,那么它將清除回收部分或全部軟引用對應(yīng)的對象。如果它不需要空間,則SoftReference指示對象保留在堆中,并且可以通過程序代碼訪問。在這種情況下,當(dāng)它們被積極使用時,它們被強(qiáng)引用,否則會被軟引用。如果清除了軟引用,則需要刷新緩存。

實(shí)際使用上,要除非緩存的對象非常大,每個數(shù)量級為幾千字節(jié),才值得考慮使用軟引用對象。例如:實(shí)現(xiàn)一個文件服務(wù)器,它需要定期檢索相同的文件,或者需要緩存大型對象圖。如果對象很小,必須清除很多對象才能產(chǎn)生影響,那么不建議使用,因?yàn)榍宄浺脤ο髸黾诱麄€過程的開銷。

5.2 弱引用

弱引用也是用來描述非必需對象,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)送之前。?當(dāng)垃圾收集器工作時,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象?。

在JDK1.2之后,提供了WeakReference類來實(shí)現(xiàn)弱引用。

/**

? ? * 簡單使用弱引用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進(jìn)行垃圾回收

? ? ? ? System.gc();

? ? ? ? // after gc -> null

? ? ? ? System.out.println("after gc -> " + sr.get());

? ? }

可以看到被弱引用關(guān)聯(lián)的對象,在gc之后被回收掉。

有意思的地方是,如果把上面代碼中的:

WeakReference<String> sr = new WeakReference<>(new String("hello world "));

改為

WeakReference<String> sr = new WeakReference<>("hello world ");

程序?qū)⑤敵?/p>

before gc -> hello world

after gc -> hello world

這是因?yàn)槭褂肑ava的String直接賦值和使用new區(qū)別在于:

new 會在堆區(qū)創(chuàng)建一個可以被正?;厥盏膶ο蟆?/p>

String直接賦值,會在Java StringPool(字符串常量池)里創(chuàng)建一個String對象,存于pergmen(永生代區(qū))中,通常不會被gc回收。

WeakHashMap

為了更方便使用弱引用,Java還提供了WeakHashMap,功能類似HashMap,內(nèi)部實(shí)現(xiàn)是用弱引用對key進(jìn)行包裝,當(dāng)某個key對象沒有任何強(qiáng)引用指向,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");

? ? ? ? // 使沒有任何強(qiáng)引用指向key1

? ? ? ? key1 = null;

? ? ? ? System.out.println("before gc weakHashMap = " + weakHashMap + " , size=" + weakHashMap.size());

? ? ? ? // 通知JVM的gc進(jìn)行垃圾回收

? ? ? ? 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 引用隊(duì)列

介紹虛引用之前,先介紹引用隊(duì)列:

在使用引用對象時,通過判斷get()方法返回的值是否為null來判斷對象是否已經(jīng)被回收,當(dāng)這樣做并不是非常高效,特別是當(dāng)我們有很多引用對象,如果想找出哪些對象已經(jīng)被回收,需要遍歷所有所有對象。

更好的方案是使用引用隊(duì)列,在構(gòu)造引用對象時與隊(duì)列關(guān)聯(lián),當(dāng)gc(垃圾回收線程)準(zhǔn)備回收一個對象時,如果發(fā)現(xiàn)它還僅有軟引用(或弱引用,或虛引用)指向它,就會在回收該對象之前,把這個軟引用(或弱引用,或虛引用)加入到與之關(guān)聯(lián)的引用隊(duì)列(ReferenceQueue)中。

如果一個軟引用(或弱引用,或虛引用)對象本身在引用隊(duì)列中,就說明該引用對象所指向的對象被回收了,所以要找出所有被回收的對象,只需要遍歷引用隊(duì)列。

當(dāng)軟引用(或弱引用,或虛引用)對象所指向的對象被回收了,那么這個引用對象本身就沒有價值了,如果程序中存在大量的這類對象(注意,我們創(chuàng)建的軟引用、弱引用、虛引用對象本身是個強(qiáng)引用,不會自動被gc回收),就會浪費(fèi)內(nèi)存。因此我們這就可以手動回收位于引用隊(duì)列中的引用對象本身。

/**

? ? * 引用隊(duì)列demo

? ? */

? ? private static void refQueueDemo() {

? ? ? ? ReferenceQueue<String> refQueue = new ReferenceQueue<>();

? ? ? ? // 用于檢查引用隊(duì)列中的引用值被回收

? ? ? ? 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進(jìn)行垃圾回收");

? ? ? ? // 通知JVM的gc進(jìn)行垃圾回收

? ? ? ? System.gc();

? ? }

程序輸出:

ref1 value = value1, ref2 value = value2, ref3 value = value3

開始通知JVM的gc進(jìn)行垃圾回收

引用對象被回收, 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 虛引用

虛引用也稱為幽靈引用或者幻影引用,不同于軟引用和弱引用,虛引用不用于訪問引用對象所指示的對象,相反,?通過不斷輪詢虛引用對象關(guān)聯(lián)的引用隊(duì)列,可以得到對象回收事件?。一個對象是否有虛引用的存在,完全不會對其生產(chǎn)時間構(gòu)成影響,也無法通過虛引用來取得一個對象實(shí)例。雖然這看起來毫無意義,但它實(shí)際上可以用來做對象回收時?資源清理、釋放?,它比finalize更靈活,我們可以基于虛引用做更安全可靠的對象關(guān)聯(lián)的資源回收。

finalize的問題

Java語言規(guī)范并不保證finalize方法會被及時地執(zhí)行、而且根本不會保證它們會被執(zhí)行?

如果可用內(nèi)存沒有被耗盡,垃圾收集器不會運(yùn)行,finalize方法也不會被執(zhí)行。

性能問題?

JVM通常在單獨(dú)的低優(yōu)先級線程中完成finalize的執(zhí)行。

對象再生問題?

finalize方法中,可將待回收對象賦值給GC Roots可達(dá)的對象引用,從而達(dá)到對象再生的目的。

針對不靠譜finalize方法,完全可以使用虛引用來實(shí)現(xiàn)。在JDK1.2之后,提供了PhantomReference類來實(shí)現(xiàn)虛引用。

下面是簡單的使用例子,通過訪問引用隊(duì)列可以得到對象的回收事件:

/**

? ? * 簡單使用虛引用demo

? ? * 虛引用在實(shí)現(xiàn)一個對象被回收之前必須做清理操作是很有用的,比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進(jìn)行垃圾回收

? ? ? ? System.gc();

? ? ? ? // null, 調(diào)用phantomRef.get()不管在什么情況下會一直返回null

? ? ? ? System.out.println(phantomRef.get());

? ? ? ? // 當(dāng)GC發(fā)現(xiàn)了虛引用,GC會將phantomRef插入進(jìn)我們之前創(chuàng)建時傳入的refQueue隊(duì)列

? ? ? ? // 注意,此時phantomRef對象,并沒有被GC回收,在我們顯式地調(diào)用refQueue.poll返回phantomRef之后

? ? ? ? // 當(dāng)GC第二次發(fā)現(xiàn)虛引用,而此時JVM將phantomRef插入到refQueue會插入失敗,此時GC才會對phantomRef對象進(jìn)行回收

? ? ? ? Thread.sleep(200);

? ? ? ? Reference<?> pollObj = refQueue.poll();

? ? ? ? // java.lang.ref.PhantomReference@1540e19d

? ? ? ? System.out.println(pollObj);

? ? ? ? if (null != pollObj) {

? ? ? ? ? ? // 進(jìn)行資源回收的操作

? ? ? ? }

? ? }

比較常見的,可以基于虛引用實(shí)現(xiàn)JDBC連接池,鎖的釋放等場景。

以連接池為例,調(diào)用方正常情況下使用完連接,需要把連接釋放回池中,但是不可避免有可能程序有bug,造成連接沒有正常釋放回池中。基于虛引用對Connection對象進(jìn)行包裝,并關(guān)聯(lián)引用隊(duì)列,就可以通過輪詢引用隊(duì)列檢查哪些連接對象已經(jīng)被GC回收,釋放相關(guān)連接資源。?具體實(shí)現(xiàn)已上傳github的caison-blog-demo倉庫?。

6 總結(jié)

對比一下幾種引用對象的不同:

引用類型 GC回收時間 常見用途 生存時間

強(qiáng)引用 永不 對象的一般狀態(tài) JVM停止運(yùn)行時

軟引用 內(nèi)存不足時 對象緩存 內(nèi)存不足時終止

弱引用 GC時 對象緩存 GC后終止

虛引用,配合引用隊(duì)列使用,通過不斷輪詢引用隊(duì)列獲取對象回收事件。

雖然引用對象是一個非常有用的工具來管理你的內(nèi)存消耗,但有時它們是不夠的,或者是過度設(shè)計(jì)的 。例如,使用一個Map來緩存從數(shù)據(jù)庫中讀取的數(shù)據(jù)。雖然可以使用弱引用來作為緩存,但最終程序需要運(yùn)行一定量的內(nèi)存。如果不能給它足夠?qū)嶋H足夠的資源完成任何工作,那么錯誤恢復(fù)機(jī)制有多強(qiáng)大也沒有用。

當(dāng)遇到OutOfMemoryError錯誤,第一反應(yīng)是要弄清楚它為什么會發(fā)生,也許真的是程序有bug,也許是可用內(nèi)存設(shè)置的太低。

在開發(fā)過程中,應(yīng)該制定程序具體的使用內(nèi)存大小,而已要關(guān)注實(shí)際使用中用了多少內(nèi)存。大多數(shù)應(yīng)用程序在實(shí)際運(yùn)行負(fù)載下,程序的內(nèi)存占用會達(dá)到穩(wěn)定狀態(tài),可以用此來作為參考來設(shè)置合理的堆大小。如果程序的內(nèi)存使用量隨著時間的推移而上升,很有可能是因?yàn)楫?dāng)對象不再使用時仍然擁有對對象的強(qiáng)引用。引用對象在這里可能會有所幫助,但更有可能是把它當(dāng)做一個bug來進(jìn)行修復(fù)。

如果想學(xué)習(xí)Java工程化、高性能及分布式、深入淺出。性能調(diào)優(yōu)、Spring,MyBatis,Netty源碼分析的朋友可以加我的Java高級架構(gòu)進(jìn)階群:180705916,群里有阿里大牛直播講解技術(shù),以及Java大型互聯(lián)網(wǎng)技術(shù)的視頻免費(fèi)分享給大家

---------------------

作者:SJYUA

來源:CSDN

原文:https://blog.csdn.net/yexunce3159/article/details/83059975?utm_source=copy

版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請附上博文鏈接!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容