JVM 垃圾回收

最后更新: 2025-01-27 11:37:11 +0800 +0800
原文以英文撰写。您正在阅读AI翻译版本。

JVM 规范要求自动存储管理,但不规定回收器或堆布局。因此,回收器的行为取决于 所部署的 JVM、版本、标志、堆大小和工作负载。

可达性与内存池 #

跟踪从 GC 根开始,包括活动线程的栈和寄存器、静态字段、JNI 句柄以及 VM 运行时 结构。能够从根强可达的对象会保持存活。软引用可能在内存压力下被清除,弱引用不会 保留其引用对象,虚引用则通过 ReferenceQueue 支持事后协调。这些机制都不能替代 确定性的资源清理;应使用 try-with-resources,并且仅将 Cleaner 作为安全网。终结机制 已被弃用,并计划移除。

Java 堆存储对象和数组。在 HotSpot 中,元空间、代码缓存、线程栈、直接缓冲区、 GC 记账信息以及本地库会消耗堆外内存。-Xmx 只限制 Java 堆,因此容器限制必须为 本地内存预留余量。内存池名称和记账方式取决于回收器及具体实现。

分代与分配 #

分代回收器利用了许多分配对象会很快失效这一事实。新对象会分配到年轻代区域; 存活对象可能在幸存区之间移动,并晋升到老年代区域。为了避免每次年轻代回收都扫描 所有老年代对象,写屏障会将老年代到年轻代的指针记录在卡表或记忆集内。分配速率、 存活集大小、晋升速率以及可用的疏散空间,比单独观察堆占用率更能预测内存压力。

HotSpot 回收器的权衡 #

HotSpot 提供了目标不同的回收器:

  • Serial 针对小堆,将实现开销降至最低。
  • Parallel 通过并行的 stop-the-world 工作优先保证吞吐量。
  • G1 将堆划分为多个区域,在平衡并发工作与疏散工作的同时,朝暂停时间目标运行。
  • ZGC 将大部分标记和重定位工作并发执行,以实现较短暂停,但代价是屏障以及 并发资源使用。
  • 如果所选发行版提供 Shenandoah,它也会执行并发疏散,以缩短暂停时长。

回收器的可用性和默认设置会随 JDK 供应商及版本变化。不要将历史默认值硬编码到 运维指导中,而应使用 java -XX:+PrintCommandLineFlags -version 以及对应版本的 发布文档检查已部署的运行时。

所有跟踪式回收器都需要协调点。根处理、重新标记以及某些疏散工作可能会停止应用线程 (STW)。并发阶段能够减少暂停时间,但会消耗 CPU 和内存带宽,需要屏障,并且需要 为重定位保留空闲空间。因此,较低的暂停目标可能降低吞吐量,或在分配压力下失败。

基于证据的调优 #

启用统一日志,并将其与应用延迟和资源指标关联:

sh
java -Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags ...
java -XX:StartFlightRecording=filename=recording.jfr ...

使用 JFR 分配事件、jcmd、怀疑存在对象保留时生成的堆转储,以及 CPU/容器指标, 区分过度分配、不断增长的存活集、碎片化和本地内存耗尽。按以下顺序调优:

  1. 消除非预期的对象保留和分配热点;
  2. 根据测得的存活集和分配突发量设置堆大小;
  3. 针对测得的延迟和吞吐量目标选择回收器;
  4. 只修改少量有文档依据的目标;然后
  5. 重复具有代表性的负载测量。

System.gc() 是请求,不是正确性机制,而且可能引入干扰性工作。应用行为不得 依赖回收时机。

参考资料 #

评论

在 GitHub 上阅读或评论(发表评论需要登录)