简化ZGC中的弱引用处理
摘要
这篇乌普萨拉大学的硕士论文与Oracle合作完成,研究了通过提出三种流水线修改和一种替代的注释字段机制,来减少ZGC垃圾收集器中弱引用处理的开销。
<p><a href="https://lobste.rs/s/bbwiob/simplifying_weak_reference_processing">评论</a></p>
查看缓存全文
缓存时间: 2026/06/15 00:48
# 简化 ZGC 中的弱引用处理 - Inside.java 原文链接:https://inside.java/2026/06/11/thesis-simplify-weak-reference-processing-zgc/ 我叫 Fredrik,最近在乌普萨拉大学完成了计算机与信息工程硕士学位。在硕士论文中,我与 Oracle 斯德哥尔摩办公室的 GC 团队合作,研究分代 ZGC 中弱引用处理的开销,以及是否可以通过对流水线的针对性修改来降低这一开销,或通过不同的弱语义表示来完全避免。 ## 问题 Java 的 `WeakReference` 提供了一种持有对象引用但不阻止其被回收的方式。当 GC 判定一个对象仅为弱可达时,它会清空 `referent` 字段,并且如果该引用已注册了 `ReferenceQueue`,则会将引用入队,以便应用程序对回收事件作出响应。此通知机制是可选的:`WeakReference` 构造函数接受 `null` 队列参数,许多使用场景(例如缓存、驻留映射、监听器注册)从不注册队列。尽管存在这种区别,ZGC 的引用处理流水线仍统一对待所有弱引用。每个被发现的引用都会通过隐藏的 `discovered` 字段链接到一个线程本地的侵入式链表中,然后通过待处理列表传递给 `ReferenceHandler` 线程,并由该线程遍历,无论是否实际需要入队。回调机制的可选性与为它执行的无条件工作之间的这种不匹配,已在 OpenJDK 问题跟踪系统(JDK-8029205)中提出,但尚未在 JDK 中得到解决。每个引用的处理成本与弱引用数量呈线性关系,这使得它成为分配大量弱引用的工作负载中的瓶颈。本论文研究是否可以通过三种正交的流水线修改来降低该成本,以及是否可以通过用带注解的字段机制替换 `WeakReference` 对象来更根本地避免该成本。 ## 四种机制 ### 1. 跳过入队分离 (`sep`) 最简单的修改是在标记阶段将无队列的弱引用路由到每个工作线程独立的已发现列表。此列表上的引用由 GC 线程直接处理和清除,无需加入待处理列表或交给 `ReferenceHandler` 线程。`ZReferenceProcessor` 中的关键变更是在发现时进行队列检查: `` if (type == REF_WEAK && !has_reference_queue(ref)) { weak_no_queue_list_per_worker.append(ref); // 完全绕过待处理列表 } else { discovered_list_per_worker.append(ref); // 正常入队流水线 } `` 两个列表由专门的函数处理,确保无入队路径中不包含每个引用的队列检查。 ### 2. 动态数组 (`dyn`) 引用处理期间遍历的侵入式链表表现出较差的缓存局部性:每个元素都是散布在堆中的 `WeakReference` 对象,而跟随 `discovered` 链需要为每个引用加载新的缓存行。`dyn` 机制将此列表替换为在 C 堆上分配的连续 `ZWeakRefArray`。在发现期间以 O(1) 分摊时间追加引用;在处理期间,通过基于索引的访问顺序迭代数组,使引用保持在 L1/L2 缓存中。数组在周期之间保留容量,以避免在引用数量稳定时重新分配。另一个好处是,可以在发现期间预加载字段数据并内联存储在数组的每个条目中,从而实现下面描述的清除路径优化。 ### 3. 优化清除路径 (`clear_path`) 标准的 ZGC 清除 referent 路径执行三个操作:用于读取 referent 的加载屏障、用于确定引用类型的虚方法调用,以及通过 ZGC 屏障执行的 CAS 操作,以原子方式将字段设置为彩色空值。对于无队列弱引用,这三者都可以简化: 1. **消除加载屏障。** 当与 `dyn` 结合时,引用地址和值在发现时被预加载并存储在数组条目中。处理时无需额外的堆加载。 2. **消除虚方法调用。** 引用类型在调用点已是静态已知。 3. **将 CAS 替换为普通存储。** 应用程序对 referent 字段的唯一并发操作是清除或入队(两者都将其设置为 null),因此原子 CAS 可以替换为直接存储,而不会带来正确性风险。 这三种简化相互促进:单独应用时,它们分别将非强处理时间减少 7%(`clear_path_only`)和 36%(`dyn_only`),但在 `clear_path_dyn` 中共同实现了 81% 的减少——远大于各自贡献之和。这种超加性源于动态数组消除了指针追逐瓶颈(否则在消除 CAS 后该瓶颈仍会存在),而预加载的数据使清除逻辑无需任何屏障开销即可运行。 ### 4. 弱字段 (`weak_fields`) 这三种流水线优化加速了 `WeakReference` 的处理,但 `WeakReference` 对象本身仍存在于堆中,并且必须在每个 GC 周期中进行标记、提升和重定位。`weak_fields` 机制采用不同的方法:不是将弱指针包装在单独的对象中,而是通过字段注解直接表达弱语义。 `` // 替代: public class Cache { private final WeakReference entry; } // 使用 @weak 注解: public class Cache { private @weak Value entry; } `` `@weak` 注解由类文件解析器识别并存储在 `fieldInfo` 元数据中。在 GC 时,ZGC 的标记闭包检查每个引用字段对应的 `fieldInfo` 条目,如果该字段被注解为 `@weak`,则将其导向每个工作线程的 `ZWeakFieldArray`,而不是将其视为强引用。标记完成后,如果弱字段的 referent 不可达,则通过 CAS 将其置空。 ## 基准测试设计 两个自定义微基准测试针对 ZGC 的引用处理流水线: - **单对象基准测试。** 2 千万个持有者对象,每个包含一个指向单个共享目标对象的 `WeakReference` 或 `@weak` 字段。在经过随机化设置(Fisher-Yates 洗牌)后,释放指向目标对象的强引用,并调用 `System.gc()`,将整个弱引用负载集中到一个 GC 周期中,最大化非强处理时间。 - **多对象基准测试。** 2 百万个带有可变大小字节数组负载的对象与持有者对象配对。强引用分五轮释放(每轮 20%),每轮后调用 `System.gc()`,将引用处理分散到多个周期中。 两个基准测试都使用 `-XX:+UseZGC`、100 GB 堆和 `-XX:InitialTenuringThreshold=1`,以便对象快速提升到老年代,ZGC 在那里发现和处理引用。每个变体在 UPPMAX Pelle 超级计算机集群的专用 AMD EPYC 9454P 节点(48 核,768 GiB RAM)上运行 250 次测量迭代,四个并行实例通过 `taskset` 固定到隔离的 CPU 集合上,以避免实例间干扰。 ## 结果 ### 非强引用处理时间 优化最直接针对的指标是 `Concurrent Process Non-Strong`,即 ZGC 并发引用处理阶段的挂钟持续时间。 | 变体 | 单对象中位数 | 相比基线 | 多对象中位数 | 相比基线 | |------------|--------------|----------|--------------|----------| | `none` (基线) | 996.9 ms | — | 44.1 ms | — | | `sep_only` | 943.8 ms | −5 % | 44.1 ms | 0 % | | `dyn_only` | 639.1 ms | −36 % | 27.5 ms | −38 % | | `clear_path_only` | 923.7 ms | −7 % | 41.5 ms | −6 % | | `clear_path_dyn` | 187.9 ms | **−81 %** | 18.8 ms | **−57 %** | | `all` | 184.4 ms | **−81 %** | 18.7 ms | **−57 %** | | `weak_fields` | 484.6 ms | −51 % | 35.8 ms | −19 % | `clear_path_dyn` 和 `all` 变体明显领先。`sep_only` 变体显示入队阶段本身并非主要瓶颈:将无队列引用路由离开 `ReferenceHandler` 线程在这些无队列基准测试中对处理时间影响甚微。然而,它应该能减少 `ReferenceHandler` 线程中的待处理列表遍历,并可能改善混合了无队列和已注册队列引用的工作负载中处理步骤的分支预测。 ### 总 GC 收集时间 目标阶段的 81% 减少转化为总 Major 收集时间的适度改善,因为在单对象基准测试中,非强处理仅占基线 Major 收集时间的 14.7%,在多对象基准测试中占 4.5%。此外,这些改进应谨慎看待,因为总收集时间的分布在各个变体中有大量重叠,只有 `weak_fields` 变体与基线明显分离。 | 变体 | 单对象中位数 | 相比基线 | 多对象中位数 | 相比基线 | |--------------|--------------|----------|--------------|----------| | `none` | 6 958 ms | — | 982 ms | — | | `all` | 6 377 ms | −8 % | 967 ms | −2 % | | `weak_fields` | 4 136 ms | **−41 %** | 708 ms | **−28 %** | `weak_fields` 在单对象基准测试中将 Major 收集时间减少了 41%,老年代时间减少了 37%(多对象基准测试中分别为 28% 和 31%)。这一改进涵盖了每个阶段——并发标记、重定位、年轻代——因为从堆中消除数百万个 `WeakReference` 对象全面减少了 GC 工作负载,而不仅仅是引用处理阶段。 ### 内存使用 动态数组在单对象基准测试中带来了显著的辅助 GC 内存开销,其中同时存在 2 千万个条目: | 变体 | 辅助 GC 内存(单对象,中位数最大值) | |------------------|--------------------------------------| | `none` | 120 MB | | `dyn_only` | 308 MB (+157%) | | `clear_path_dyn` | 1 268 MB (+957%) | | `all` | 884 MB (+637%) | | `weak_fields` | 446 MB (+272%) | 在多对象基准测试中,绝对数值较小(基线 292 MB),相对开销显著收缩,因为引用数量较少。`all` 相比 `clear_path_dyn` 节省了约 30% 的辅助 GC 内存(因为跳过入队分离将无队列引用路由到单独列表,因此不需要在数组条目中存储引用地址),使其成为总体更具吸引力的流水线变体。在所有 `WeakReference` 变体中,Java 堆占用基本相同(单对象基准测试中约为 1 720 MB)。`weak_fields` 将其减少到 806 MB(−53%),直接反映了 `WeakReference` 对象的消失。 ## 关键发现 结果表明,弱引用开销更像是一个*表示*问题,而不是*流水线*问题。要有效减少它,似乎需要重新考虑弱语义在语言中的编码方式,而不仅仅是处理已分配的对象。`clear_path_dyn` 和 `all` 组合表明,精心设计的缓存友好数据结构与简化的每引用逻辑之间的相互作用,可以在目标阶段实现大幅减少。然而,即使在专门设计来最大化其效果的条件之下,81% 的减少也仅带来 8% 的收集时间减少。相比之下,`@weak` 字段注解在每一周期的每一个阶段消除了造成该开销的对象,在单对象基准测试中实现了 41% 的 Major 收集时间节省和 53% 的堆节省。 这一发现与弱语义在更广泛语言生态系统中的实现方式一致。Go 的 `weak.Pointer`、C++ 的 `std::weak_ptr` 和 .NET 的 `WeakReference` 都将弱可达性与清理通知视为独立问题。`@weak` 注解使 Java 的无回调弱引用成本更接近该模型。 目前 `weak_fields` 实现涉及 27 个文件(而最复杂的流水线变体最多涉及 10 个),并且仍是一个原型。论文中描述了几种前进方向,包括将一个或多个流水线变体集成到 OpenJDK 项目中。 --- 论文 (http://urn.kb.se/resolve?urn=urn:nbn:se:uu:diva-588851) 可在乌普萨拉大学的 DiVA 门户获取。完整代码库 (https://github.com/efreham1/SimplifyingWeakRefs) 是 OpenJDK 的一个分支,所有四种机制在 `patches/` 下实现为源文件叠加层,并包含构建和基准测试脚本。完整测量数据集 (https://doi.org/10.5281/zenodo.20445473) 已在 Zenodo 上发布。 我衷心感谢 Stefan Johansson 和 Tobias Wrigstad 在整个项目中的指导,感谢 Oracle 斯德哥尔摩办公室的每一位同事,感谢他们的支持、慷慨以及分享专业知识。
相似文章
弱链优化:多智能体推理与协作框架
本论文提出WORC框架,这是一个针对多智能体LLM系统的弱链优化框架,通过基于元学习的权重预测和不确定性驱动的资源分配来识别并强化表现不佳的智能体,在推理基准上达到82.2%的准确率,同时提升了系统稳定性。
Unix GC 重制版
详解 Linux 内核 AF_UNIX 垃圾收集器的重写,包括背景、新的基于图的模型以及一个释放后使用漏洞。
与您协同进步:将用户修正编译为编码代理的运行时强制
TRACE 是一个技能层管道,通过从交互式编码代理中挖掘用户修正,编译为运行时检查,在减少重复偏好违反方面显著优于仅靠记忆,这一点在 ClawArena 和 MemoryArena 任务中得到验证。
安全 Rust 的边界
TokioConf 2026 的一篇演讲/博客文章探讨了如何通过为复杂指针结构实现追踪式垃圾回收,将安全 Rust 推向极限,并分享处理循环引用与原始指针 GC 设计的技巧。
内存增强型LLM智能体中的状态污染
本文识别并研究了LLM智能体中的“记忆洗白”现象,即有毒或对抗性上下文被压缩成记忆摘要后,能够逃避标准毒性检测器,同时仍影响后续生成。文章引入了亚阈值传播间隙(SPG)来衡量隐藏的下游影响,并表明在摘要之前对有毒状态进行消毒比事后清理更有效。