Java中Reference的实现机制

2023-03-15 13:42:51 浏览数 (2)

本文将从源码角度分析Java中Reference的实现机制。OpenJDK版本:

➜ jdk hg id 76072a077ee1 jdk-11 28

Java中的Reference机制基本上都是围绕Java类java.lang.ref.Reference来实现的,其子类有

java.lang.ref.SoftReference

java.lang.ref.WeakReference

java.lang.ref.PhantomReference

java.lang.ref.FinalReference

其中 java.lang.ref.FinalReference 类仅供JDK内部使用,和 java.lang.ref.Finalizer 类一起用于实现 Object.finalize 方法的调用(该方法现已不推荐使用)。

其他三个类分别对应了Java对象的三种 reachability 级别。其适用场景可以参考 Java的API文档:

Soft references are for implementing memory-sensitive caches, weak references are for implementing canonicalizing mappings that do not prevent their keys (or values) from being reclaimed, and phantom references are for scheduling post-mortem cleanup actions.

我们首先看下java.lang.ref.Reference类的构造函数:

代码语言:javascript复制
private T referent;         /* Treated specially by GC */


/* The queue this reference gets enqueued to by GC notification or by
 * calling enqueue()...
 */
volatile ReferenceQueue<? super T> queue;


/**
 * Returns this reference object's referent.  If this reference object has
 * been cleared, either by the program or by the garbage collector, then
 * this method returns <code>null</code>...
 */
public T get() {
    return this.referent;
}


Reference(T referent) {
    this(referent, null);
}


Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

由上面代码我们可以看到,在创建Reference时,我们可以传入referent和queue参数。Reference虽然持有referent的引用,但由于其特殊性,它并不能阻止referent被GC回收。当referent被GC回收之后,如果queue不为null,该Reference对象会被放到这个queue里。

接下来,我们再来看下java.lang.ref.Reference类的static代码块

代码语言:javascript复制
static {
    ...
    Thread handler = new ReferenceHandler(tg, "Reference Handler");
    ...
    handler.setPriority(Thread.MAX_PRIORITY);
    handler.setDaemon(true);
    handler.start();
    ...
}

该static代码块启动了一个名为Reference Handler的线程。我们可以用jstack命令确认下它是存在的

代码语言:javascript复制
➜  ~ jstack 3656
...
"Reference Handler" #2 daemon prio=10 os_prio=0 cpu=0.33ms elapsed=49.35s tid=0x00007f15b0124800 nid=0x770e waiting on condition  [0x00007f159433b000]
   java.lang.Thread.State: RUNNABLE
  at java.lang.ref.Reference.waitForReferencePendingList(java.base@11/Native Method)
  at java.lang.ref.Reference.processPendingReferences(java.base@11/Reference.java:241)
  at java.lang.ref.Reference$ReferenceHandler.run(java.base@11/Reference.java:213)
...

由上可见,该线程确实是存在的。我们再看下该线程干了什么事

代码语言:javascript复制
/* High-priority thread to enqueue pending References
 */
private static class ReferenceHandler extends Thread {
    ...
    public void run() {
        while (true) {
            processPendingReferences();
        }
    }
}

该线程会一直调用processPendingReferences 方法,直到该Java进程关闭。看下该方法

代码语言:javascript复制
/*
 * Atomically get and clear (set to null) the VM's pending-Reference list.
 */
private static native Reference<Object> getAndClearReferencePendingList();
...
/*
 * Wait until the VM's pending-Reference list may be non-null.
 */
private static native void waitForReferencePendingList();
...
private static void processPendingReferences() {
    ...
    waitForReferencePendingList();
    Reference<Object> pendingList;
    synchronized (processPendingLock) {
        pendingList = getAndClearReferencePendingList();
        ...
    }
    while (pendingList != null) {
        Reference<Object> ref = pendingList;
        pendingList = ref.discovered;
        ref.discovered = null;


        if (ref instanceof Cleaner) {
           ((Cleaner)ref).clean();
           ...
        } else {
            ReferenceQueue<? super Object> q = ref.queue;
            if (q != ReferenceQueue.NULL) q.enqueue(ref);
        }
    }
    ...
}

其逻辑也比较简单,该方法会先调用waitForReferencePendingList方法,阻塞等待,直到pending-Reference list不为空,接着调用getAndClearReferencePendingList方法获取这个list,最后遍历这个list中的所有Reference对象,如果这个Reference对象的queue字段不为null,就把这个Reference对象enqueue到这个queue中。

接下来我们再看下waitForReferencePendingList和getAndClearReferencePendingList这两个native方法。

C文件src/java.base/share/native/libjava/Reference.c

代码语言:javascript复制
JNIEXPORT jobject JNICALL
Java_java_lang_ref_Reference_getAndClearReferencePendingList(JNIEnv *env, jclass ignore)
{
    return JVM_GetAndClearReferencePendingList(env);
}


JNIEXPORT void JNICALL
Java_java_lang_ref_Reference_waitForReferencePendingList(JNIEnv *env, jclass ignore)
{
    JVM_WaitForReferencePendingList(env);
}

继续看下对应的JVM方法

C 文件src/hotspot/share/prims/jvm.cpp

代码语言:javascript复制
JVM_ENTRY(jobject, JVM_GetAndClearReferencePendingList(JNIEnv* env))
  JVMWrapper("JVM_GetAndClearReferencePendingList");


  MonitorLockerEx ml(Heap_lock);
  oop ref = Universe::reference_pending_list();
  if (ref != NULL) {
    Universe::set_reference_pending_list(NULL);
  }
  return JNIHandles::make_local(env, ref);
JVM_END


JVM_ENTRY(void, JVM_WaitForReferencePendingList(JNIEnv* env))
  JVMWrapper("JVM_WaitForReferencePendingList");
  MonitorLockerEx ml(Heap_lock);
  while (!Universe::has_reference_pending_list()) {
    ml.wait();
  }
JVM_END

继续找到对应的Universe中的方法

C 文件src/hotspot/share/memory/universe.cpp

代码语言:javascript复制
oop Universe::reference_pending_list() {
  ...
  return _reference_pending_list;
}


void Universe::set_reference_pending_list(oop list) {
  ...
  _reference_pending_list = list;
}


bool Universe::has_reference_pending_list() {
  ...
  return _reference_pending_list != NULL;
}

由上可见,这些方法最终都使用了_reference_pending_list字段,我们再看下这个字段的定义

C 文件src/hotspot/share/memory/universe.hpp

代码语言:javascript复制
// References waiting to be transferred to the ReferenceHandler
static oop          _reference_pending_list;

根据这个字段的定义及其注释可知,该字段持有的oop列表,就是最终要传给Reference Handler线程的pending-Reference list。

至此,_reference_pending_list的处理部分都已经很清晰了。

接下来我们看下这些Reference又是怎样被添加到这个_reference_pending_list里的。

先说下大致流程,JVM在每一次的GC过程中,都会通过一定的方式,找到当前存活的java.lang.ref.Reference对象及其子类对象,根据Reference对象的 reachability 级别判断其字段referent引用的对象是否应该继续存活,如果不应该,referent字段所指的对象就会被垃圾回收,而该referent字段也会被置为null,最后,这个Reference对象会被添加到 _reference_pending_list 列表中。

具体流程我们来看下代码。

以G1 GC为例,不管Young GC, Concurrent Mark, 还是Full GC,在找到存活的Reference对象后,都会调用下面方法对Reference进行处理

C 文件src/hotspot/share/gc/shared/referenceProcessor.cpp

代码语言:javascript复制
ReferenceProcessorStats ReferenceProcessor::process_discovered_references(
  BoolObjectClosure*            is_alive,
  OopClosure*                   keep_alive,
  VoidClosure*                  complete_gc,
  AbstractRefProcTaskExecutor*  task_executor,
  ReferenceProcessorPhaseTimes* phase_times) {
  ...
  {
    RefProcTotalPhaseTimesTracker tt(RefPhase1, phase_times, this);
    process_soft_ref_reconsider(is_alive, keep_alive, complete_gc,
                                task_executor, phase_times);
  }
  ...
  {
    RefProcTotalPhaseTimesTracker tt(RefPhase2, phase_times, this);
    process_soft_weak_final_refs(is_alive, keep_alive, complete_gc, task_executor, phase_times);
  }


  {
    RefProcTotalPhaseTimesTracker tt(RefPhase3, phase_times, this);
    process_final_keep_alive(keep_alive, complete_gc, task_executor, phase_times);
  }


  {
    RefProcTotalPhaseTimesTracker tt(RefPhase4, phase_times, this);
    process_phantom_refs(is_alive, keep_alive, complete_gc, task_executor, phase_times);
  }
  ...
  return stats;
}
由上面代码可以看到,该逻辑分别对soft、weak、phantom和final类型的Reference做了处理。我们以PhantomReference为例,看下其具体是怎么做的。
C  文件src/hotspot/share/gc/shared/referenceProcessor.cpp
void ReferenceProcessor::process_phantom_refs(BoolObjectClosure* is_alive,
                                              OopClosure* keep_alive,
                                              VoidClosure* complete_gc,
                                              AbstractRefProcTaskExecutor* task_executor,
                                              ReferenceProcessorPhaseTimes* phase_times) {
  ...
  if (_processing_is_mt) {
    ...
  } else {
    ...
    for (uint i = 0; i < _max_num_queues; i  ) {
      removed  = process_phantom_refs_work(_discoveredPhantomRefs[i], is_alive, keep_alive, complete_gc);
    }
    ...
  }
  ...
}

该方法最终调用了process_phantom_refs_work方法,继续看下这个方法

代码语言:javascript复制
size_t ReferenceProcessor::process_phantom_refs_work(DiscoveredList&    refs_list,
                                          BoolObjectClosure* is_alive,
                                          OopClosure*        keep_alive,
                                          VoidClosure*       complete_gc) {
  DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
  while (iter.has_next()) {
    iter.load_ptrs(...);
    oop const referent = iter.referent();
    if (referent == NULL || iter.is_referent_alive()) {
      ...
      iter.remove();
      iter.move_to_next();
    } else {
      iter.clear_referent();
      ...
      iter.next();
    }
  }
  iter.complete_enqueue();
  ...
  refs_list.clear();
  return iter.removed();
}

在该方法中,refs_list参数表示这次GC检测到的所有存活的PhantomReference对象。该方法会遍历这些对象,并检查其持有的referent是否存活,如果存活,就从该list中移除,如果不存活,就把该PhantomReference对象的referent字段设置为null,然后再处理下一个。

在所有PhantomReference对象都处理完之后,此时refs_list队列中存放的都是已经被GC回收了referent对象的PhantomReference对象,且PhantomReference对象的referent字段都已经为null。

最后再调用iter.complete_enqueue()方法,这个方法会把refs_list中所有的Reference全都加到_reference_pending_list队列中,看下具体代码

代码语言:javascript复制
void DiscoveredListIterator::complete_enqueue() {
  if (_prev_discovered != NULL) {
    ...
    oop old = Universe::swap_reference_pending_list(_refs_list.head());
    HeapAccess<AS_NO_KEEPALIVE>::oop_store_at(_prev_discovered, java_lang_ref_Reference::discovered_offset, old);
  }
}

再看下 Universe::swap_reference_pending_list方法的实现。

C 文件src/hotspot/share/memory/universe.cpp

代码语言:javascript复制
oop Universe::swap_reference_pending_list(oop list) {
  assert_pll_locked(is_locked);
  return Atomic::xchg(list, &_reference_pending_list);
}

由上面两段代码可以看到,_reference_pending_list字段最终指向了_refs_list列表的head,而_reference_pending_list字段原来指向的列表则被append到_refs_list列表之后,即,在调用了DiscoveredListIterator::complete_enqueue方法之后,_refs_list列表被添加到了_reference_pending_list中。

到现在为止,整个流程算是全部完整了。

我们再梳理下整个流程:

首先,java.lang.ref.Reference类在初始化时,会启动一个Reference Handler线程,该线程会调用waitForReferencePendingList这个native方法阻塞等待,直到JVM中的_reference_pending_list字段不为null。

JVM中,每一次GC过程都会找出当前存活的Reference对象,并检查其引用的referent对象是否存活,如果没有存活了,就会把该Reference对象的referent字段置为null,并把这个Reference对象放到_reference_pending_list列表中。

当_reference_pending_list列表有数据后,JVM会通知Reference Handler线程,使其从等待中返回。之后,该线程会调用getAndClearReferencePendingList这个native方法,获取_reference_pending_list列表,并把_reference_pending_list字段置为null。

获取_reference_pending_list列表后,该线程会遍历这个列表中的所有Reference对象,检查其queue字段是否不为null,如果是,就会把该Reference对象enquene到这个queue中。

最后,业务逻辑检查对应的ReferenceQueue,根据ReferenceQueue返回的Reference对象及其相关字段,决定是否做进一步的处理。

0 人点赞