乐趣区

关于java:Java四种引用类型原理你真的搞明白了吗五分钟带你深入理解

Java 中一共有 4 种援用类型(其实还有一些其余的援用类型比方 FinalReference):强援用、软援用、弱援用、虚援用。

其中强援用就是咱们常常应用的 Object a = new Object(); 这样的模式,在 Java 中并没有对应的 Reference 类。

本篇文章次要是剖析软援用、弱援用、虚援用的实现,这三种援用类型都是继承于 Reference 这个类,次要逻辑也在 Reference 中。

问题

在剖析前,先抛几个问题?

1. 网上大多数文章对于软援用的介绍是:在内存不足的时候才会被回收,那内存不足是怎么定义的?什么才叫内存不足?

2. 网上大多数文章对于虚援用的介绍是:形同虚设,虚援用并不会决定对象的生命周期。次要用来跟踪对象被垃圾回收器回收的流动。真的是这样吗?

3. 虚援用在 Jdk 中有哪些场景下用到了呢?

Reference

咱们先看下 Reference.java 中的几个字段

public abstract class Reference<T> {
    // 援用的对象
    private T referent;        
    // 回收队列,由使用者在 Reference 的构造函数中指定
    volatile ReferenceQueue<? super T> queue;
     // 当该援用被退出到 queue 中的时候,该字段被设置为 queue 中的下一个元素,以造成链表构造
    volatile Reference next;
    // 在 GC 时,JVM 底层会保护一个叫 DiscoveredList 的链表,寄存的是 Reference 对象,discovered 字段指向的就是链表中的下一个元素,由 JVM 设置
    transient private Reference<T> discovered;  
    // 进行线程同步的锁对象
    static private class Lock { }
    private static Lock lock = new Lock();
    // 期待退出 queue 的 Reference 对象,在 GC 时由 JVM 设置,会有一个 java 层的线程 (ReferenceHandler) 源源不断的从 pending 中提取元素退出到 queue
    private static Reference<Object> pending = null;
}

一个 Reference 对象的生命周期如下:

次要分为 Native 层和 Java 层两个局部。

Native 层在 GC 时将须要被回收的 Reference 对象退出到 DiscoveredList 中(代码在 referenceProcessor.cpp 中 process_discovered_references 办法),而后将 DiscoveredList 的元素挪动到 PendingList 中(代码在 referenceProcessor.cpp 中 enqueue_discovered_ref_helper 办法),PendingList 的队首就是 Reference 类中的 pending 对象。

看看 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 对象,则记录下来,上面做非凡解决
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // 指向 PendingList 的下一个对象
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                   // 如果 pending 为 null 就先期待,当有对象退出到 PendingList 中时,jvm 会执行 notify
                    if (waitForNotify) {lock.wait();
                    }
                    // retry if waited
                    return waitForNotify;
                }
            }
        } 
        ...

        // 如果时 CLeaner 对象,则调用 clean 办法进行资源回收
        if (c != null) {c.clean();
            return true;
        }
        // 将 Reference 退出到 ReferenceQueue,开发者能够通过从 ReferenceQueue 中 poll 元素感知到对象被回收的事件。ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
 }

流程比较简单:就是源源不断的从 PendingList 中提取出元素,而后将其退出到 ReferenceQueue 中去,开发者能够通过从 ReferenceQueue 中 poll 元素感知到对象被回收的事件。

另外须要留神的是,对于 Cleaner 类型(继承自虚援用)的对象会有额定的解决:在其指向的对象被回收时,会调用 clean 办法,该办法次要是用来做对应的资源回收,在堆外内存 DirectByteBuffer 中就是用 Cleaner 进行堆外内存的回收,这也是虚援用在 java 中的典型利用。

看完了 Reference 的实现,再看看几个实现类里,各自有什么不同。

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

}

软援用的实现很简略,就多了两个字段:clock 和 timestamp。clock 是个动态变量,每次 GC 时都会将该字段设置成以后工夫。timestamp 字段则会在每次调用 get 办法时将其赋值为 clock(如果不相等且对象没被回收)。

那这两个字段的作用是什么呢?这和软援用在内存不够的时候才被回收,又有什么关系呢?

这些还得看 JVM 的源码才行,因为决定对象是否须要被回收都是在 GC 中实现的。

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)
{
 ...
   // 还记得上文提到过的 DiscoveredList 吗?refs_lists 就是 DiscoveredList。// 对于 DiscoveredList 的解决分为几个阶段,SoftReference 的解决就在第一阶段
 ...
      for (uint i = 0; i < _max_num_q; i++) {process_phase1(refs_lists[i], policy,
                       is_alive, keep_alive, complete_gc);
      }
 ...
}

// 该阶段的次要目标就是当内存足够时,将对应的 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 */));
    // 判断援用的对象是否存活
    bool referent_is_dead = (iter.referent() != NULL) && !iter.is_referent_alive();
    // 如果援用的对象曾经不存活了,则会去调用对应的 ReferencePolicy 判断该对象是不断要被回收
    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 发现的某种援用类型(虚援用、软援用、弱援用等),而 process_discovered_reflist 办法的作用就是将不须要被回收的对象从 refs_lists 移除掉,refs_lists 最初剩下的元素全是须要被回收的元素,最初会将其第一个元素赋值给上文提到过的 Reference.java#pending 字段。

ReferencePolicy 一共有 4 种实现:NeverClearPolicy,AlwaysClearPolicy,LRUCurrentHeapPolicy,LRUMaxHeapPolicy。

其中 NeverClearPolicy 永远返回 false,代表永远不回收 SoftReference,在 JVM 中该类没有被应用,AlwaysClearPolicy 则永远返回 true,在 referenceProcessor.hpp#setup 办法中中能够设置 policy 为 AlwaysClearPolicy,至于什么时候会用到 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 的动态字段 clock,java_lang_ref_SoftReference::timestamp(p)对应是字段 timestamp。如果上次 GC 后有调用 SoftReference#get,interval 值为 0,否则为若干次 GC 之间的时间差。

_max_interval 则代表了一个临界值,它的值在 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 默认为 1000,前者的计算方法和上次 GC 后可用堆大小无关,后者计算方法和(堆大小 - 上次 gc 时堆应用大小)无关。

看到这里你就晓得 SoftReference 到底什么时候被被回收了,它和应用的策略(默认应该是 LRUCurrentHeapPolicy),堆可用大小,该 SoftReference 上一次调用 get 办法的工夫都有关系。

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,没有做任何的改变。那 referent 字段是什么时候被置为 null 的呢?要搞清楚这个问题咱们再看下上文提到过的 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 为软援用的时候,这里 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:
  // 移除所有指向对象还存活的援用
  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:
  // 依据 clear_referent 的值决定是否将不存活对象回收
  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,之后会被 GC 回收
      iter.clear_referent();} else {
      // keep the referent around
      // 标记援用的对象为存活,该对象在这次 GC 将不会被回收
      iter.make_referent_alive();}
    ...
  }
    ...
}

不论是弱援用还是其余援用类型,将字段 referent 置 null 的操作都产生在 process_phase3 中,而具体行为是由 clear_referent 的值决定的。而 clear_referent 的值则和援用类型相干。

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 个字段就是 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);
  }
    ...
}

能够看到,对于 Soft references 和 Weak references clear_referent 字段传入的都是 true,这也合乎咱们的预期:对象不可达后,援用字段就会被置为 null,而后对象就会被回收(对于软援用来说,如果内存足够的话,在 Phase 1,相干的援用就会从 refs_list 中被移除,到 Phase 3 时 refs_list 为空集合)。

但对于 Final references 和 Phantom references,clear_referent 字段传入的是 false,也就意味着被这两种援用类型援用的对象,如果没有其余额定解决,只有 Reference 对象还存活,那援用的对象是不会被回收的。Final references 和对象是否重写了 finalize 办法无关,不在本文剖析范畴之内,咱们接下来看看 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 办法永远返回 null,咱们看个 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 后会将 phanRef 退出到 refQueue 中
        Reference<? extends Object> phanRefP = refQueue.remove();
         // 这里输入 true
        System.out.println(phanRefP==phanRef);
    }

从以上代码中能够看到,虚援用可能在指向对象不可达时失去一个 ’ 告诉 ’(其实所有继承 References 的类都有这个性能),须要留神的是 GC 实现后,phanRef.referent 仍然指向之前创立 Object,也就是说 Object 对象始终没被回收!

而造成这一景象的起因在上一大节开端曾经说了:对于 Final references 和 Phantom references,clear_referent 字段传入的时 false,也就意味着被这两种援用类型援用的对象,如果没有其余额定解决,在 GC 中是不会被回收的。

对于虚援用来说,从 refQueue.remove(); 失去援用对象后,能够调用 clear 办法强行解除援用和对象之间的关系,使得对象下次能够 GC 时能够被回收掉。

End

针对文章结尾提出的几个问题,看完剖析,咱们曾经能给出答复:

1. 咱们常常在网上看到软援用的介绍是:在内存不足的时候才会回收,那内存不足是怎么定义的?为什么才叫内存不足

软援用会在内存不足时被回收,内存不足的定义和该援用对象 get 的工夫以及以后堆可用内存大小都有关系,计算公式在上文中也曾经给出。

2. 网上对于虚援用的介绍是:形同虚设,与其余几种援用都不同,虚援用并不会决定对象的生命周期。次要用来跟踪对象被垃圾回收器回收的流动。真的是这样吗

严格的说,虚援用是会影响对象生命周期的,如果不做任何解决,只有虚援用不被回收,那其援用的对象永远不会被回收。所以一般来说,从 ReferenceQueue 中取得 PhantomReference 对象后,如果 PhantomReference 对象不会被回收的话(比方被其余 GC ROOT 可达的对象援用),须要调用 clear 办法解除 PhantomReference 和其援用对象的援用关系。

3. 虚援用在 Jdk 中有哪些场景下用到了呢

DirectByteBuffer 中是用虚援用的子类 Cleaner.java 来实现堆外内存回收的,后续会写篇文章来说说堆外内存的里里外外。

写在最初

欢送大家关注我的公众号【惊涛骇浪如码】,海量 Java 相干文章,学习材料都会在外面更新,整顿的材料也会放在外面。

感觉写的还不错的就点个赞,加个关注呗!点关注,不迷路,继续更新!!!

退出移动版