Java四種引用類型原理你真的搞明白了嗎?五分鐘帶你深入理解!

Java中一共有4種引用類型(其實(shí)還有一些其他的引用類型比如FinalReference):強(qiáng)引用、軟引用、弱引用、虛引用。

其中強(qiáng)引用就是我們經(jīng)常使用的Object a = new Object(); 這樣的形式,在Java中并沒(méi)有對(duì)應(yīng)的Reference類。

本篇文章主要是分析軟引用、弱引用、虛引用的實(shí)現(xiàn),這三種引用類型都是繼承于Reference這個(gè)類,主要邏輯也在Reference中。

問(wèn)題

在分析前,先拋幾個(gè)問(wèn)題?

1.網(wǎng)上大多數(shù)文章對(duì)于軟引用的介紹是:在內(nèi)存不足的時(shí)候才會(huì)被回收,那內(nèi)存不足是怎么定義的?什么才叫內(nèi)存不足?

2.網(wǎng)上大多數(shù)文章對(duì)于虛引用的介紹是:形同虛設(shè),虛引用并不會(huì)決定對(duì)象的生命周期。主要用來(lái)跟蹤對(duì)象被垃圾回收器回收的活動(dòng)。真的是這樣嗎?

3.虛引用在Jdk中有哪些場(chǎng)景下用到了呢?

Reference

我們先看下Reference.java中的幾個(gè)字段

public abstract class Reference<T> {
    //引用的對(duì)象
    private T referent;        
    //回收隊(duì)列,由使用者在Reference的構(gòu)造函數(shù)中指定
    volatile ReferenceQueue<? super T> queue;
     //當(dāng)該引用被加入到queue中的時(shí)候,該字段被設(shè)置為queue中的下一個(gè)元素,以形成鏈表結(jié)構(gòu)
    volatile Reference next;
    //在GC時(shí),JVM底層會(huì)維護(hù)一個(gè)叫DiscoveredList的鏈表,存放的是Reference對(duì)象,discovered字段指向的就是鏈表中的下一個(gè)元素,由JVM設(shè)置
    transient private Reference<T> discovered;  
    //進(jìn)行線程同步的鎖對(duì)象
    static private class Lock { }
    private static Lock lock = new Lock();
    //等待加入queue的Reference對(duì)象,在GC時(shí)由JVM設(shè)置,會(huì)有一個(gè)java層的線程(ReferenceHandler)源源不斷的從pending中提取元素加入到queue
    private static Reference<Object> pending = null;
}

一個(gè)Reference對(duì)象的生命周期如下:

主要分為Native層和Java層兩個(gè)部分。

Native層在GC時(shí)將需要被回收的Reference對(duì)象加入到DiscoveredList中(代碼在referenceProcessor.cpp中process_discovered_references方法),然后將DiscoveredList的元素移動(dòng)到PendingList中(代碼在referenceProcessor.cpp中enqueue_discovered_ref_helper方法),PendingList的隊(duì)首就是Reference類中的pending對(duì)象。

看看Java層的代碼

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;
                     //如果是Cleaner對(duì)象,則記錄下來(lái),下面做特殊處理
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    //指向PendingList的下一個(gè)對(duì)象
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                   //如果pending為null就先等待,當(dāng)有對(duì)象加入到PendingList中時(shí),jvm會(huì)執(zhí)行notify
                    if (waitForNotify) {
                        lock.wait();
                    }
                    // retry if waited
                    return waitForNotify;
                }
            }
        } 
        ...

        // 如果時(shí)CLeaner對(duì)象,則調(diào)用clean方法進(jìn)行資源回收
        if (c != null) {
            c.clean();
            return true;
        }
        //將Reference加入到ReferenceQueue,開(kāi)發(fā)者可以通過(guò)從ReferenceQueue中poll元素感知到對(duì)象被回收的事件。
        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
 }

流程比較簡(jiǎn)單:就是源源不斷的從PendingList中提取出元素,然后將其加入到ReferenceQueue中去,開(kāi)發(fā)者可以通過(guò)從ReferenceQueue中poll元素感知到對(duì)象被回收的事件。

另外需要注意的是,對(duì)于Cleaner類型(繼承自虛引用)的對(duì)象會(huì)有額外的處理:在其指向的對(duì)象被回收時(shí),會(huì)調(diào)用clean方法,該方法主要是用來(lái)做對(duì)應(yīng)的資源回收,在堆外內(nèi)存DirectByteBuffer中就是用Cleaner進(jìn)行堆外內(nèi)存的回收,這也是虛引用在java中的典型應(yīng)用。

看完了Reference的實(shí)現(xiàn),再看看幾個(gè)實(shí)現(xiàn)類里,各自有什么不同。

SoftReference

public class SoftReference<T> extends Reference<T> {

    static private long clock;

    private long timestamp;

    public SoftReference(T referent) {
        super(referent);
        this.timestamp = clock;
    }

    public SoftReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
        this.timestamp = clock;
    }

    public T get() {
        T o = super.get();
        if (o != null && this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }

}

軟引用的實(shí)現(xiàn)很簡(jiǎn)單,就多了兩個(gè)字段:clock和timestamp。clock是個(gè)靜態(tài)變量,每次GC時(shí)都會(huì)將該字段設(shè)置成當(dāng)前時(shí)間。timestamp字段則會(huì)在每次調(diào)用get方法時(shí)將其賦值為clock(如果不相等且對(duì)象沒(méi)被回收)。

那這兩個(gè)字段的作用是什么呢?這和軟引用在內(nèi)存不夠的時(shí)候才被回收,又有什么關(guān)系呢?

這些還得看JVM的源碼才行,因?yàn)闆Q定對(duì)象是否需要被回收都是在GC中實(shí)現(xiàn)的。

size_t
ReferenceProcessor::process_discovered_reflist(
  DiscoveredList               refs_lists[],
  ReferencePolicy*             policy,
  bool                         clear_referent,
  BoolObjectClosure*           is_alive,
  OopClosure*                  keep_alive,
  VoidClosure*                 complete_gc,
  AbstractRefProcTaskExecutor* task_executor)
{
 ...
   //還記得上文提到過(guò)的DiscoveredList嗎?refs_lists就是DiscoveredList。
   //對(duì)于DiscoveredList的處理分為幾個(gè)階段,SoftReference的處理就在第一階段
 ...
      for (uint i = 0; i < _max_num_q; i++) {
        process_phase1(refs_lists[i], policy,
                       is_alive, keep_alive, complete_gc);
      }
 ...
}

//該階段的主要目的就是當(dāng)內(nèi)存足夠時(shí),將對(duì)應(yīng)的SoftReference從refs_list中移除。
void
ReferenceProcessor::process_phase1(DiscoveredList&    refs_list,
                                   ReferencePolicy*   policy,
                                   BoolObjectClosure* is_alive,
                                   OopClosure*        keep_alive,
                                   VoidClosure*       complete_gc) {

  DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
  // Decide which softly reachable refs should be kept alive.
  while (iter.has_next()) {
    iter.load_ptrs(DEBUG_ONLY(!discovery_is_atomic() /* allow_null_referent */));
    //判斷引用的對(duì)象是否存活
    bool referent_is_dead = (iter.referent() != NULL) && !iter.is_referent_alive();
    //如果引用的對(duì)象已經(jīng)不存活了,則會(huì)去調(diào)用對(duì)應(yīng)的ReferencePolicy判斷該對(duì)象是不時(shí)要被回收
    if (referent_is_dead &&
        !policy->should_clear_reference(iter.obj(), _soft_ref_timestamp_clock)) {
      if (TraceReferenceGC) {
        gclog_or_tty->print_cr("Dropping reference (" INTPTR_FORMAT ": %s"  ") by policy",
                               (void *)iter.obj(), iter.obj()->klass()->internal_name());
      }
      // Remove Reference object from list
      iter.remove();
      // Make the Reference object active again
      iter.make_active();
      // keep the referent around
      iter.make_referent_alive();
      iter.move_to_next();
    } else {
      iter.next();
    }
  }
 ...
}

refs_lists中存放了本次GC發(fā)現(xiàn)的某種引用類型(虛引用、軟引用、弱引用等),而process_discovered_reflist方法的作用就是將不需要被回收的對(duì)象從refs_lists移除掉,refs_lists最后剩下的元素全是需要被回收的元素,最后會(huì)將其第一個(gè)元素賦值給上文提到過(guò)的Reference.java#pending字段。

ReferencePolicy一共有4種實(shí)現(xiàn):NeverClearPolicy,AlwaysClearPolicy,LRUCurrentHeapPolicy,LRUMaxHeapPolicy。

其中NeverClearPolicy永遠(yuǎn)返回false,代表永遠(yuǎn)不回收SoftReference,在JVM中該類沒(méi)有被使用,AlwaysClearPolicy則永遠(yuǎn)返回true,在referenceProcessor.hpp#setup方法中中可以設(shè)置policy為AlwaysClearPolicy,至于什么時(shí)候會(huì)用到AlwaysClearPolicy,大家有興趣可以自行研究。

LRUCurrentHeapPolicy和LRUMaxHeapPolicy的should_clear_reference方法則是完全相同:

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;
}

timestamp_clock就是SoftReference的靜態(tài)字段clock,java_lang_ref_SoftReference::timestamp(p)對(duì)應(yīng)是字段timestamp。如果上次GC后有調(diào)用SoftReference#get,interval值為0,否則為若干次GC之間的時(shí)間差。

_max_interval則代表了一個(gè)臨界值,它的值在LRUCurrentHeapPolicy和LRUMaxHeapPolicy兩種策略中有差異。

void LRUCurrentHeapPolicy::setup() {
  _max_interval = (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB;
  assert(_max_interval >= 0,"Sanity check");
}

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");
}

其中SoftRefLRUPolicyMSPerMB默認(rèn)為1000,前者的計(jì)算方法和上次GC后可用堆大小有關(guān),后者計(jì)算方法和(堆大小-上次gc時(shí)堆使用大?。┯嘘P(guān)。

看到這里你就知道SoftReference到底什么時(shí)候被被回收了,它和使用的策略(默認(rèn)應(yīng)該是LRUCurrentHeapPolicy),堆可用大小,該SoftReference上一次調(diào)用get方法的時(shí)間都有關(guān)系。

WeakReference

public class WeakReference<T> extends Reference<T> {

    public WeakReference(T referent) {
        super(referent);
    }

    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

}

可以看到WeakReference在Java層只是繼承了Reference,沒(méi)有做任何的改動(dòng)。那referent字段是什么時(shí)候被置為null的呢?要搞清楚這個(gè)問(wèn)題我們?cè)倏聪律衔奶岬竭^(guò)的process_discovered_reflist方法:

size_t
ReferenceProcessor::process_discovered_reflist(
  DiscoveredList               refs_lists[],
  ReferencePolicy*             policy,
  bool                         clear_referent,
  BoolObjectClosure*           is_alive,
  OopClosure*                  keep_alive,
  VoidClosure*                 complete_gc,
  AbstractRefProcTaskExecutor* task_executor)
{
 ...

  //Phase 1:將所有不存活但是還不能被回收的軟引用從refs_lists中移除(只有refs_lists為軟引用的時(shí)候,這里policy才不為null)
  if (policy != NULL) {
    if (mt_processing) {
      RefProcPhase1Task phase1(*this, refs_lists, policy, true /*marks_oops_alive*/);
      task_executor->execute(phase1);
    } else {
      for (uint i = 0; i < _max_num_q; i++) {
        process_phase1(refs_lists[i], policy,
                       is_alive, keep_alive, complete_gc);
      }
    }
  } else { // policy == NULL
    assert(refs_lists != _discoveredSoftRefs,
           "Policy must be specified for soft references.");
  }

  // Phase 2:
  // 移除所有指向?qū)ο筮€存活的引用
  if (mt_processing) {
    RefProcPhase2Task phase2(*this, refs_lists, !discovery_is_atomic() /*marks_oops_alive*/);
    task_executor->execute(phase2);
  } else {
    for (uint i = 0; i < _max_num_q; i++) {
      process_phase2(refs_lists[i], is_alive, keep_alive, complete_gc);
    }
  }

  // Phase 3:
  // 根據(jù)clear_referent的值決定是否將不存活對(duì)象回收
  if (mt_processing) {
    RefProcPhase3Task phase3(*this, refs_lists, clear_referent, true /*marks_oops_alive*/);
    task_executor->execute(phase3);
  } else {
    for (uint i = 0; i < _max_num_q; i++) {
      process_phase3(refs_lists[i], clear_referent,
                     is_alive, keep_alive, complete_gc);
    }
  }

  return total_list_count;
}

void
ReferenceProcessor::process_phase3(DiscoveredList&    refs_list,
                                   bool               clear_referent,
                                   BoolObjectClosure* is_alive,
                                   OopClosure*        keep_alive,
                                   VoidClosure*       complete_gc) {
  ResourceMark rm;
  DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
  while (iter.has_next()) {
    iter.update_discovered();
    iter.load_ptrs(DEBUG_ONLY(false /* allow_null_referent */));
    if (clear_referent) {
      // NULL out referent pointer
      //將Reference的referent字段置為null,之后會(huì)被GC回收
      iter.clear_referent();
    } else {
      // keep the referent around
      //標(biāo)記引用的對(duì)象為存活,該對(duì)象在這次GC將不會(huì)被回收
      iter.make_referent_alive();
    }
    ...
  }
    ...
}

不管是弱引用還是其他引用類型,將字段referent置null的操作都發(fā)生在process_phase3中,而具體行為是由clear_referent的值決定的。而clear_referent的值則和引用類型相關(guān)。

ReferenceProcessorStats ReferenceProcessor::process_discovered_references(
  BoolObjectClosure*           is_alive,
  OopClosure*                  keep_alive,
  VoidClosure*                 complete_gc,
  AbstractRefProcTaskExecutor* task_executor,
  GCTimer*                     gc_timer) {
  NOT_PRODUCT(verify_ok_to_handle_reflists());
    ...
  //process_discovered_reflist方法的第3個(gè)字段就是clear_referent
  // Soft references
  size_t soft_count = 0;
  {
    GCTraceTime tt("SoftReference", trace_time, false, gc_timer);
    soft_count =
      process_discovered_reflist(_discoveredSoftRefs, _current_soft_ref_policy, true,
                                 is_alive, keep_alive, complete_gc, task_executor);
  }

  update_soft_ref_master_clock();

  // Weak references
  size_t weak_count = 0;
  {
    GCTraceTime tt("WeakReference", trace_time, false, gc_timer);
    weak_count =
      process_discovered_reflist(_discoveredWeakRefs, NULL, true,
                                 is_alive, keep_alive, complete_gc, task_executor);
  }

  // Final references
  size_t final_count = 0;
  {
    GCTraceTime tt("FinalReference", trace_time, false, gc_timer);
    final_count =
      process_discovered_reflist(_discoveredFinalRefs, NULL, false,
                                 is_alive, keep_alive, complete_gc, task_executor);
  }

  // Phantom references
  size_t phantom_count = 0;
  {
    GCTraceTime tt("PhantomReference", trace_time, false, gc_timer);
    phantom_count =
      process_discovered_reflist(_discoveredPhantomRefs, NULL, false,
                                 is_alive, keep_alive, complete_gc, task_executor);
  }
    ...
}

可以看到,對(duì)于Soft references和Weak references clear_referent字段傳入的都是true,這也符合我們的預(yù)期:對(duì)象不可達(dá)后,引用字段就會(huì)被置為null,然后對(duì)象就會(huì)被回收(對(duì)于軟引用來(lái)說(shuō),如果內(nèi)存足夠的話,在Phase 1,相關(guān)的引用就會(huì)從refs_list中被移除,到Phase 3時(shí)refs_list為空集合)。

但對(duì)于Final references和 Phantom references,clear_referent字段傳入的是false,也就意味著被這兩種引用類型引用的對(duì)象,如果沒(méi)有其他額外處理,只要Reference對(duì)象還存活,那引用的對(duì)象是不會(huì)被回收的。Final references和對(duì)象是否重寫(xiě)了finalize方法有關(guān),不在本文分析范圍之內(nèi),我們接下來(lái)看看Phantom references。

PhantomReference

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

    public T get() {
        return null;
    }

    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

}

可以看到虛引用的get方法永遠(yuǎn)返回null,我們看個(gè)demo。

 public static void demo() throws InterruptedException {
        Object obj = new Object();
        ReferenceQueue<Object> refQueue =new ReferenceQueue<>();
        PhantomReference<Object> phanRef =new PhantomReference<>(obj, refQueue);

        Object objg = phanRef.get();
        //這里拿到的是null
        System.out.println(objg);
        //讓obj變成垃圾
        obj=null;
        System.gc();
        Thread.sleep(3000);
        //gc后會(huì)將phanRef加入到refQueue中
        Reference<? extends Object> phanRefP = refQueue.remove();
         //這里輸出true
        System.out.println(phanRefP==phanRef);
    }

從以上代碼中可以看到,虛引用能夠在指向?qū)ο蟛豢蛇_(dá)時(shí)得到一個(gè)'通知'(其實(shí)所有繼承References的類都有這個(gè)功能),需要注意的是GC完成后,phanRef.referent依然指向之前創(chuàng)建Object,也就是說(shuō)Object對(duì)象一直沒(méi)被回收!

而造成這一現(xiàn)象的原因在上一小節(jié)末尾已經(jīng)說(shuō)了:對(duì)于Final references和 Phantom references,clear_referent字段傳入的時(shí)false,也就意味著被這兩種引用類型引用的對(duì)象,如果沒(méi)有其他額外處理,在GC中是不會(huì)被回收的。

對(duì)于虛引用來(lái)說(shuō),從refQueue.remove();得到引用對(duì)象后,可以調(diào)用clear方法強(qiáng)行解除引用和對(duì)象之間的關(guān)系,使得對(duì)象下次可以GC時(shí)可以被回收掉。

End

針對(duì)文章開(kāi)頭提出的幾個(gè)問(wèn)題,看完分析,我們已經(jīng)能給出回答:

1.我們經(jīng)常在網(wǎng)上看到軟引用的介紹是:在內(nèi)存不足的時(shí)候才會(huì)回收,那內(nèi)存不足是怎么定義的?為什么才叫內(nèi)存不足?

軟引用會(huì)在內(nèi)存不足時(shí)被回收,內(nèi)存不足的定義和該引用對(duì)象get的時(shí)間以及當(dāng)前堆可用內(nèi)存大小都有關(guān)系,計(jì)算公式在上文中也已經(jīng)給出。

2.網(wǎng)上對(duì)于虛引用的介紹是:形同虛設(shè),與其他幾種引用都不同,虛引用并不會(huì)決定對(duì)象的生命周期。主要用來(lái)跟蹤對(duì)象被垃圾回收器回收的活動(dòng)。真的是這樣嗎?

嚴(yán)格的說(shuō),虛引用是會(huì)影響對(duì)象生命周期的,如果不做任何處理,只要虛引用不被回收,那其引用的對(duì)象永遠(yuǎn)不會(huì)被回收。所以一般來(lái)說(shuō),從ReferenceQueue中獲得PhantomReference對(duì)象后,如果PhantomReference對(duì)象不會(huì)被回收的話(比如被其他GC ROOT可達(dá)的對(duì)象引用),需要調(diào)用clear方法解除PhantomReference和其引用對(duì)象的引用關(guān)系。

3.虛引用在Jdk中有哪些場(chǎng)景下用到了呢?

DirectByteBuffer中是用虛引用的子類Cleaner.java來(lái)實(shí)現(xiàn)堆外內(nèi)存回收的,后續(xù)會(huì)寫(xiě)篇文章來(lái)說(shuō)說(shuō)堆外內(nèi)存的里里外外。

寫(xiě)在最后

歡迎大家關(guān)注我的公眾號(hào)【風(fēng)平浪靜如碼】,海量Java相關(guān)文章,學(xué)習(xí)資料都會(huì)在里面更新,整理的資料也會(huì)放在里面。

覺(jué)得寫(xiě)的還不錯(cuò)的就點(diǎn)個(gè)贊,加個(gè)關(guān)注唄!點(diǎn)關(guān)注,不迷路,持續(xù)更新?。。?/p>

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

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

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