徹底理解 java Reference

java引用體系中我們最熟悉的就是強引用類型,如 A a= new A();這是我們經(jīng)常說的強引用StrongReference寸谜,jvm gc時會檢測對象是否存在強引用竟稳,如果存在由根對象對其有傳遞的強引用,則不會對其進行回收熊痴,即使內(nèi)存不足拋出OutOfMemoryError他爸。

除了強引用外,Java還引入了SoftReference果善,WeakReference诊笤,PhantomReference,F(xiàn)inalReference 巾陕,這些類放在java.lang.ref包下讨跟,類的繼承體系如下圖


image.png

Java額外引入這個四種類型引用主要目的是在jvm 在gc時,按照引用類型的不同鄙煤,在回收時采用不同的邏輯晾匠。可以把這些引用看作是對對象的一層包裹梯刚,jvm根據(jù)外層不同的包裹凉馆,對其包裹的對象采用不同的回收策略.

Reference指代引用對象本身,Referent指代被引用對象

對象可達性判斷
jvm gc時亡资,判斷一個對象是否存在引用時句喜,都是從根結(jié)合引用(Root Set of References)開始去標(biāo)識,往往到達一個對象的引用路徑會存在多條咳胃,如下圖

image.png

那么 垃圾回收時會依據(jù)兩個原則來判斷對象的可達性:

  • 單一路徑中,以最弱的引用為準(zhǔn)
  • 多路徑中存崖,以最強的引用為準(zhǔn)

例如Obj4的引用,存在3個路徑:1->6供搀、2->5葛虐、3->4, 那么從根對象到Obj4最強的引用是2->5涕蚤,因為它們都是強引用万栅。如果僅僅存在一個路徑對Obj4有引用時,比如現(xiàn)在只剩1->6,那么根對象到Obj4的引用就是以最弱的為準(zhǔn),就是SoftReference引用,Obj4就是softly-reachable對象管跺。

Java最初只有普通的強引用,只有對象存在引用艇拍,則對象就不會被回收卸夕,即使內(nèi)存不足,也是如此个初,JVM會爆出OOM院溺,也不會去回收存在引用的對象逐虚。

如果只提供強引用痊班,我們就很難寫出“這個對象不是很重要凝果,如果內(nèi)存不足GC回收掉也是可以的”這種語義的代碼器净。Java在1.2版本中完善了引用體系,提供了4中引用類型:強引用浪慌,軟引用权纤,弱引用汹想,虛引用。使用這些引用類型冗茸,我們不但可以控制垃圾回收器對對象的回收策略夏漱,同時還能在對象被回收后得到通知,進行相應(yīng)的后續(xù)操作。

Java目前有4中引用類型:

  1. 強引用(Strong Reference):普通的的引用類型交播,new一個對象默認(rèn)得到的引用就是強引用,只要對象存在強引用隧土,就不會被GC曹傀。
  2. 軟引用(Soft Reference):相對較弱的引用,垃圾回收器會在內(nèi)存不足時回收弱引用指向的對象幕庐。JVM會在拋出OOME前清理所有弱引用指向的對象异剥,如果清理完還是內(nèi)存不足错妖,才會拋出OOME潮模。所以軟引用一般用于實現(xiàn)內(nèi)存敏感緩存擎厢。
  3. 弱引用(Weak Reference):更弱的引用類型,垃圾回收器在GC時會回收此對象厘惦,也可以用于實現(xiàn)緩存酝静,比如JDK提供的WeakHashMap别智。
  4. 虛引用(Phantom Reference):一種特殊的引用類型,不能通過虛引用獲取到關(guān)聯(lián)對象渺杉,只是用于獲取對象被回收的通知耳舅。

SoftReference:軟引用浦徊,堆內(nèi)存不足時,垃圾回收器會回收對應(yīng)引用
WeakReference:弱引用呢岗,每次垃圾回收都會回收其引用
PhantomReference:虛引用悉尾,對引用無影響,只用于獲取對象被回收的通知
FinalReference:Java用于實現(xiàn)finalization的一個內(nèi)部類

Reference的核心

Java的多種引用類型實現(xiàn)惫霸,不是通過擴展語法實現(xiàn)的,而是利用類實現(xiàn)的茫打,Reference類表示一個引用轮洋,其核心代碼就是一個成員變量reference

public abstract class Reference<T> {
    private T referent; // 會被GC特殊對待
    
    // 獲取Reference管理的對象
    public T get() {
        return this.referent;
    }
    
    // ...
}

如果JVM沒有對這個變量做特殊處理,它依然只是一個普通的強引用汉柒,之所以會出現(xiàn)不同的引用類型,是因為JVM垃圾回收器硬編碼識別SoftReference正塌,WeakReference,PhantomReference等這些具體的類鸠天,對其reference變量進行特殊對象,才有了不同的引用類型的效果饥瓷。

Reference及其子類有兩大功能:

  • 實現(xiàn)特定的引用類型
  • 用戶可以對象被回收后得到通知

第一個功能很清楚词裤,第二個功能是如何做到的呢吼砂?

一種思路是在新建一個Reference實例是因俐,添加一個回調(diào),當(dāng)java.lang.ref.Reference#referent被回收時,JVM調(diào)用該回調(diào)衷敌,這種思路比較符合一般的通知模型,但是對于引用與垃圾回收這種底層場景來說面氓,會導(dǎo)致實現(xiàn)復(fù)雜,性能不高的問題禀横,比如需要考慮在什么線程中執(zhí)行這個回調(diào)复亏,回調(diào)執(zhí)行阻塞怎么辦等等抬闷。

所以Reference使用了一種更加原始的方式來做通知,就是把引用對象被回收的Reference添加到一個隊列中炕泳,用戶后續(xù)自己去從隊列中獲取并使用。

理解了設(shè)計后對應(yīng)到代碼上就好理解了,Reference有一個queue成員變量,用于存儲引用對象被回收的Reference實例:

public abstract class Reference<T> {
    // 會被GC特殊對待
    private T referent; 
    // reference被回收后掉瞳,當(dāng)前Reference實例會被添加到這個隊列中
    volatile ReferenceQueue<? super T> queue;
    
    // 只傳入reference的構(gòu)造函數(shù)耀怜,意味著用戶只需要特殊的引用類型麻裁,不關(guān)心對象何時被GC
    Reference(T referent) {
        this(referent, null);
    }
    
    // 傳入referent和ReferenceQueue的構(gòu)造函數(shù)丑勤,reference被回收后惠啄,會添加到queue中
    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
    
    // ...
}

Reference的狀態(tài)
Reference對象是有狀態(tài)的。一共有4中狀態(tài):

  • Active:新創(chuàng)建的實例的狀態(tài)箭启,由垃圾回收器進行處理芜抒,如果實例的可達性處于合適的狀態(tài)疗绣,垃圾回收器會切換實例的狀態(tài)為Pending或者Inactive。如果Reference注冊了ReferenceQueue罢维,則會切換為Pending凳怨,并且Reference會加入pending-Reference鏈表中囤耳,如果沒有注冊ReferenceQueue宰僧,會切換為Inactive普气。
  • Pending:在pending-Reference鏈表中的Reference的狀態(tài),這些Reference等待被加入ReferenceQueue中碾局。
  • Enqueued:在ReferenceQueue隊列中的Reference的狀態(tài)忽冻,如果Reference從隊列中移除赶么,會進入Inactive狀態(tài)
  • Inactive:Reference的最終狀態(tài)
image.png

除了上文提到的ReferenceQueue匾寝,這里出現(xiàn)了一個新的數(shù)據(jù)結(jié)構(gòu):pending-Reference企孩。這個鏈表是用來干什么的呢莲组?

上文提到了奴潘,reference引用的對象被回收后浪册,該Reference實例會被添加到ReferenceQueue中,但是這個不是垃圾回收器來做的期升,這個操作還是有一定邏輯的沼瘫。 如果垃圾回收器還需要執(zhí)行這個操作坛猪,會降低其效率。從另外一方面想坠宴,Reference實例會被添加到ReferenceQueue中的實效性要求不高胳赌,所以也沒必要在回收時立馬加入ReferenceQueue。

所以垃圾回收器做的是一個更輕量級的操作:把Reference添加到pending-Reference鏈表中喂柒。Reference對象中有一個pending成員變量不瓶,是靜態(tài)變量,它就是這個pending-Reference鏈表的頭結(jié)點灾杰。要組成鏈表蚊丐,還需要一個指針,指向下一個節(jié)點艳吠,這個對應(yīng)的是java.lang.ref.Reference#discovered這個成員變量吠撮。

public abstract class Reference<T> {
    // 會被GC特殊對待
    private T referent; 
    // reference被回收后,當(dāng)前Reference實例會被添加到這個隊列中
    volatile ReferenceQueue<? super T> queue; 
    
    // 全局唯一的pending-Reference列表
    private static Reference<Object> pending = null;
    
    // Reference為Active:由垃圾回收器管理的已發(fā)現(xiàn)的引用列表(這個不在本文討論訪問內(nèi))
    // Reference為Pending:在pending列表中的下一個元素,如果沒有為null
    // 其他狀態(tài):NULL
    transient private Reference<T> discovered;  /* used by VM */
    // ...
}

ReferenceHandler線程
通過上文的討論泥兰,我們知道一個Reference實例化后狀態(tài)為Active弄屡,其引用的對象被回收后,垃圾回收器將其加入到pending-Reference鏈表鞋诗,等待加入ReferenceQueue膀捷。這個過程是如何實現(xiàn)的呢?

這個過程不能對垃圾回收器產(chǎn)生影響削彬,所以不能在垃圾回收線程中執(zhí)行全庸,也就需要一個獨立的線程來負(fù)責(zé)。這個線程就是ReferenceHandler融痛,它定義在Reference類中:

// 用于控制垃圾回收器操作與Pending狀態(tài)的Reference入隊操作不沖突執(zhí)行的全局鎖
// 垃圾回收器開始一輪垃圾回收前要獲取此鎖
// 所以所有占用這個鎖的代碼必須盡快完成壶笼,不能生成新對象,也不能調(diào)用用戶代碼
static private class Lock { };
private static Lock lock = new Lock();

private static class ReferenceHandler extends Thread {

    ReferenceHandler(ThreadGroup g, String name) {
        super(g, name);
    }

    public void run() {
        // 這個線程一直執(zhí)行
        for (;;) {
            Reference<Object> r;
            // 獲取鎖雁刷,避免與垃圾回收器同時操作
            synchronized (lock) {
                // 判斷pending-Reference鏈表是否有數(shù)據(jù)
                if (pending != null) {
                    // 如果有Pending Reference覆劈,從列表中取出
                    r = pending;
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    // 如果沒有Pending Reference,調(diào)用wait等待
                    // 
                    // wait等待鎖沛励,是可能拋出OOME的责语,
                    // 因為可能發(fā)生InterruptedException異常,然后就需要實例化這個異常對象目派,
                    // 如果此時內(nèi)存不足坤候,就可能拋出OOME,所以這里需要捕獲OutOfMemoryError企蹭,
                    // 避免因為OOME而導(dǎo)致ReferenceHandler進程靜默退出
                    try {
                        try {
                            lock.wait();
                        } catch (OutOfMemoryError x) { }
                    } catch (InterruptedException x) { }
                    continue;
                }
            }

            // 如果Reference是Cleaner白筹,調(diào)用其clean方法
            // 這與Cleaner機制有關(guān)系,不在此文的討論訪問
            if (r instanceof Cleaner) {
                ((Cleaner)r).clean();
                continue;
            }

            // 把Reference添加到關(guān)聯(lián)的ReferenceQueue中
            // 如果Reference構(gòu)造時沒有關(guān)聯(lián)ReferenceQueue谅摄,會關(guān)聯(lián)ReferenceQueue.NULL徒河,這里就不會進行入隊操作了
            ReferenceQueue<Object> q = r.queue;
            if (q != ReferenceQueue.NULL) q.enqueue(r);
        }
    }
}

ReferenceHandler線程是在Reference的static塊中啟動的:

static {
    // 獲取system ThreadGroup
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    Thread handler = new ReferenceHandler(tg, "Reference Handler");

    // ReferenceHandler線程有最高優(yōu)先級
    handler.setPriority(Thread.MAX_PRIORITY);
    handler.setDaemon(true);
    handler.start();
}

綜上,ReferenceHandler是一個最高優(yōu)先級的線程螟凭,其邏輯是從Pending-Reference鏈表中取出Reference虚青,添加到其關(guān)聯(lián)的Reference-Queue中。

image.png

再來看些細(xì)節(jié)代碼:

ReferenceQueue VS Reference

Reference作為SoftReference螺男,WeakReference棒厘,PhantomReference,F(xiàn)inalReference這幾個引用類型的父類下隧。主要有兩個字段referent奢人、queue,一個是指所引用的對象淆院,一個是與之對應(yīng)的ReferenceQueue何乎。Reference類有個構(gòu)造函數(shù) Reference(T referent, ReferenceQueue<? super T> queue),可以通過該構(gòu)造函數(shù)傳入與Reference相伴的ReferenceQueue。

ReferenceQueue本身提供隊列的功能支救,有入隊(enqueue)和出隊(poll,remove,其中remove阻塞等待提取隊列元素)抢野。ReferenceQueue對象本身保存了一個Reference類型的head節(jié)點,Reference封裝了next字段各墨,這樣就是可以組成一個單向鏈表指孤。同時ReferenceQueue提供了兩個靜態(tài)字段NULL,ENQUEUED

static ReferenceQueue<Object> NULL = new Null<>();
static ReferenceQueue<Object> ENQUEUED = new Null<>();

這兩個字段的主要功能:NULL是當(dāng)我們構(gòu)造Reference實例時queue傳入null時贬堵,會默認(rèn)使用NULL恃轩,這樣在enqueue時判斷queue是否為NULL,如果為NULL直接返回,入隊失敗黎做。ENQUEUED的作用是防止重復(fù)入隊叉跛,reference后會把其queue字段賦值為ENQUEUED,當(dāng)再次入隊時會直接返回失敗。

 boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
        synchronized (lock) {
            // Check that since getting the lock this reference hasn't already been
            // enqueued (and even then removed)
            ReferenceQueue<?> queue = r.queue;
            if ((queue == NULL) || (queue == ENQUEUED)) {
                return false;
            }
            assert queue == this;
            r.queue = ENQUEUED;
            r.next = (head == null) ? r : head;
            head = r;
            queueLength++;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(1);
            }
            lock.notifyAll();
            return true;
        }
    }

Reference與ReferenceQueue之間是如何工作的呢蒸殿?Reference里有個靜態(tài)字段pending筷厘,同時還通過靜態(tài)代碼塊啟動了Reference-handler thread。當(dāng)一個Reference的referent被回收時, 垃圾回收器會把reference添加到pending這個鏈表里,然后Reference-handler thread不斷的讀取pending中的reference伟桅,把它加入到對應(yīng)的ReferenceQueue中, 我們可以通過下面代碼塊來進行把SoftReference敞掘,WeakReference叽掘,PhantomReference與ReferenceQueue聯(lián)合使用來驗證這個機制楣铁。為了確保SoftReference在每次gc后,其引用的referent都被回收更扁,我們需要加入-XX:SoftRefLRUPolicyMSPerMB=0參數(shù)盖腕,

通過jstack命令可以看到對應(yīng)的Reference Handler thread

"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007f8fb2836800 nid=0x2e03 in Object.wait() [0x000070000082b000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000740008878> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x0000000740008878> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

因此可以看出,當(dāng)reference與referenQueue聯(lián)合使用的主要作用就是當(dāng)reference指向的referent回收時(或者要被回收 如下文要講的Finalizer)浓镜,提供一種通知機制溃列,通過queue取到這些reference,來做額外的處理工作膛薛。當(dāng)然听隐,如果我們不需要這種通知機制,我們就不用傳入額外的queue,默認(rèn)使用NULL queue就會入隊失敗哄啄。

SoftReference

根據(jù)上面我們講的對象可達性原理雅任,我們把一個對象存在根對象對其有直接或間接的SoftReference,并沒有其他強引用路徑咨跌,我們把該對象成為softly-reachable對象沪么。JVM保證在拋出OutOfMemoryError前會回收這些softly-reachable對象。JVM會根據(jù)當(dāng)前內(nèi)存的情況來決定是否回收softly-reachable對象锌半,但只要referent有強引用存在禽车,該referent就一定不會被清理,因此SoftReference適合用來實現(xiàn)memory-sensitive caches。軟引用的回收策略在不同的JVM實現(xiàn)會略有不同殉摔,javadoc中說明:

Virtual machine implementations are, however, encouraged to bias against clearing recently-created or recently-used soft references.

也就是說JVM不僅僅只會考慮當(dāng)前內(nèi)存情況州胳,還會考慮軟引用所指向的referent最近使用情況和創(chuàng)建時間來綜合決定是否回收該referent。

Hotspot在gc時會根據(jù)兩個標(biāo)準(zhǔn)來回收:

  • 根據(jù)SoftReference引用實例的timestamp(每次調(diào)用softReference.get()會自動更新該字段逸月,把最近一次垃圾回收時間賦值給timestamp,見源碼)
  • 當(dāng)前JVM heap的內(nèi)存剩余(free_heap)情況

計算的規(guī)則是:

  • free_heap 表示當(dāng)前堆剩余的內(nèi)存陋葡,單位是MB
  • interval 表示最近一次GC's clock 和 當(dāng)前我們要判斷的softReference的timestamp 差值
  • ms_per_mb is a constant number of milliseconds to keep around a SoftReference for each free megabyte in the heap(可以通過-XX:SoftRefLRUPolicyMSPerMB來設(shè)定)

那么判斷依據(jù)就是: interval <= free_heap * ms_per_mb,如果為true,則保留,false則進行對象清除彻采。_ ** SoftReferences will always be kept for at least one GC after their last access腐缤。**_ 因為 只要調(diào)用一次,那么clock和timestamp的值就會一樣肛响,clock-timestamp則為0岭粤,一定小于等于free_heap * ms_per_mb。 OpenJDK的大概referencePolicy.cpp代碼是:

void LRUMaxHeapPolicy::setup() {
  size_t max_heap = MaxHeapSize;
  max_heap -= Universe::get_heap_used_at_last_gc();
  max_heap /= M;

  _max_interval = max_heap * SoftRefLRUPolicyMSPerMB;
  assert(_max_interval >= 0,"Sanity check");
}

bool LRUMaxHeapPolicy::should_clear_reference(oop p,
                                             jlong timestamp_clock) {
  jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
  assert(interval >= 0, "Sanity check");

  // The interval will be zero if the ref was accessed since the last scavenge/gc.
  if(interval <= _max_interval) {
    return false;
  }

  return true;
}

可見特笋,SoftReference在一定程度上會影響JVM GC的剃浇,例如softly-reachable對應(yīng)的referent多次垃圾回收仍然不滿足釋放條件,那么它會停留在heap old區(qū)猎物,占據(jù)很大部分空間虎囚,在JVM沒有拋出OutOfMemoryError前,它有可能會導(dǎo)致頻繁的Full GC蔫磨。

WeakReference

當(dāng)一個對象被WeakReference引用時淘讥,處于weakly-reachable狀態(tài)時,只要發(fā)生GC時堤如,就會被清除蒲列,同時會把WeakReference注冊到引用隊列中(如果存在的話)。 WeakReference不阻礙或影響它們對應(yīng)的referent被終結(jié)(finalized)和回收(reclaimed)搀罢,因此蝗岖,WeakReference經(jīng)常被用作實現(xiàn)規(guī)范映射(canonicalizing mappings)。相比SoftReference來說榔至,WeakReference對JVM GC幾乎是沒有影響的抵赢。

下面我們舉個WeakReference應(yīng)用場景,JDK自帶的WeakHashMap唧取,我們用下面的代碼來測試查看WeakHashMap在gc后的entry的情況铅鲤,加入-verbose:gc運行。

/**
 * 加入下面參數(shù)兵怯,觀察gc情況
 * -verbose:gc
 */
public class WeakHashMapTest {

    private static Map<String,byte[]> caches=new WeakHashMap<>();

    public static void main(String[]args) throws InterruptedException {
        for (int i=0;i<100000;i++){
            caches.put(i+"",new byte[1024*1024*10]);
            System.out.println("put num: " + i + " but caches size:" + caches.size());
        }
    }
}

運行代碼我們可以看到彩匕,雖然我們不斷的往caches中put元素,但是caches size會伴隨每次gc又從0開始了媒区。

WeakHashMap實現(xiàn)原理很簡單驼仪,它除了實現(xiàn)標(biāo)準(zhǔn)的Map接口掸犬,里面的機制也和HashMap的實現(xiàn)類似。從它entry子類中可以看出绪爸,它的key是用WeakReference包裹住的湾碎。當(dāng)這個key對象本身不再被使用時,伴隨著GC的發(fā)生奠货,會自動把該key對應(yīng)的entry都在Map中清除掉介褥。它為啥能夠自動清除呢?這就是利用上面我們講的ReferenceQueue VS Reference的原理递惋。WeakHashMap里聲明了一個queue柔滔,Entry繼承WeakReference,構(gòu)造函數(shù)中用key和queue關(guān)聯(lián)構(gòu)造一個weakReference,當(dāng)key不再被使用gc后會自動把把key注冊到queue中:

 /**
     * Reference queue for cleared WeakEntries
     */
    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

   /**
     * The entries in this hash table extend WeakReference, using its main ref
     * field as the key.
     */
    private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> next;

        /**
         * Creates new entry.
         */
        Entry(Object key, V value,
              ReferenceQueue<Object> queue,
              int hash, Entry<K,V> next) {
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
       //代碼省略
    }
}

WeakHashMap關(guān)鍵的清理entry代碼:

/**
     * Expunges stale entries from the table.
     */
    private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                    Entry<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);

                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                while (p != null) {
                    Entry<K,V> next = p.next;
                    if (p == e) {
                        if (prev == e)
                            table[i] = next;
                        else
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }

這段代碼會在resize,getTable,size里執(zhí)行,清除失效的entry萍虽。

PhantomReference

PhantomReference 不同于WeakReference睛廊、SoftReference,它存在的意義不是為了獲取referent,因為你也永遠(yuǎn)獲取不到杉编,因為它的get如下

public T get() {
        return null;
 }

PhantomReference主要作為其指向的referent被回收時的一種通知機制,它就是利用上文講到的ReferenceQueue實現(xiàn)的超全。當(dāng)referent被gc回收時,JVM自動把PhantomReference對象(reference)本身加入到ReferenceQueue中邓馒,像發(fā)出信號通知一樣嘶朱,表明該reference指向的referent被回收。然后可以通過去queue中取到reference光酣,此時說明其指向的referent已經(jīng)被回收疏遏,可以通過這個通知機制來做額外的清場工作。 因此有些情況可以用PhantomReference 代替finalize()挂疆,做資源釋放更明智改览。

下面舉個例子下翎,用PhantomReference來自動關(guān)閉文件流缤言。

public class ResourcePhantomReference<T> extends PhantomReference<T> {

    private List<Closeable> closeables;

    public ResourcePhantomReference(T referent, ReferenceQueue<? super T> q, List<Closeable> resource) {
        super(referent, q);
        closeables = resource;
    }

    public void cleanUp() {
        if (closeables == null || closeables.size() == 0)
            return;
        for (Closeable closeable : closeables) {
            try {
                closeable.close();
                System.out.println("clean up:"+closeable);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ResourceCloseDeamon extends Thread {

    private static ReferenceQueue QUEUE = new ReferenceQueue();

    //保持對reference的引用,防止reference本身被回收
    private static List<Reference> references=new ArrayList<>();
    @Override
    public void run() {
        this.setName("ResourceCloseDeamon");
        while (true) {
            try {
                ResourcePhantomReference reference = (ResourcePhantomReference) QUEUE.remove();
                reference.cleanUp();
                references.remove(reference);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void register(Object referent, List<Closeable> closeables) {
        references.add(new ResourcePhantomReference(referent,QUEUE,closeables));
    }


}
public class FileOperation {

    private FileOutputStream outputStream;

    private FileInputStream inputStream;

    public FileOperation(FileInputStream inputStream, FileOutputStream outputStream) {
        this.outputStream = outputStream;
        this.inputStream = inputStream;
    }

    public void operate() {
        try {
            inputStream.getChannel().transferTo(0, inputStream.getChannel().size(), outputStream.getChannel());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


}

public class PhantomTest {

    public static void main(String[] args) throws Exception {
        //打開回收
        ResourceCloseDeamon deamon = new ResourceCloseDeamon();
        deamon.setDaemon(true);
        deamon.start();

        // touch a.txt b.txt
        // echo "hello" > a.txt

        //保留對象,防止gc把stream回收掉,其不到演示效果
        List<Closeable> all=new ArrayList<>();
        FileInputStream inputStream;
        FileOutputStream outputStream;

        for (int i = 0; i < 100000; i++) {
            inputStream = new FileInputStream("/Users/robin/a.txt");
            outputStream = new FileOutputStream("/Users/robin/b.txt");
            FileOperation operation = new FileOperation(inputStream, outputStream);
            operation.operate();
            TimeUnit.MILLISECONDS.sleep(100);

            List<Closeable>closeables=new ArrayList<>();
            closeables.add(inputStream);
            closeables.add(outputStream);
            all.addAll(closeables);
            ResourceCloseDeamon.register(operation,closeables);
            //用下面命令查看文件句柄,如果把上面register注釋掉,就會發(fā)現(xiàn)句柄數(shù)量不斷上升
            //jps | grep PhantomTest | awk '{print $1}' |head -1 | xargs  lsof -p  | grep /User/robin
            System.gc();

        }


    }

運行上面的代碼,通過jps | grep PhantomTest | awk '{print $1}' |head -1 | xargs lsof -p | grep /User/robin | wc -l 可以看到句柄沒有上升视事,而去掉ResourceCloseDeamon.register(operation,closeables);時胆萧,句柄就不會被釋放。

PhantomReference使用時一定要傳一個referenceQueue,當(dāng)然也可以傳null,但是這樣就毫無意義了俐东。因為PhantomReference的get結(jié)果為null,如果在把queue設(shè)為null,那么在其指向的referent被回收時跌穗,reference本身將永遠(yuǎn)不會可能被加入隊列中.

FinalReference

FinalReference 引用類型主要是為虛擬機提供的,提供 _ 對象被gc前需要執(zhí)行finalize方法的對象_ 的機制虏辫。

FinalReference 很簡單就是extend Reference類蚌吸,沒有做其他邏輯,只是把訪問權(quán)限改為package,因此我們是無法直接使用的砌庄。Finalizer類是我們要講的重點羹唠,它繼承了FinalReference奕枢,并且是final 類型的。Finalize實現(xiàn)很簡單佩微,也是利用上面我們講的ReferenceQueue VS Reference機制缝彬。

FinalizerThread

Finalizer靜態(tài)代碼塊里啟動了一個deamon線程,我們通過jstack命令查看線程時哺眯,總會看到一個Finalizer線程谷浅,就是這個原因:

 static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread finalizer = new FinalizerThread(tg);
        finalizer.setPriority(Thread.MAX_PRIORITY - 2);
        finalizer.setDaemon(true);
        finalizer.start();
    }

FinalizerThread run方法是不斷的從queue中去取Finalizer類型的reference,然后執(zhí)行runFinalizer釋放方法奶卓。

 public void run() {
            if (running)
                return;

            // Finalizer thread starts before System.initializeSystemClass
            // is called.  Wait until JavaLangAccess is available
            while (!VM.isBooted()) {
                // delay until VM completes initialization
                try {
                    VM.awaitBooted();
                } catch (InterruptedException x) {
                    // ignore and continue
                }
            }
            final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
            running = true;
            for (;;) {
                try {
                    Finalizer f = (Finalizer)queue.remove();
                    f.runFinalizer(jla);
                } catch (InterruptedException x) {
                    // ignore and continue
                }
            }
       }

runFinalizer方法體一疯,執(zhí)行事發(fā)邏輯,可以看出如果finalize方法中拋出異常會被直接吃掉:

  private void runFinalizer(JavaLangAccess jla) {
        synchronized (this) {
            if (hasBeenFinalized()) return;
            remove();
        }
        try {
            Object finalizee = this.get();
            if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
                jla.invokeFinalize(finalizee);

                /* Clear stack slot containing this variable, to decrease
                   the chances of false retention with a conservative GC */
                finalizee = null;
            }
        } catch (Throwable x) { }
        super.clear();
    }

介紹完上面的處理機制,那么剩下的就是入queue的事情夺姑,就是哪些類對象需要入隊违施,何時入隊.

哪些類對象是Finalizer reference類型的referent呢

只要類覆寫了Object 上的finalize方法,方法體非空瑟幕。那么這個類的實例都會被Finalizer引用類型引用的磕蒲。下文中我們簡稱Finalizer 型的referent為finalizee。

何時調(diào)用Finalizer.register生成一個Finalizer類型的reference

Finalizer的構(gòu)造函數(shù)是private的只盹,也就是不能通過new 來生成一個Fianlizer reference辣往。只能通過靜態(tài)的register方法來生成。同時Finalizer有個靜態(tài)字段unfinalized殖卑,維護了一個未執(zhí)行finalize方法的reference列表站削,在構(gòu)造函數(shù)中通過add()方法把Finalizer引用本身加入到unfinalized列表中,同時關(guān)聯(lián)finalizee和queue,實現(xiàn)通知機制孵稽。維護靜態(tài)字段unfinalized的目的是為了一直保持對未未執(zhí)行finalize方法的reference的強引用许起,防止被gc回收掉。

  private static Finalizer unfinalized = null;
    private Finalizer(Object finalizee) {
        super(finalizee, queue);
        add();
    }
    /* Invoked by VM */
    static void register(Object finalizee) {
        new Finalizer(finalizee);
    }
    private void add() {
        synchronized (lock) {
            if (unfinalized != null) {
                this.next = unfinalized;
                unfinalized.prev = this;
            }
            unfinalized = this;
        }
    }

那么register是被VM何時調(diào)用的呢菩鲜?JVM通過VM參數(shù) RegisterFinalizersAtInit 的值來確定何時調(diào)用register园细,RegisterFinalizersAtInit默認(rèn)為true,則會在構(gòu)造函數(shù)返回之前調(diào)用call_register_finalizer方法。

void Parse::return_current(Node* value) {
  if (RegisterFinalizersAtInit &&
      method()->intrinsic_id() == vmIntrinsics::_Object_init) {
    call_register_finalizer();
  }
  ..............
}

如果通過-XX:-RegisterFinalizersAtInit 設(shè)為false接校,則會在對象空間分配好之后就調(diào)用call_register_finalizer

nstanceOop InstanceKlass::allocate_instance(TRAPS) {
  bool has_finalizer_flag = has_finalizer(); // Query before possible GC
  int size = size_helper();  // Query before forming handle.

  KlassHandle h_k(THREAD, this);

  instanceOop i;

  i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL);
  if (has_finalizer_flag && !RegisterFinalizersAtInit) {
    i = register_finalizer(i, CHECK_NULL);
  }
  return i;
}

何時入queue
當(dāng)一個finalizee 只剩Finalizer引用猛频,沒有其他引用時,需要被回收了蛛勉,GC就會把該finalizee對應(yīng)的reference放到Finalizer的refereneQueue中,等待FinalizerThread來執(zhí)行finalizee的finalize方法鹿寻,然后finalizee對象才能被GC回收。

Finalizer問題

  1. finalizee對象在finalize重新被賦給一個強引用復(fù)活诽凌,那么下次GC前會不會被再次執(zhí)行finalize方法?

答案是不會的毡熏,runFinalizer中會把該finalizee對應(yīng)的Finalizer引用從unfinalized隊列中移除,第二次執(zhí)行的時會通過hasBeenFinalized方法判斷侣诵,保證不會被重復(fù)執(zhí)行痢法。

 private void runFinalizer(JavaLangAccess jla) {
        synchronized (this) {
            if (hasBeenFinalized()) return;
            remove();
        }
        try {
            Object finalizee = this.get();
            if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
                jla.invokeFinalize(finalizee);

                /* Clear stack slot containing this variable, to decrease
                   the chances of false retention with a conservative GC */
                finalizee = null;
            }
        } catch (Throwable x) { }
        super.clear();
    }
  1. finalizee至少兩次GC回收才可能被回收恬试?
    第一次GC把finalizee對應(yīng)的Finalizer reference加入referenceQueue等待FinalizerThread來執(zhí)行finalize方法。第二次GC才有可能釋放finalizee對象本身疯暑,前提是FinalizerThread已經(jīng)執(zhí)行完finalize方法了训柴,并把Finalizer reference從Finalizer靜態(tài)unfinalized鏈表中剔除,因為這個鏈表和Finalizer reference對finalizee構(gòu)成的是一個強引用妇拯。

  2. Finalizer 機制導(dǎo)致JVM Full GC 頻繁幻馁,stop-the-world延長?

因為如果finalizee上的finalize方法體執(zhí)行過程耗時比較長越锈,會導(dǎo)致對象一直堆積仗嗦,多次GC仍不能釋放,沖進old區(qū)甘凭,造成Old區(qū)GC過程延長稀拐,暫停時間增加,可能頻繁觸發(fā)Full GC丹弱。

小結(jié)

通過對SoftReference德撬,WeakReference,PhantomReference躲胳,F(xiàn)inalReference 的介紹蜓洪,可以看出JDK提供這些類型的reference 主要是用來和GC交互的,根據(jù)reference的不同坯苹,讓JVM采用不同策略來進行對對象的回收(reclaim)隆檀。softly-reachable的referent在保證在OutOfMemoryError之前回收對象,weakly-reachable的referent在發(fā)生GC時就會被回收粹湃,finalizer型的reference 主要提供GC前對referent進行finalize執(zhí)行機制恐仑。同時這些reference和referenceQueue在一起提供通知機制,PhantomReference的作用就是僅僅就是提供對象回收通知機制为鳄,F(xiàn)inalizer借助這種機制實現(xiàn)referent的finalize執(zhí)行裳仆,SoftReference、WeakReference也可以配合referenceQueue使用济赎,實現(xiàn)對象回收通知機制鉴逞。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市司训,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌液南,老刑警劉巖壳猜,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異滑凉,居然都是意外死亡统扳,警方通過查閱死者的電腦和手機喘帚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來咒钟,“玉大人吹由,你說我怎么就攤上這事≈熳欤” “怎么了倾鲫?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長萍嬉。 經(jīng)常有香客問我乌昔,道長,這世上最難降的妖魔是什么壤追? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任磕道,我火速辦了婚禮,結(jié)果婚禮上行冰,老公的妹妹穿的比我還像新娘溺蕉。我一直安慰自己,他們只是感情好悼做,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布焙贷。 她就那樣靜靜地躺著,像睡著了一般贿堰。 火紅的嫁衣襯著肌膚如雪辙芍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天羹与,我揣著相機與錄音故硅,去河邊找鬼。 笑死纵搁,一個胖子當(dāng)著我的面吹牛吃衅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播腾誉,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼徘层,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了利职?” 一聲冷哼從身側(cè)響起趣效,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎猪贪,沒想到半個月后跷敬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡热押,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年西傀,在試婚紗的時候發(fā)現(xiàn)自己被綠了斤寇。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡拥褂,死狀恐怖娘锁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情饺鹃,我是刑警寧澤莫秆,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站尤慰,受9級特大地震影響馏锡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜伟端,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一杯道、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧责蝠,春花似錦党巾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至肴敛,卻和暖如春署海,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背医男。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工砸狞, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人镀梭。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓刀森,卻偏偏與公主長得像,于是被迫代替她去往敵國和親报账。 傳聞我的和親對象是個殘疾皇子研底,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

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

  • 感知GC。怎么感知:* 通過get來判斷已經(jīng)被GC(PhantomReference 在任何時候get都是null...
    YDDMAX_Y閱讀 1,830評論 0 4
  • 引用類型 JDK1.2之后透罢,Java擴充了引用的概念榜晦,將引用分為強引用、軟引用琐凭、弱引用和虛引用四種芽隆。 強引用類似于...
    德彪閱讀 4,401評論 0 10
  • java.lang.ref 該包下提供了Reference相關(guān)的類,包括基類Reference统屈,三個子類WeakR...
    chandarlee閱讀 2,243評論 1 50
  • JDK1.2之后胚吁,Java擴充了引用的概念,將引用分為強引用愁憔、軟引用腕扶、弱引用和虛引用四種。 強引用類似于”O(jiān)bje...
    lesline閱讀 4,869評論 0 0
  • 1 Java的引用 對于Java中的垃圾回收機制來說吨掌,對象是否被應(yīng)該回收的取決于該對象是否被引用半抱。因此,引用也是J...
    高級java架構(gòu)師閱讀 385評論 0 1