深入理解Reference引用

本文結(jié)合,ThreadLocal內(nèi)存泄漏 和 DirectByteBuffer釋放 講解 Java 中的 Reference 班利。

四種引用類型

  • 強(qiáng)引用(Strong Reference):被強(qiáng)引用的對象饥漫,GC不能夠收集。常見得強(qiáng)引用對象得方式有: 賦值 Object obj = new Object() 罗标,集合引用list.add(new Object())等等趾浅。
  • 軟引用(Soft Reference):被軟引用的對象愕提,GC會在即將發(fā)生內(nèi)存溢出時,只要沒有對它強(qiáng)引用皿哨,就把它納入GC收集對象內(nèi)浅侨,進(jìn)行回收,軟引用通過 SoftReference 來實(shí)現(xiàn)证膨,它有兩個構(gòu)造 (T reference)(T reference,ReferenceQueue<? super T> queue)如输,和一個 get() 方法,用于獲取引用的對象央勒。
  • 弱引用(Weak Reference):被弱引用得對象不见,下一次GC時,只要沒有對它強(qiáng)引用就會納入GC收集對象內(nèi)崔步,進(jìn)行回收稳吮。與 SoftReference 相同,有兩個構(gòu)造和一個獲取引用對象得方法井濒。
  • 虛引用(Phantom Reference):被虛引用得對象隨時可以被GC灶似,并且它不能通過get() 獲取到引用對象,這個方法固定返回為 null 瑞你,存在得意義在于酪惭,可以在對象被收集時,‘得到通知’ 進(jìn)而做一些其他工作者甲,例如记焊,DirectByteBuffer 就是利用 PhantomReference 做直接內(nèi)存得釋放工作得冕末。

Reference

強(qiáng)引用(Strong Reference)底層實(shí)現(xiàn)無法感知,其他三種(Soft/Weak/Phantom Reference)均繼承于 abstract class Reference<T>,他們的兩個 構(gòu)造方法 和 一個 獲取引用對象 的方法也 均來自于 Reference翰意。

// SoftReference 簡單實(shí)現(xiàn)如下
public class SoftReference<T> extends Reference<T> {
    public SoftReference(T referent) {
        //在構(gòu)造 Soft/Weak/Phantom Reference 時堕汞,一般都需要掉用父級得回調(diào)歇盼。
        super(referent);
        this.timestamp = clock;
    }
    
    public T get() {
        T o = super.get();
        if (o != null && this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }
}
Refernece 狀態(tài)
Reference內(nèi)部狀態(tài).jpg
  1. Active:最初狀態(tài)违寿,被 GC 特殊處理,當(dāng)引用可達(dá)性發(fā)生變化時扫倡,狀態(tài)會變?yōu)?Pending 或者 Inactive 谦秧,具體是那個狀態(tài),依據(jù)于這個 Referece 在創(chuàng)建的時候是否撵溃,綁定了一個 ReferenceQueue 疚鲤。
  2. Pending:在這個狀態(tài)時,Reference 內(nèi)部變量 Reference<Object> pending 會被賦值為當(dāng)前的引用(這個賦值操作是 JVM 負(fù)責(zé)的) 缘挑,由內(nèi)部啟動得線程掉用它得 enqueu 方法進(jìn)入另一個狀態(tài)集歇。
  3. Enqueue:將 Reference<Object> pending 放入到 ReferenceQueue 內(nèi)并喚醒,所有在這個隊(duì)列上等待得線程语淘,
  4. Inactive:終態(tài)诲宇,到此為止际歼,這個 Reference 再也不能更改其他狀態(tài)了。
GC“回調(diào)/通知”業(yè)務(wù)線程

Reference 存在一個靜態(tài)代碼塊會啟動一個線程姑蓝,私有靜態(tài)成員 pending 由 垃圾收集器設(shè)置鹅心,并喚醒這個線程處理相關(guān)邏輯。摘要原代碼:

public abstract class Reference<T> {
    //由 collector 設(shè)置纺荧,并喚醒下面啟動的線程
    private static Reference<Object> pending = null;
    
    static {
        ...
        //處理邏輯旭愧,如果 pending == null 則 wait
        //否則為clearner對象,則直接調(diào)用clear() clear方法一般會開啟線程宙暇,不應(yīng)該阻塞這個loop
        //否則存在ReferenceQueue输枯,則放入 queue 中并喚醒等待的用戶線程
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        //最高優(yōu)先級
        handler.setPriority(Thread.MAX_PRIORITY);
        //守護(hù)線程
        handler.setDaemon(true);
        handler.start();
        ...
    }
}
Reference線程運(yùn)行流轉(zhuǎn).jpg

ThreadLocal & WeakReference 的使用

ThreadLocal 本質(zhì)上是一個門面類。通過它設(shè)置value占贫,本質(zhì)上是桃熄,在 Thread 的成員變量 ThreadLocal.ThreadLocalMap threadLocals = null; 中放入 Entry<k,v> ,其中 k 為這個 ThreadLocal 對象型奥,value 為需要存的數(shù)值瞳收。這樣的話就會有內(nèi)存泄漏的風(fēng)險,代碼描述如下:

//產(chǎn)生一個threadLocal對象
ThreadLocal<Object> threadLocal = new ThreadLocal();
...
//產(chǎn)生一個Thread t
new Thread(()->{
    //某些場景下設(shè)置 ThreadLocal 變量
    //本質(zhì)是在 threadLocalMap 中放入一個 Entry<threadLocal,Object>
    threadLocal.set(new Object());

    while (true) {
        //之后去使用這個內(nèi)容
        threadLocal.get();
    }
}).start();
...
//在之后的某塊代碼桩引,將這個ThreadLocal給設(shè)置成null了
//之后,線程內(nèi)通過這個 ThreadLocal 其實(shí)已經(jīng)無法訪問到期望的 value 了
//但實(shí)際上收夸,Entry<threadLocal,Object> 仍然被 threadLocalMap 強(qiáng)引用坑匠,占用著內(nèi)存
threadLocal = null;

從開發(fā)角度來說,將 threadLocal = null; => threadLocal.remove() 就可以解決這個問題卧惜。從Java語言層面其實(shí)ThreadLocal機(jī)制也存在其他操作來減少內(nèi)存溢出的風(fēng)險厘灼。

看一下ThreadLocalMap的實(shí)現(xiàn)

static class ThreadLocalMap {
    //繼承了 WeakRefernce ,Entry的key其實(shí)時一個弱飲用
    //也就是說咽瓷,當(dāng)ThreadLocal沒有任何強(qiáng)引用的時候设凹,通過 Reference#get()方法獲取key就會是 null
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Entry(ThreadLocal<?> k, Object v) {
            //k == ThreadLocal
            //Entry 的 Key 時 WeakReference,當(dāng)沒有強(qiáng)引用時茅姜,會get到null
            super(k);
            value = v;
        }
    }
    
    //這個方法內(nèi)闪朱,會移除掉 key == null 的 Entry
    //這個方法會在,put 的時候钻洒,如果 key 為 null 時調(diào)用
    private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
    }
}

簡單來說就是奋姿,ThreadLocalMap 中 Entry<K,V> K是一個 ThreadLocal 的弱引用,當(dāng) ThreadLocal 沒有任何強(qiáng)引用時素标,Entry的再獲取 K的時候称诗,會得到一個 null,再下一次 put 的時候头遭,就會從 ThreadLocalMap 中溢出掉所有 key 為 null 的 Entry寓免。

DirectByteBuffer & PhantomReference 的使用

//設(shè)置堆最大最小10m癣诱,直接內(nèi)存最大使用10m
//-Xmx10m -Xms10m -XX:MaxDirectMemorySize=10m
public static void main(String[] args) throws IOException {
    //分配10m directbuffer
    ByteBuffer buff1 = ByteBuffer.allocateDirect(1024 * 1024 * 10);
    //GC之后,在分配10m袜香,這里并不會內(nèi)存溢出
    buff1 = null;
    //這個顯示調(diào)用去掉撕予,其實(shí)在直接內(nèi)存不足的時候,也會自動出發(fā) FullGC 
    System.gc();
    ByteBuffer buff2 = ByteBuffer.allocateDirect(1024 * 1024 * 10);
}

看上面的代碼舉例困鸥,很奇怪的一點(diǎn)是嗅蔬,GC 一般來說只會對 Java堆 以及 MateSpace(1.8 方法區(qū)實(shí)現(xiàn))做回收,那為什么直接內(nèi)存在運(yùn)行了 System.gc() 之后也仿佛被回收了呢疾就?

DirectByteBuffer 時怎么被釋放的呢澜术?

答案在于 DirectByteBuffer 的創(chuàng)建過程,代碼如下:

DirectByteBuffer(int cap) {   // package-private    
    ...
    //關(guān)鍵在于這個 Cleaner猬腰,對于 DirectByteBuffer 的虛引用鸟废,并且接受一個 Runnable,這里是 Deallocator姑荷。
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;

}

這個Cleaner其實(shí)本質(zhì)上是一個對 DirectByteBuffer 對象的虛引用盒延,并且還接受了一個 Deallocator 對象(本質(zhì)上時一個Runnable,核心代碼是通過unsafe釋放內(nèi)存)鼠冕。上面講過添寺,再 GC 時,收集器一旦發(fā)現(xiàn)一個引用可達(dá)發(fā)生了變化懈费,就會走 GC“回調(diào)/通知”業(yè)務(wù)線程 這一套邏輯(上面講過了)计露。從而調(diào)用了 Cleaner#clean ,在這個例子中憎乙,這個方法票罐,其實(shí)就是運(yùn)行 Dealocator ,最終通過 unsafe.freeMemory(address) 釋放內(nèi)存泞边。

總的來說就是该押,通過Reference(具體來說是PhantomPeference)的通知/回調(diào)機(jī)制,在回收引用對象時阵谚,運(yùn)行一段用戶代碼蚕礼,調(diào)用unsafe.freeMemory(address)釋放了直接內(nèi)存。

除了 ThreadLocal DirectByteBuffer 外梢什,其他利用 Reference 在垃圾回收時闻牡,觸發(fā)一些用戶操作的類,還有很多绳矩。如:WeakHashMap 利用 WeakReference 防止內(nèi)存溢出罩润。

上述中,我這里將 Reference這個機(jī)制翼馆,叫做 GC“回調(diào)/通知”業(yè)務(wù)線程 并不妥當(dāng)割以,原諒我已經(jīng)詞窮了金度,介于這個詞語可以直觀的反饋這個機(jī)制,還請大家見諒严沥。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末猜极,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子消玄,更是在濱河造成了極大的恐慌跟伏,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翩瓜,死亡現(xiàn)場離奇詭異受扳,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)兔跌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門勘高,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人坟桅,你說我怎么就攤上這事华望。” “怎么了仅乓?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵赖舟,是天一觀的道長。 經(jīng)常有香客問我夸楣,道長宾抓,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任裕偿,我火速辦了婚禮洞慎,結(jié)果婚禮上痛单,老公的妹妹穿的比我還像新娘嘿棘。我一直安慰自己,他們只是感情好旭绒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布鸟妙。 她就那樣靜靜地躺著,像睡著了一般挥吵。 火紅的嫁衣襯著肌膚如雪重父。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天忽匈,我揣著相機(jī)與錄音房午,去河邊找鬼。 笑死丹允,一個胖子當(dāng)著我的面吹牛郭厌,可吹牛的內(nèi)容都是我干的袋倔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼折柠,長吁一口氣:“原來是場噩夢啊……” “哼宾娜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起扇售,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤前塔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后承冰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體华弓,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年巷懈,在試婚紗的時候發(fā)現(xiàn)自己被綠了该抒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡顶燕,死狀恐怖凑保,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情涌攻,我是刑警寧澤欧引,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站恳谎,受9級特大地震影響芝此,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜因痛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一婚苹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鸵膏,春花似錦膊升、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至债查,卻和暖如春非区,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背盹廷。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工征绸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓管怠,卻偏偏與公主長得像剥汤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子排惨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容