偶然发现的 JDK 8 虚引用 BUG

124 阅读3分钟

以前学习Java时,一直有一个疑惑:既生弱应用,何生虚引用?

弱应用和虚引用都不会影响所引用的对象被垃圾回收,而且都可以配合引用队列来使用,那么这两者有什么区别呢?虚引用是不是可以被弱应用完全取代?

为什么ThreadLocalWeakHashMap的实现中用的是弱引用,而Java nio的DirectByteBuffer所使用的Cleaner用的又是虚引用?

查阅资料并实践检验后,得知了弱应用与虚引用的区别:

加入引用队列的时机不同:

  • 弱引用:在GC时发现对象不可达之后,对象执行finalize()之前。
  • 虚引用:在GC时发现对象不可达之后,在对象执行完finalize()且没有达成自救之后或曾经达成自救却又一次进入不可达状态之后。换一句话说,在这个对象已经确认可以被垃圾回收时。

至此疑惑基本被解决了,但是我浏览stackoverflow时看到了一个相关的提问回答,链接是stackoverflow.com/questions/2…

网友Xiao-Feng Li回答中表示:虚引用所引用的对象只有等到虚引用本身被GC,对象自身才可以被GC。

这和我们学到的“虚引用不会影响对象的生命周期”相违背了,我立刻去测试了一下。测试代码如下:

public class JDK8PhantomRefTest {
    static class MyObject {
        byte[] bytes;
    }

    /**
     * 使用jdk1.8,JVM参数设置为-Xmx100M
     */
    public static void main(String[] args) {
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        List<Reference<?>> referenceHolder = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            MyObject obj = new MyObject();
            obj.bytes = new byte[1024 * 1024 * 4];  // 4M
            Reference<MyObject> ref = new PhantomReference<>(obj, queue);
//            Reference<MyObject> ref = new WeakReference<>(obj, queue);
            referenceHolder.add(ref);
        }
    }
}

给JVM分配100M的内存,每次循环创建4M的对象,循环100次,必然触发GC,然而MyObject的对象只会被Reference对象引用,而不会被外界强引用,Reference对象则被referenceHolder强引用。

  • 设置JVM参数-Xmx100M,然后在“Oracle OpenJDK 1.8.0_291”下运行上面的代码,运行结果是堆空间溢出,抛出OOME。(换用更早期版本,比如JDK7运行结果也一样)
  • 用弱应用替换虚引用,将代码中的Reference<MyObject> ref = new PhantomReference<>(obj, queue);替换为Reference<MyObject> ref = new WeakReference<>(obj, queue);,运行结果是程序正常运行结束。

网友Xiao-Feng Li说的是对的,虚引用会阻止对象被GC

如果改用更新的JDK版本,比如JDK11等,那么无论是使用弱应用还是使用虚引用,程序都会正常运行结束。我认为是JDK8及之前版本中的一个BUG。

解析

虚引用会阻止对象被GC的原因:

  • 所有Reference对象都持有成员变量referent,其表示引用本体。
  • 当GC时发现对象不可达,则JVM会自动把WeakReferencereferent置为null,并加入ReferenceQueue
  • 而在JDK8中,JVM忘记把PhantomReferencereferent置为null了,除非应用系统通过反射强行修改成员变量。因此,如果一个对象被虚引用所引用,除非这个虚引用本身,即PhantomReference对象被垃圾回收,否则这个被引用的对象永远不会被垃圾回收。

站在现在2024年的视角来看,finalize机制和虚引用都是Java中失败的设计。