關(guān)于弱引用WeakReference所引用的對象的回收規(guī)則
什么是弱引用
- 弱引用實例:java.lang.ref.WeakReference類或者其子類的一個實例,就是一個弱引用實例。
- 弱引用:如果一個弱引用實例的成員變量referent引用了一個對象obj,那么就稱這個弱引用實例對obj的引用是弱引用。
- 弱引用對象:被一個弱引用實例引用的對象,稱為弱引用對象。
public class WeakReference<T> extends Reference<T> { /** * 構(gòu)造一個弱引用實例,此弱引用實例會引用給定的參數(shù)對象 */ public WeakReference(T referent) { super(referent); } /** * 構(gòu)造一個引用了給定對象referent的弱引用實例,同時將referent對象注冊到一個引用隊列 */ public WeakReference(T referent, ReferenceQueue<? super T> q) { super(referent, q); } }
示例
在實際業(yè)務(wù)場景中,我們通常會定義一個WeakReference的子類來解決我們的需求。 例如:
class Apple{ String color; void Apple(String color){ this.color = color; } String getColor(){ return color; } void setColor(String color){ this.color = color; } public String toString(){ return new StringBuilder("Apple[color=").append(this.color).append("]").toString(); } // 當(dāng)對象被GC回收時,會回調(diào)finalize()方法 protected void finalize() throws Throwable{ System.out.println(this); } }
如下所定義的Salad類,其實例是一個弱引用實例,其實例會持有一個Apple類對象的弱引用。
當(dāng)一個Apple實例對象只被salad類實例(或者其它弱引用實例)引用時,它就會被GC回收。
class Salad extends WeakReference<Apple>{ Apple apple; void Salad(Apple apple){ super(apple); } }
弱引用 WeakReference 相關(guān)的GC回收規(guī)則
當(dāng)一個對象只被弱引用實例引用(持有)時,這個對象就會被GC回收。
WeakReference類的javadoc:
一個弱引用實例,不會對它(的成員變量referent)所引用的對象的finalizable(是否可銷毀)、finalized(銷毀)和 reclaimed(GC回收)產(chǎn)生任何影響。
如果GC在某個時間點確定某對象是弱可達的(只被某個或某些弱引用對象引用),那么它就會清除對該弱可達對象的所有弱引用(將引用了弱可達對象的弱引用實例的referent置為null:referent=null),同時還會找出從"GC Roots"到該對象的強引用鏈和軟引用鏈上的所有弱可達對象,然后也會清除對這些弱可達對象的所有弱引用。
同時,GC會將以上弱可達對象標記為可銷毀的(finalizable)。然后會立刻或者在稍后的某個時間點,將以上那些清除了的弱引用實例對象入隊到它們在創(chuàng)建時就注冊到的queue中去。(參考Reference)
弱引用通常用于實現(xiàn)規(guī)范化映射(WeakHashMap、WeakCache)。
注意
- 上述規(guī)則中,會被GC標記為finalizable的的是弱引用實例引用的對象,而非弱引用實例本身
- 如果顯式地聲明了一個變量E e,并使之指向一個對象:e = new E(),這時變量e就是這個新創(chuàng)建的對象的一個強引用。如果變量e所引用的這個對象同時又被WeakReference的一個實例持有,則由于存在對對象的一個強引用e,對象并不符合上述回收規(guī)則,因此對象至少在變量e的作用域范圍內(nèi)都不會被回收。
示例:
public class Test{ public static void main(String[] args) throws InterruptedException{ // saladWithRedApple 引用的Apple對象符合弱引用回收規(guī)則 Salad saladWithRedApple = new Salad(new Apple("red ")); Apple green = new Apple("green"); // saladWithGreenApple 引用的Apple對象不符合弱引用回收規(guī)則,因為它同時被green這個強引用所引用 Salad saladWithGreenApple = new Salad(green); System.gc(); try{ Thread.sleep(5000); }catch(InterruptedException e){ e.printStackTrace(); } out.println(saladWithRedApple.get()==null); // true out.println(saladWithGreenApple.get()==null); // false } }
Reference
此類是所有Reference類的實例對象(所有Reference的實現(xiàn)類的實例對象)的抽象基類,此類中定義了所有Reference類的實例對象的通用操作。由于引用類對象同垃圾回收兩者之間有密切的關(guān)系(對象的回收本身就與對象的引用關(guān)系密切,例如初代垃圾收集器就是判斷對象是否還被變量引用來確定對象是否可以被會回收的),因此子類可能不會直接實現(xiàn)Reference類(而是實現(xiàn)WeakReference等類,以避免出現(xiàn)垃圾對象不被及時回收的情況。
注:如果用戶直接實現(xiàn)Reference類,就相當(dāng)于一個定義一個強引用類,因為GC對于用戶自定義的類并沒有做任何特殊處理。但GC對于JDK中定義的 SoftReference 和 WeakReference 等,都做了特殊處理,因此就有了不要直接實現(xiàn)Reference類的建議)。
在Reference類中定義了以下幾個實例變量:
private T referent; /* 會被GC進行特殊處理 */ /* 此引用實際指向的對象 */ volatile ReferenceQueue<? super T> queue; /* 當(dāng)前實例創(chuàng)建時如果對此queue賦值,則稱當(dāng)前實例注冊到了此queue */ Reference next; /* 用于確定當(dāng)前實例是否處于active狀態(tài)。active:null, pending:this, enqueued:next element in queue, inactive:this */ transient private Reference<T> discovered; /* used by VM */
Reference對實例對象定義了4種內(nèi)部狀態(tài)(沒有顯式地用枚舉類聲明出來):
- 活動的(active):剛創(chuàng)建的Reference實例處于活動狀態(tài)。垃圾收集器會對active狀態(tài)的引用所指向的實際對象referent做特殊處理:當(dāng)垃圾收集器監(jiān)測到active狀態(tài)的實例的referent的可達性變成了某個特定狀態(tài)時,會將當(dāng)前Reference實例的狀態(tài)由active更改為pending或者inactive。具體取決于實例創(chuàng)建時,是否注冊到了一個ReferenceQueue隊列(即r的queue是否為null),如果實例創(chuàng)建時注冊了queue(注意注冊到queue與添加到queue不是一個概念,這里的注冊到了queue,實際是指r持有了一個ReferenceQueue實例的引用),則實例狀態(tài)改為pending,并會被添加到掛起隊列(pendinglist,掛起隊列同queue不是一個隊列);否則實例狀態(tài)被改為inactive。
- 掛起(pending):當(dāng)實例被添加到掛起隊列pending-list中后,狀態(tài)就會被改為pending,即掛起隊列中的所有元素的狀態(tài)都是Pending。掛起隊列中的元素都在等待線程類將實例入隊(添加到元素自身持有的queue中去)。創(chuàng)建時沒有注冊到queue的Reference實例永遠也不會變成此狀態(tài)。
- enqueued:當(dāng)實例被添加到其自身持有的queue(即其創(chuàng)建時注冊的queue)后,狀態(tài)被更改為enqueued。當(dāng)實例被從此隊列中移除后,狀態(tài)就變?yōu)閕nactive
- 不活躍(inactive):當(dāng)實例被更改成此狀態(tài)后,其狀態(tài)就不會再改變了。
實例在各個狀態(tài)下時,其所持有的ReferenceQueue實例-queue變量和持有的Reference實例-next變量的值如下:
- active狀態(tài)時:queue = 實例被創(chuàng)建時如果注冊了queue,則此queue就不會空。否則queue=ReferenceQueue.NULL; next = null.
- pending狀態(tài)時:queue=創(chuàng)建時注冊到的queue,next=this。
- enqueued狀態(tài)時:queue=ReferenceQueue.ENQUEUED(其實也是null);next=原來的隊頭(頭插法),如果原來的隊列為空,則next=this。
- inactive狀態(tài)時:queue=ReferenceQueue.NULL,next=this。
在如上這種模式下,垃圾收集器僅需要通過檢查next字段就能確定實例是否需要特別的處理:如果next==null,那么實例處于active狀態(tài),如果next!=null,這垃圾收集器只需對實例進行常規(guī)處理。 為了確保垃圾收集器能在不干擾對reference實例對象進行enqueue()的應(yīng)用線程的正常運行的情況下,能發(fā)現(xiàn)處于active狀態(tài)的實例,垃圾收集器應(yīng)該通過discovered字段鏈接這些處于active狀態(tài)的實例。 discovered字段也用于鏈接掛起列表中的Reference實例對象。
ReferenceQueue
Reference類中定義了一個此類的對象:
volatile ReferenceQueue<? super T> queue;
當(dāng)某Reference類實例(或其子類的實例)可能將不會再被使用,需要被垃圾收集器監(jiān)測以回收時,應(yīng)將對象追加到此queue中。
垃圾收集器會不斷監(jiān)測此queue中的實例的狀態(tài),當(dāng)監(jiān)測到實例變更為某種狀態(tài)時,會對對象進行垃圾回收。
當(dāng)前對象r入隊后即queue.enqueue(),就會將自己的queue變量置空,即r.queue=null,以便垃圾收集器回收。
Lock
static private class Lock { } private static Lock lock = new Lock();
定義了Lock類并創(chuàng)建了一個此類的實例,用于作為同步垃圾收集線程的對象。
垃圾收集線程在每個收集周期開始時必須先獲得此鎖,因此獲得此鎖的其它代碼應(yīng)盡快完成:盡量不要創(chuàng)建新的對象、盡量避免調(diào)用用戶代碼。
pending
pending是一個全局變量,每個JVM中只有一份。
private static Reference<Object> pending=null; // 是一個鏈表的頭結(jié)點
pending指向一個鏈表的頭結(jié)點,當(dāng)某個處于特殊狀態(tài)的Reference實例需要插入此鏈表中時,會采用頭插法的方式,將自己設(shè)置為pending,成為新的頭結(jié)點。
pending所指向的鏈表中的所有 Reference 實例都是處于掛起狀態(tài)、等待入隊的 Reference 實例。
當(dāng)active狀態(tài)的引用實例的referent的可達性處于某個特定狀態(tài)時,垃圾收集線程會將此Reference實例添加到這個pending鏈表,并等待引用處理線程將元素從pending鏈表中移除,然后enqueue()到元素注冊的queue中去。
這個鏈表被lock對象保護。
這個鏈表用元素的discovered字段鏈接每個元素(相當(dāng)于鏈表節(jié)點中的next字段)。
ReferenceHandler
此線程用于將pending所指向的鏈表中的所有處于pending狀態(tài)的Reference實例,入隊到它們各自持有的queue中去。
此線程會調(diào)用boolean tryHandlePending(boolean waitFor)方法來處理pending狀態(tài)的引用對象。
此線程的優(yōu)先級被設(shè)置為最高。
如果可能還有其它pending狀態(tài)的引用實例,tryHandlePending會返回true。
如果沒有其它pending狀態(tài)的實例,并且希望應(yīng)用程序可以做更多的有意義的工作而不是這個線程一直自旋,一直占用CPU,則會返回false。
tryHandlePending方法的waitForNotify參數(shù)的意義:如果參數(shù)值為true,則線程會一直wait直到VM notify了它、或者線程被interrupted。
如果參數(shù)值為false,則當(dāng)沒有pending狀態(tài)的引用時,線程就立即退出了。
如果處理了一個pending狀態(tài)的引用,則方法返回true。如果沒有要處理的對象,則一直wait,直到被notify或者被
private static class ReferenceHandler extends Thread { ... public void run() { while (true) { tryHandlePending(true); } } ... }
static boolean tryHandlePending(boolean waitForNotify) { Reference<Object> r; Cleaner c; try { synchronized (lock) { if (pending != null) { r = pending; // 'instanceof' might throw OutOfMemoryError sometimes // so do this before un-linking 'r' from the 'pending' chain... c = r instanceof Cleaner ? (Cleaner) r : null; // unlink 'r' from 'pending' chain pending = r.discovered; r.discovered = null; } else { // The waiting on the lock may cause an OutOfMemoryError // because it may try to allocate exception objects. if (waitForNotify) { lock.wait(); } // retry if waited return waitForNotify; } } } catch (OutOfMemoryError x) { // Give other threads CPU time so they hopefully drop some live references // and GC reclaims some space. // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above // persistently throws OOME for some time... Thread.yield(); // retry return true; } catch (InterruptedException x) { // retry return true; } // Fast path for cleaners if (c != null) { c.clean(); return true; } ReferenceQueue<? super Object> q = r.queue; if (q != ReferenceQueue.NULL) q.enqueue(r); return true; }
Reference類中的靜態(tài)代碼塊
Reference中定義了一些靜態(tài)代碼塊,主要是啟動一個線程,將處于pending狀態(tài)的引用類對象入隊,入隊后的Reference實例的狀態(tài)將變成Enqueued。
在Reference類中,lock、pending、handler = new ReferenceHandler(…)、tryHandlePending(…)這些成員都是類成員,因此,handler 線程是對全局的pending鏈表中的所有處于pending狀態(tài)的Re實例進行處理。 queue、next、discovered則是實例變量。
static { ThreadGroup tg = Thread.currentThread().getThreadGroup(); for (ThreadGroup tgn = tg; tgn != null; tg = tgn, tgn = tg.getParent()); Thread handler = new ReferenceHandler(tg, "Reference Handler"); /* If there were a special system-only priority greater than * MAX_PRIORITY, it would be used here */ handler.setPriority(Thread.MAX_PRIORITY); handler.setDaemon(true); handler.start(); // provide access in SharedSecrets SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() { @Override public boolean tryHandlePendingReference() { return tryHandlePending(false); } }); }
WeakReference
弱引用對象,不會其引用的對象被JVM設(shè)置為可回收狀態(tài),然后被回收。弱引用通常用于實現(xiàn)規(guī)范化映射。
如果垃圾收集器在某個時間點確定某個對象的可達性是弱可達的(即這個對象可以通過一個弱引用鏈可達,即使同時也有其它強引用鏈或者軟引用鏈可達此對象),那么GC就會清除所有引用這個對象的弱引用,還會通過可以到達這個對象的強引用鏈和軟引用鏈找到鏈上其它對象上的所有弱引用、并清除所有這些弱引用。
同時,GC還會將所有之前被清除了弱引用的對象聲明為finalizable的。并且可能同時或者接著就會將那些弱引用實例本身添加到它們注冊到的ReferenceQueue隊列中去。
ReferenceQueue 類什么用?
在Reference類中,定義了一個ReferenceQueue類型的成員變量,變量名為queue。 并定義了相應(yīng)的構(gòu)造函數(shù):
public Reference( T referent, ReferenceQueue<? super T> queue){ this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; }
當(dāng)構(gòu)造一個引用實例時,如果初始化了成員變量queue的值,我稱之為將引用實例與queue綁定了。 那么將引用實例與這個queue綁定,有什么用呢? 由于GC會對Reference類及其子類的實例進行特殊方式的處理,比如對于weak引用實例,會在每次GC時都會將其發(fā)現(xiàn)的所有weak引用實例的referent 斷開其引用。但是用戶可能需要對此做一些個性化的處理。因此,JVM設(shè)計出這樣的方式:GC在清理weak實例時,會將weak實例入隊到其綁定的queue中,用戶就可以去queue中獲取這些被GC處理了的weak實例,然后再做一些個性化處理。
并不是所有的reference實例都必須綁定一個queue,如果用戶不需要對被GC的實例做特殊的處理,就不用設(shè)置。
一般在緩存map場景下,會定義一個ReferenceQueue,如WeakHashMap,WeakCache等。因為通常將實際的key封裝成一個WeakReference類實例,存儲到緩存map的key中,目的是借助GC自動及時釋放緩存內(nèi)存,防止map過大。但GC自動將map中weak實例對實際key的referent置為null后,相應(yīng)的entry就失去了在map中存在的意義了,這時queue的作用就出來了:GC在清理weak實例時,將此實例入隊到創(chuàng)建實例時綁定到的queue中,用戶主動遍歷這個queue,將queue中元素對應(yīng)的map中的entry清理掉。否則map永遠也不會釋放這些已經(jīng)失去意義的entry,這就會造成內(nèi)存泄漏。
WeakReference常用場景下的內(nèi)存泄漏問題
以ThreadLocal.ThreadLocalMap為例,經(jīng)常看到如下說法:ThreadLocalMap中, Entry extends WeakReference<ThreadLocal> ,Entry的key也即ThreadLocal實例本身會被賦值給WeakReference的referent,JVM執(zhí)行GC時,只要遇到弱引用就會將其斷開,即設(shè)置 referent=null ,則Entry的key變null了,那么Entry的value就已經(jīng)沒有意義了,也應(yīng)該能被GC回收掉,否則就是內(nèi)存泄漏。但是如果我們不主動調(diào)用 threadlocal.remove() ,不主動設(shè)置vlaue=null,那么被value引用的對象就會一直到線程銷毀都無法被GC回收掉,這就是ThreadLocal會造成內(nèi)存泄漏的說法。
但是,針對這種情況,ThreadLocal也不是什么都沒做。
在ThreadLocal實例每次執(zhí)行set(T value)方法時(首次創(chuàng)建線程的threadLocals對象時除外),最后都會執(zhí)行以下代碼
if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();
cleanSomeSlots方法代碼如下:
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { // 檢查key是否為null n = len; removed = true; i = expungeStaleEntry(i); // 清理value } } while ( (n >>>= 1) != 0); return removed; }
可見,除了第一次,其后每次向線程的threadLocals 中添加entry時,都會清理在此之前被GC掉的的key對應(yīng)的entry。
也就是說,通常情況下,每個線程最多只會存在一個應(yīng)該被GC回收但未能被回收的泄漏的對象。
如果這個對象非常大,占用JVM內(nèi)存空間較多,那么就影響較大。
如果線程非常多,每個線程都有一個泄漏的對象,那么影響也較大。
到此這篇關(guān)于關(guān)于弱引用WeakReference所引用的對象的回收規(guī)則的文章就介紹到這了,更多相關(guān)弱引用WeakReference回收規(guī)則內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解java中面向?qū)ο笤O(shè)計模式類與類的關(guān)系
這篇文章主要介紹了java面向?qū)ο笤O(shè)計模式中類與類之間的關(guān)系,下面小編和大家一起來學(xué)習(xí)一下吧2019-05-05java理論基礎(chǔ)Stream管道流狀態(tài)與并行操作
這篇文章主要為大家介紹了java理論基礎(chǔ)Stream管道流狀態(tài)與并行操作,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步2022-03-03最新IntelliJ?IDEA?2022配置?Tomcat?8.5?的詳細步驟演示
這篇文章主要介紹了IntelliJ?IDEA?2022?詳細配置?Tomcat?8.5?步驟演示,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-08-08Java Swing中的工具欄(JToolBar)和分割面版(JSplitPane)組件使用案例
這篇文章主要介紹了Java Swing中的工具欄(JToolBar)和分割面版(JSplitPane)組件使用案例,本文直接給出代碼實例和效果截圖,需要的朋友可以參考下2014-10-10