首页 > 其他分享 >PhantomReference 和 WeakReference 究竟有何不同

PhantomReference 和 WeakReference 究竟有何不同

时间:2024-06-19 21:54:36浏览次数:21  
标签:对象 FinalReference 有何 referent WeakReference bit PhantomReference

本文基于 OpenJDK17 进行讨论,垃圾回收器为 ZGC。

提示: 为了方便大家索引,特将在上篇文章 《以 ZGC 为例,谈一谈 JVM 是如何实现 Reference 语义的》 中讨论的众多主题独立出来。


PhantomReference 和 WeakReference 如果仅仅从概念上来说其实很难区别出他们之间究竟有何不同,比如, PhantomReference 是用来跟踪对象是否被垃圾回收的,如果对象被 GC ,那么其对应的 PhantomReference 就会被加入到一个 ReferenceQueue 中,这个 ReferenceQueue 是在创建 PhantomReference 对象的时候注册进去的。

我们在应用程序中可以通过检查这个 ReferenceQueue 中的 PhantomReference 对象,从而可以判断出其引用的 referent 对象已经被回收,随即可以做一些释放资源的工作。

public class PhantomReference<T> extends Reference<T> {
 public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

而 WeakReference 的概念是,如果一个对象在 JVM 堆中已经没有任何强引用链或者软引用链了,在只有一个 WeakReference 引用它的情况下,那么这个对象就会被 GC,与其对应的 WeakReference 也会被加入到其注册的 ReferenceQueue 中。后面的套路和 PhantomReference 一模一样。

既然两者在概念上都差不多,JVM 处理的过程也差不多,那么 PhantomReference 可以用来跟踪对象是否被垃圾回收,WeakReference 可不可以跟踪呢 ?

事实上,在大部分情况下 WeakReference 也是可以的,但是在一种特殊的情况下 WeakReference 就不可以了,只能由 PhantomReference 来跟踪对象的回收状态。

image

上图中,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 不能跟踪对象的垃圾回收状态。

image

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 的这两种语义的呢

image

首先在 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 方法中:

bool ZReferenceProcessor::should_drop(oop reference, ReferenceType type) const {
  // 获取 Reference 所引用的 referent
  const oop referent = reference_referent(reference);
  
  // 如果 referent 仍然存活,那么就会将 Reference 对象移除,不需要被 ReferenceHandler 线程处理
  if (type == REF_PHANTOM) {
    // 针对 PhantomReference 对象的特殊处理
    return ZBarrier::is_alive_barrier_on_phantom_oop(referent);
  } else {
    // 针对 WeakReference 对象的处理
    return ZBarrier::is_alive_barrier_on_weak_oop(referent);
  }
}

should_drop 方法主要是用来判断一个被 Reference 引用的 referent 对象是否存活,但是根据 Reference 类型的不同,比如这里的 PhantomReference 和 WeakReference,具体的判断逻辑是不一样的。

根据前面几个小节的内容,我们知道 ZGC 是通过一个 _livemap 标记位图,来标记一个对象的存活状态的,ZGC 会将整个 JVM 堆划分成一个一个的 page,然后从 page 中一个一个的分配对象。每一个 page 结构中有一个 _livemap,用来标记该 page 中所有对象的存活状态。

class ZPage : public CHeapObj<mtGC> {
private:
  ZLiveMap           _livemap;
}

在 ZGC 中 ZPage 共分为三种类型:

// Page types
const uint8_t     ZPageTypeSmall                = 0;
const uint8_t     ZPageTypeMedium               = 1;
const uint8_t     ZPageTypeLarge                = 2;
  • ZPageTypeSmall 尺寸为 2M , SmallZPage 中的对象尺寸按照 8 字节对齐,最大允许的对象尺寸为 256K。

  • ZPageTypeMedium 尺寸和 MaxHeapSize 有关,一般会设置为 32 M,MediumZPage 中的对象尺寸按照 4K 对齐,最大允许的对象尺寸为 4M。

  • ZPageTypeLarge 尺寸不定,但需要按照 2M 对齐。如果一个对象的尺寸超过 4M 就需要在 LargeZPage 中分配。

uintptr_t ZObjectAllocator::alloc_object(size_t size, ZAllocationFlags flags) {
  if (size <= ZObjectSizeLimitSmall) {
    // 对象 size 小于等于 256K ,在 SmallZPage 中分配
    return alloc_small_object(size, flags);
  } else if (size <= ZObjectSizeLimitMedium) {
    // 对象 size 大于 256K 但小于等于 4M ,在 MediumZPage 中分配
    return alloc_medium_object(size, flags);
  } else {
    // 对象 size 超过 4M ,在 LargeZPage 中分配
    return alloc_large_object(size, flags);
  }
}

那么 ZPage 中的这个 _livemap 中的 bit 位个数,是不是就应该和一个 ZPage 所能容纳的最大对象个数保持一致,因为一个对象是否存活按理说是不是用一个 bit 就可以表示了 ?

  • ZPageTypeSmall 中最大能容纳的对象个数为 2M / 8B = 262144,那么对应的 _livemap 中是不是只要 262144 个 bit 就可以了。

  • ZPageTypeMedium 中最大能容纳的对象个数为 32M / 4K = 8192,那么对应的 _livemap 中是不是只要 8192 个 bit 就可以了。

  • ZPageTypeLarge 只会容纳一个大对象。在 ZGC 中超过 4M 的就是大对象。

inline uint32_t ZPage::object_max_count() const {
  switch (type()) {
  case ZPageTypeLarge:
    // A large page can only contain a single
    // object aligned to the start of the page.
    return 1;

  default:
    return (uint32_t)(size() >> object_alignment_shift());
  }
}

但实际上 ZGC 中的 _livemap 所包含的 bit 个数是在此基础上再乘以 2,也就是说一个对象需要用两个 bit 位来标记。

static size_t bitmap_size(uint32_t size, size_t nsegments) {
  return MAX2<size_t>(size, nsegments) * 2;
}

那 ZGC 为什么要用两个 bit 来标记对象的存活状态呢 ?答案就是为了区分本小节中介绍的这种特殊情况,一个对象是否存活分为两种情况:

  1. 对象被 FinalReference 复活,这样 ZGC 会标记第一个低位 bit —— 1

  2. 对象存在强引用链,人家原本就应该存活,这样 ZGC 会将两个 bit 位全部标记 —— 11

而在本小节中我们讨论的就是对象在被 FinalReference 复活的情况下,PhantomReference 和 WeakReference 的处理有何不同,了解了这些背景知识之后,那么我们再回头来看 should_drop 方法的判断逻辑:

首先对于 PhantomReference 来说,在 ZGC 的 Concurrent Process Non-Strong References 阶段是通过 ZBarrier::is_alive_barrier_on_phantom_oop 来判断其引用的 referent 对象是否存活的。

inline bool ZHeap::is_object_live(uintptr_t addr) const {
  ZPage* page = _page_table.get(addr);
  // PhantomReference 判断的是第一个低位 bit 是否被标记
  // 而 FinalReference 复活 referent 对象标记的也是第一个 bit 位
  return page->is_object_live(addr);
}

inline bool ZPage::is_object_marked(uintptr_t addr) const {
  //  获取第一个 bit 位 index
  const size_t index = ((ZAddress::offset(addr) - start()) >> object_alignment_shift()) * 2;
  // 查看是否被 FinalReference 标记过
  return _livemap.get(index);
}

我们看到 PhantomReference 判断的是第一个 bit 位是否被标记过,而在 FinalReference 复活 referent 对象的时候标记的就是第一个 bit 位。所以 should_drop 方法返回 true,PhantomReference 从 _discovered_list 中移除。

而对于 WeakReference 来说,却是通过 Barrier::is_alive_barrier_on_weak_oop 来判断其引用的 referent 对象是否存活的。

inline bool ZHeap::is_object_strongly_live(uintptr_t addr) const {
  ZPage* page = _page_table.get(addr);
  // WeakReference 判断的是第二个高位 bit 是否被标记
  return page->is_object_strongly_live(addr);
}

inline bool ZPage::is_object_strongly_marked(uintptr_t addr) const {

  const size_t index = ((ZAddress::offset(addr) - start()) >> object_alignment_shift()) * 2;
  //  获取第二个 bit 位 index
  return _livemap.get(index + 1);
}

我们看到 WeakReference 判断的是第二个高位 bit 是否被标记过,所以这种情况下,无论 referent 对象是否被 FinalReference 复活,should_drop 方法都会返回 false 。WeakReference 仍然会保留在 _discovered_list 中,随后和 FinalReference 一起被 ReferenceHandler 线程处理。

所以总结一下他们的核心区别就是:

  1. PhantomReference 对象只有在对象被回收的时候,才会被 ReferenceHandler 线程处理,它会被 FinalReference 影响。

  2. WeakReference 对象只要是发生 GC , 并且它引用的 referent 对象没有任何强引用链或者软引用链的时候,都会被 ReferenceHandler 线程处理,不会被 FinalReference 影响。

标签:对象,FinalReference,有何,referent,WeakReference,bit,PhantomReference
From: https://www.cnblogs.com/binlovetech/p/18257514

相关文章

  • 国际物流中双清是什么意思?双清到门是什么?有何区别?
    1.双清是什么意思?清是指清关,所谓“双清”也就是起运港报关+目的港报关的一系列过程。也就是说,你把货给货代公司做,由货代公司负责安排拖车,报关,配船。船到目的港以后的换单,清关都是货代来做。注意,这个过程不一定包括目的港的送货。而丰年物流团队做了13年印尼专线服务,提供一站......
  • 等保一体机相较于零散得安全等保产品,究竟 有何优点?
    尽管等保建设的标准是统一的,但由于不同行业和用户规模的差异,建设方案呈现出多样化的特点。虽然重点行业过等保现象确实已经十分普遍,如政府机关、金融、医疗、教育等行业。但对中小型企业来说,他们很容易就成为了“等保受众群体中沉默的大多数”。比如大型的金融企业过等保,可......
  • 浅谈 Java 引用:弱引用 WeakReference
    前言最近在测试micrometer的Gauge度量时,发现被观察的目标对象,在一开始时能被采集到指标,过了一段时间后(jvm发生了gc),被观察对象的指标采集不到了,经过跟踪发现,Gauge在构建被观察对象时,使用了Java的弱引用。测试场景回溯目标为了采集httpclient的连接管理器(连接池)的指标......
  • 安卓APK安装包arm64-v8a、armeabi-v7a、x86、x86_64有何区别?如何选择?
    https://www.cnblogs.com/yang-yz/p/17276615.html 在GitHub网站下载Android安装包,Actions资源下的APK文件通常有以下版本供选择:例如上图是某Android客户端的安装包文件,有以下几个版本可以选择:mobile-release.apk(通用版本,体积最大)mobile-universal-release.apkmobile-ar......
  • 电子阅览室有何作用
     随着互联网的快速发展,电子阅览室逐渐成为人们获取知识的新方式。它为读者提供了便捷、高效的阅读体验,具有诸多作用。首先,电子阅览室拥有丰富的电子书籍资源,涵盖了各个领域的知识。无论是文学作品还是学术论文,读者都能够轻松找到自己感兴趣的内容。同时,电子阅览室还提供了高质......
  • 鸿蒙系统和MIUI之类的系统有何区别?
    文章目录2套鸿蒙系统MIUI或HyperOSHarmonyOS2套鸿蒙系统HarmonyOS(鸿蒙操作系统)是华为开发的一种面向全场景的分布式操作系统,设计初衷是为了实现不同设备间的无缝协同,提供一致的用户体验。它支持智能手机、智能穿戴、智慧屏、车机、智能家居等多种终端设备。Harmon......
  • Vue3中的不同生命周期钩子是如何演变的?他们与传统的Vue2生命周期钩子有何不同?
    随着JavaScript生态系统不断演变,前端框架也在不断进化,以便提供更强大、更简洁的开发体验。Vue.js作为一个流行的前端框架,也在其最新版本Vue3中进行了重大更新,其中一个显著的变化就是生命周期钩子的演变。本文将详细探讨Vue3中的不同生命周期钩子是如何演变的,并对比它们与......
  • 什么是Spring的Java配置?它相比XML配置有何优势?
    Spring的Java配置指的是使用Java注解和Java代码来配置Spring应用的方式,它是Spring框架提供的一种配置Spring容器和其Bean的替代方法,与传统的基于XML的配置相比,Java配置提供了一种类型安全和更具可读性的配置方式。Java配置的定义:Java配置通常涉及以下几个方面:使用@Config......
  • BDD和ATDD、实例化需求有何区别?
    除了BDD之外,想必你也曾听说过ATDD和实例化需求这两个术语,那它们之间有何区别呢?要想得到问题的答案,不得不先了解下另外一个术语:TDD。TDD TDD,测试驱动开发,是由KentBeck在1999年提出的,强调测试先行,避免过度设计。TDD测试驱动开发的核心为红-绿-重构三步骤:1、编写一......
  • 在Linux中,硬链接和软链接有何区别?
    在Linux系统中,硬链接(HardLink)和软链接(也称为符号链接,SymbolicLink)是文件系统中的两种特殊类型的链接,它们允许多个名称指向同一个文件或目录。尽管它们有相似之处,但它们之间存在一些关键的区别:1.硬链接(HardLink)文件名相同:硬链接实际上是文件系统中的一个文件名,指向一个inode......