• 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏吧

虚引用真的不影响对象的生命周期吗?99%的人都搞错了

互联网 diligentman 2周前 (11-22) 8次浏览

Java的四大引用,大家都很熟悉吧:

  • 强应用:正常代码中的引用。一个对象能通过强应用访问到,那它就永远不会被回收
  • 软引用:比强引用弱一级的引用,内存不足时引用指向的对象会被回收
  • 弱引用:比软引用弱一级的引用,下一次GC时指向对象会被回收
  • 虚引用

最后一个虚应用是今天要讨论的。很多文章都是这么写的:

> 一个对象是否有虚引用存在,对其生存不会产生任何影响。

事实上,这个是错的。正确的表述是:

在Java 8以及之前的版本中,在虚引用回收后,虚引用指向的对象才会回收。在Java 9以及更新的版本中,虚引用不会对对象的生存产生任何影响。

一个示例

首先用Java 8,带上-Xmx10m -XX:+HeapDumpOnOutOfMemoryError参数运行如下代码:

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public final class Main {

    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<byte[]> queue = new ReferenceQueue&lt;&gt;();
        PhantomReference<byte[]> ref = new PhantomReference&lt;&gt;(new byte[1024 * 1024 * 5], queue);

        System.out.println(queue.poll());
        System.out.println("第一次gc");
        System.gc();
        Thread.sleep(300L);
        System.out.println(queue.poll());
        System.out.println("第二次gc");
        System.gc();
        byte[] bytes1 = new byte[1024 * 1024 * 6];
        System.out.println("ending");
    }
}

你猜猜结果是什么?

null
第一次gc
java.lang.ref.PhantomReference@6d06d69c
第二次gc
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid17738.hprof ...
Heap dump file created [6153765 bytes in 0.010 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at Main.main(Main.java:17)

也就是说,一个5M的数组,只被虚引用指向了,但是在OOM之前,它也不能被回收。

再看看heapdump:

虚引用真的不影响对象的生命周期吗?99%的人都搞错了

从这张图可以看到,正是由于虚引用的存在,导致这个对象无法回收掉。

再去看看虚引用的文档,里面有这么一段:

> An object that is reachable via phantom references will remain so until all such references are cleared or themselves become unreachable.

翻译过来就是:

被虚引用指向的对象会一直存在,直到这些引用被清除或者这些引用不可达。

也就是说,只要有虚引用指向这个对象,那这个对象就会一直存在。

Java 11下的表现

更加奇怪的是,在Java 11下,用同样的参数运行这个程序,结果如下:

null
第一次gc
java.lang.ref.PhantomReference@5e91993f
第二次gc
ending

没有OOM了。

Java 9 引入的变更

翻了下变更记录,这个变化是在Java 9引入的:

虚引用真的不影响对象的生命周期吗?99%的人都搞错了

修改的代码更是寥寥几行:

--- a/src/share/vm/gc/shared/referenceProcessor.cpp	Thu Dec 24 07:35:18 2015 -0800
+++ b/src/share/vm/gc/shared/referenceProcessor.cpp	Mon Dec 28 13:48:43 2015 -0500
@@ -243,7 +243,7 @@
   // Phantom references
   {
     GCTraceTime(Debug, gc, ref) tt("PhantomReference", gc_timer);
-    process_discovered_reflist(_discoveredPhantomRefs, NULL, false,
+    process_discovered_reflist(_discoveredPhantomRefs, NULL, true,
                                is_alive, keep_alive, complete_gc, task_executor);
 
     // Process cleaners, but include them in phantom timing.  We expect

从代码来看,就是在处理虚引用的时候,将第三个参数clear_referent从false变为了true。

为了理清楚这个逻辑,我们来看看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)
{
  // 省略了无关逻辑

  // 阶段三:
  // 切断剩余引用指向的对象
  if (mt_processing) {
    RefProcPhase3Task phase3(*this, refs_lists, clear_referent, true /*marks_oops_alive*/);
    task_executor-&gt;execute(phase3);
  } else {
    for (uint i = 0; i &lt; _max_num_q; i++) {
      // 我们关注这个逻辑
      process_phase3(refs_lists[i], clear_referent,
                     is_alive, keep_alive, complete_gc);
    }
  }

  return total_list_count;
}

接下来看看process_phase3的逻辑:

void
ReferenceProcessor::process_phase3(DiscoveredList&amp;    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 */));
    // 这儿,如果clear_reference为true,就会清理指向的对象
    //  否则,就会将指向的对象标记为alive
    if (clear_referent) {
      // NULL out referent pointer
      iter.clear_referent();
    } else {
      // keep the referent around
      iter.make_referent_alive();
    }

可以看到,在Java 8之前的逻辑中,会调用make_referent_alive方法,导致虚引用指向的对象无法回收。

而在Java 9之后的逻辑中,会调用clear_referent,回收掉执行的对象。

于此同时,Java 9中,PhantomReference的文档说明也变了:

> Phantom reference objects, which are enqueued after the collector determines that their referents may otherwise be reclaimed.

在确定指向的对象会被回收后,虚引用会被放到队列( ReferenceQueue)中。

为什么Java 8不回收虚引用的对象呢

PhantomReference是为了追踪对象GC、回收对象关联的资源的。在Java 8的实现中,确保对象在真正GC前能被对应的ReferenceQueue处理,所以将对象标记为活跃,不回收对象。

显然,在这种情况下,会导致本可以回收的对象无法回收的问题,所以在Java 9中,确保PhantomReference指向的对象在回收后(而不是原来的回收前),会被对应的ReferenceQueue处理,这样在一定程度上保证了功能,又修复了这个问题。

一些参考

  • 知乎上的一个相关的问题
  • 网友遇到的一个相关的问题
  • 为数不多的能将PhantomReference讲清楚的文章
    </byte[]></byte[]>

喜欢 (0)