PhantomReference 和 WeakReference 究竟有何不同

本文基于 OpenJDK17 进行讨论,垃圾回收器为 ZGC。
提示: 为了方便大家索引,特将在上篇文章 《以 ZGC 为例,谈一谈 JVM 是如何实现 Reference 语义的》 中讨论的众多主题独立出来。
PhantomReference 和 WeakReference 如果仅仅从概念上来说其实很难区别出他们之间究竟有何不同,比如, PhantomReference 是用来跟踪对象是否被垃圾回收的,如果对象被 GC ,那么其对应的 PhantomReference 就会被加入到一个 ReferenceQueue 中,这个 ReferenceQueue 是在创建 PhantomReference 对象的时候注册进去的。
我们在应用程序中可以通过检查这个 ReferenceQueue 中的 PhantomReference 对象,从而可以判断出其引用的 referent 对象已经被回收,随即可以做一些释放资源的工作。
而 WeakReference 的概念是,如果一个对象在 JVM 堆中已经没有任何强引用链或者软引用链了,在只有一个 WeakReference 引用它的情况下,那么这个对象就会被 GC,与其对应的 WeakReference 也会被加入到其注册的 ReferenceQueue 中。后面的套路和 PhantomReference 一模一样。
既然两者在概念上都差不多,JVM 处理的过程也差不多,那么 PhantomReference 可以用来跟踪对象是否被垃圾回收,WeakReference 可不可以跟踪呢 ?
事实上,在大部分情况下 WeakReference 也是可以的,但是在一种特殊的情况下 WeakReference 就不可以了,只能由 PhantomReference 来跟踪对象的回收状态。

上图中,object1 对象在 JVM 堆中被一个 WeakReference 对象和 FinalReference 对象同时引用,除此之外没有任何强引用链和软引用链,根据 FinalReference 的语义,这个 object1 是不是就要被回收了,但为了执行它的 finalize() 方法所以 JVM 会将 object1 复活。
根据 WeakReference 的语义,此时发生了 GC,并且 object1 没有任何强引用链和软引用链,那么此时 JVM 是不是就会将 WeakReference 加入到 _reference_pending_list 中,后面再由 ReferenceHandler 线程转移到 ReferenceQueue 中,等待应用程序的处理。
也就是说在这种情况下,FinalReference 和 WeakReference 在本轮 GC 中,都会被 JVM 处理,但是 object1 却是存活状态,所以 WeakReference 不能跟踪对象的垃圾回收状态。

object2 对象在 JVM 堆中被一个 PhantomReference 对象和 FinalReference 对象同时引用,除此之外没有任何强引用链和软引用链,根据 FinalReference 的语义, JVM 会将 object2 复活。
但根据 PhantomReference 的语义,只有在 object2 要被垃圾回收的时候,JVM 才会将 PhantomReference 加入到 _reference_pending_list 中,但是此时 object2 已经复活了,所以 PhantomReference 这里就不会被加入到 _reference_pending_list 中了。
也就是说在这种情况下,只有 FinalReference 在本轮 GC 中才会被 JVM 处理,随后 FinalizerThread 会调用 Finalizer 对象(FinalReference 类型)的 runFinalizer 方法,最终就会执行到 object2 对象的 finalize() 方法。
当 object2 对象的 finalize() 方法被执行完之后,在下一轮 GC 中就会回收 object2 对象,那么根据 PhantomReference 的语义,PhantomReference 对象只有在下一轮 GC 中才会被 JVM 加入到 _reference_pending_list 中,随后被 ReferenceHandler 线程处理。
所以在这种特殊的情况就只有 PhantomReference 才能用于跟踪对象的垃圾回收状态,而 WeakReference 却不可以。
那 JVM 是如何实现 PhantomReference 和 WeakReference 的这两种语义的呢 ?

首先在 ZGC 的 Concurrent Mark 阶段,GC 线程会将 JVM 堆中所有需要被处理的 Reference 对象加入到一个临时的 _discovered_list 中。
随后在 Concurrent Process Non-Strong References 阶段,GC 会通过 should_drop
方法再次判断 _discovered_list 中存放的这些临时 Reference 对象所引用的 referent 是否存活 ?
如果这些 referent 仍然存活,那么就需要将对应的 Reference 对象从 _discovered_list 中移除。
如果这些 referent 不再存活,那么就将对应的 Reference 对象继续保留在 _discovered_list,最后将 _discovered_list 中的 Reference 对象全部转移到 _reference_pending_list 中,随后唤醒 ReferenceHandler 线程去处理。
PhantomReference 和 WeakReference 的核心区别就在这个 should_drop
方法中:
should_drop 方法主要是用来判断一个被 Reference 引用的 referent 对象是否存活,但是根据 Reference 类型的不同,比如这里的 PhantomReference 和 WeakReference,具体的判断逻辑是不一样的。
根据前面几个小节的内容,我们知道 ZGC 是通过一个 _livemap 标记位图,来标记一个对象的存活状态的,ZGC 会将整个 JVM 堆划分成一个一个的 page,然后从 page 中一个一个的分配对象。每一个 page 结构中有一个 _livemap,用来标记该 page 中所有对象的存活状态。
在 ZGC 中 ZPage 共分为三种类型:
ZPageTypeSmall 尺寸为 2M , SmallZPage 中的对象尺寸按照 8 字节对齐,最大允许的对象尺寸为 256K。
ZPageTypeMedium 尺寸和 MaxHeapSize 有关,一般会设置为 32 M,MediumZPage 中的对象尺寸按照 4K 对齐,最大允许的对象尺寸为 4M。
ZPageTypeLarge 尺寸不定,但需要按照 2M 对齐。如果一个对象的尺寸超过 4M 就需要在 LargeZPage 中分配。
那么 ZPage 中的这个 _livemap 中的 bit 位个数,是不是就应该和一个 ZPage 所能容纳的最大对象个数保持一致,因为一个对象是否存活按理说是不是用一个 bit 就可以表示了 ?
ZPageTypeSmall 中最大能容纳的对象个数为
2M / 8B = 262144
,那么对应的 _livemap 中是不是只要 262144 个 bit 就可以了。ZPageTypeMedium 中最大能容纳的对象个数为
32M / 4K = 8192
,那么对应的 _livemap 中是不是只要 8192 个 bit 就可以了。ZPageTypeLarge 只会容纳一个大对象。在 ZGC 中超过 4M 的就是大对象。
但实际上 ZGC 中的 _livemap 所包含的 bit 个数是在此基础上再乘以 2,也就是说一个对象需要用两个 bit 位来标记。
那 ZGC 为什么要用两个 bit 来标记对象的存活状态呢 ?答案就是为了区分本小节中介绍的这种特殊情况,一个对象是否存活分为两种情况:
对象被 FinalReference 复活,这样 ZGC 会标记第一个低位 bit ——
1
。对象存在强引用链,人家原本就应该存活,这样 ZGC 会将两个 bit 位全部标记 ——
11
。
而在本小节中我们讨论的就是对象在被 FinalReference 复活的情况下,PhantomReference 和 WeakReference 的处理有何不同,了解了这些背景知识之后,那么我们再回头来看 should_drop 方法的判断逻辑:
首先对于 PhantomReference 来说,在 ZGC 的 Concurrent Process Non-Strong References 阶段是通过 ZBarrier::is_alive_barrier_on_phantom_oop
来判断其引用的 referent 对象是否存活的。
我们看到 PhantomReference 判断的是第一个 bit 位是否被标记过,而在 FinalReference 复活 referent 对象的时候标记的就是第一个 bit 位。所以 should_drop 方法返回 true,PhantomReference 从 _discovered_list 中移除。
而对于 WeakReference 来说,却是通过 Barrier::is_alive_barrier_on_weak_oop
来判断其引用的 referent 对象是否存活的。
我们看到 WeakReference 判断的是第二个高位 bit 是否被标记过,所以这种情况下,无论 referent 对象是否被 FinalReference 复活,should_drop 方法都会返回 false 。WeakReference 仍然会保留在 _discovered_list 中,随后和 FinalReference 一起被 ReferenceHandler 线程处理。
所以总结一下他们的核心区别就是:
PhantomReference 对象只有在对象被回收的时候,才会被 ReferenceHandler 线程处理,它会被 FinalReference 影响。
WeakReference 对象只要是发生 GC , 并且它引用的 referent 对象没有任何强引用链或者软引用链的时候,都会被 ReferenceHandler 线程处理,不会被 FinalReference 影响。
版权声明: 本文为 InfoQ 作者【bin的技术小屋】的原创文章。
原文链接:【http://xie.infoq.cn/article/bcfb8b936352522f7bfb61e1d】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论