JVM 内存结构与垃圾回收

共 57 题
📑 题目列表 57 题
#
★★★

1. CMS GC 在 JDK 14 被完全移除后遗留项目的迁移路径与 G1/ZGC 的替代方案

CMS GC 在 JDK 14 中被完全移除,遗留项目该如何规划迁移路径,G1 与 ZGC 作为替代方案应如何选择?

  • CMS 移除的时间线与历史背景
  • G1 与 ZGC 的停顿模型与适用场景差异
  • 迁移路径的验证方法与参数迁移

CMS 自 JDK 9 起被标记为废弃,并在 JDK 14 被正式移除(JEP 363)。遗留项目迁移的第一步是明确目标收集器:若业务对吞吐敏感、堆规模适中且能容忍一定停顿,首选 G1(其目标就是替代 CMS 成为默认收集器);若业务对低延迟有硬性要求(如亚秒级停顿),则考虑 ZGC(JDK 15 GA,JDK 21 引入分代模式)。迁移路径上应先做参数对齐:把 -XX:UseConcMarkSweepGC、-XX:CMSParallelRemarkEnabled、-XX:CMSInitiatingOccupancyFraction 等 CMS 专属参数移除,改用 G1 的 -XX:MaxGCPauseMillis、-XX:InitiatingHeapOccupancyPercent 或 ZGC 的 -XX:SoftMaxHeapSize 等。随后用灰度流量做压测,对比 GC 日志、暂停时间、吞吐与内存占用,确认无回归后再全量切换。

迁移的核心是"停顿模型"的匹配:CMS 用并发标记清除降低停顿,但存在碎片与浮动垃圾;G1 用 Region 化 + 并发标记 + 可预测停顿,ZGC 用染色指针 + 并发整理实现亚毫秒停顿。选择替代方案不能只看"低延迟",还要评估堆规模、CPU 开销(ZGC 的读屏障有额外开销)和吞吐目标。

// JDK 8 遗留参数(CMS)
// -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70
// 迁移到 G1(JDK 11+ 默认)
// -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45
// 迁移到 ZGC(JDK 21+)
// -XX:+UseZGC -Xms16g -Xmx16g -XX:SoftMaxHeapSize=14g
#
★★★

2. CMS GC 的工作流程与问题(碎片、浮动垃圾)

CMS GC 的工作流程是怎样的,它有哪些固有缺陷(如内存碎片、浮动垃圾)?

  • CMS 的并发标记清除流程
  • CMS 的碎片与浮动垃圾问题
  • CMS 的失败退回机制

CMS 全称 Concurrent Mark Sweep,工作流程分为初始标记(STW,标记 GC Roots 直接可达对象)、并发标记(与应用并发,沿引用链标记)、重新标记(STW,修正并发标记期间应用修改的对象引用,用增量更新)和并发清除(与应用并发,清除垃圾对象)。CMS 的固有问题是:其一,不整理内存,仅做标记-清除,长期运行会产生大量碎片,导致大对象无法分配而触发 Full GC(此时退回 Serial 收集器,串行停顿极长);其二,并发阶段应用线程继续运行会不断产生新垃圾,即"浮动垃圾",只能在下次 GC 回收,因此 CMS 必须预留足够空间(通常用 CMSInitiatingOccupancyFraction 提前触发)。CMS 是 JDK 8 时最常用的低延迟收集器,但并无完整的内存整理能力。

理解 CMS 的关键是"以并发的代价换取低停顿",但代价是碎片、浮动垃圾和并发失败(Concurrent Mode Failure)。CMS 预留空间不足时可能出现 Promotion Failure,甚至触发 Full GC 的 Serial 兜底,停顿反而比 G1 更差。这也是 CMS 最终被 G1 取代的原因之一。

#
★★★

3. Epsilon GC(No-Op GC)的测试价值

Epsilon GC(No-Op GC)的测试价值是什么,它适用于哪些场景?

  • Epsilon GC 的机制(不回收、只分配)
  • 测试中的内存分配与泄漏评估价值
  • 适用场景与限制

Epsilon GC(JEP 318)是一个只分配、不回收的"无操作"GC。它不执行任何垃圾回收,堆满后直接抛出 OutOfMemoryError。它的核心测试价值在于:其一,基准测试中隔离 GC 开销,让测试结果反映纯应用代码分配成本,从而对比"有 GC"与"无 GC"的差异;其二,验证应用是否真的需要 GC——若某应用在 Epsilon 下也能正常运行到退出,说明其分配模式对 GC 无强依赖;其三,压测堆内存上限与分配压力,检测内存泄漏。限制是它不能用于生产(会 OOM),且 JDK 11 起作为实验性功能可用。

Epsilon 的哲学是"回收本身就是一种开销",通过牺牲 GC 能力换取零 GC 开销,从而在测试中精确测量分配路径。它非常适合评估"应用是否能承受 GC 开销"以及"分配速率是否异常"。

// 启用 Epsilon GC(JDK 11+,仅用于测试)
// java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xmx2g -jar app.jar
// 运行后观察是否过早 OOM,以评估分配压力与泄漏
#
★★★

4. Epsilon GC(No-Op GC,JEP 318)在性能测试与短生命周期应用的适用边界

Epsilon GC(JEP 318)在性能测试与短生命周期应用中的适用边界是什么?

  • Epsilon 在性能测试中的用法
  • 短生命周期应用(如命令行工具、批处理)的适用性
  • 不适用场景与 OOM 风险

Epsilon GC 的适用边界可以概括为"测试辅助 + 极短生命周期 + 明确堆上限"。在性能测试中,它用于测量纯分配开销、隔离 GC 扰动,或验证 GC 是否是性能瓶颈;在启动即执行、短暂运行即退出的短生命周期工具(如一次性数据处理、CLI)中,若确认分配量远小于堆上限,可以启用 Epsilon 避免 GC 启动开销,从而缩短总运行时间。但它的边界是:一旦分配逼近堆上限就会 OOM,且无法回收任何对象,因此不适用于长期运行的服务、有大量对象生命周期波动或内存泄漏风险的场景。使用前必须用压测确认峰值分配不会超过 -Xmx。

判断 Epsilon 是否适用,关键是"分配的峰值是否在堆内且无需回收"。短生命周期应用若分配量小且确定,Epsilon 能省去 GC 线程开销;但对任何不确定分配或长期进程,启用 Epsilon 都是灾难性的。

#
★★★

5. 堆外内存(Direct Memory)不在 GC 管辖范围内,为何仍会出现 Direct Buffer OOM 及如何配合 MaxDirectMemorySize 治理

堆外内存(Direct Memory)不在 GC 管辖范围内,为何仍会出现 Direct Buffer OOM,如何配合 MaxDirectMemorySize 治理?

  • Direct Memory 与 GC 的关系
  • Direct Buffer OOM 的成因
  • MaxDirectMemorySize 与清理机制

堆外内存由 -XX:MaxDirectMemorySize 限制(默认与堆上限相同),虽然不在 GC 管辖范围内,但 DirectBuffer 的分配会通过静态计数(Bits.totalCapacity)统计,当累计超过 MaxDirectMemorySize 时抛出 Direct buffer memory 的 OutOfMemoryError。出现 OOM 的常见原因是:DirectBuffer 对象本身是堆上的小对象,其底层 Native 内存的释放依赖 Java 侧引用被回收;若持有 DirectBuffer 的引用未释放(如 Netty 未 release、ByteBuffer 未置空),Cleaner 无法触发,底层 Native 内存持续占用。治理手段包括:设置合理的 -XX:MaxDirectMemorySize 并监控,使用 NMT(Native Memory Tracking)定位堆外占用,及时显式 release/clean 缓冲,以及用 Netty 的 PooledByteBufAllocator 做池化复用。

DirectBuffer 的 OOM 本质是"堆外配额超限 + 清理不及时"的双重问题。GC 只负责回收堆上 DirectBuffer 的引用对象,而真正释放底层内存依赖 Cleaner 回调,因此治理核心是控制分配总量、及时释放并监控堆外占用。

// 设置堆外内存上限
// -XX:MaxDirectMemorySize=512m
// 启用 NMT 定位堆外内存
// -XX:NativeMemoryTracking=summary
// jcmd <pid> VM.native_memory detail
// 显式释放 DirectBuffer(Netty 场景)
// ByteBuf.release();  // 或 ByteBuffer.cleaner().clean()
#
★★★

6. Full GC 的危害与排查路径

Full GC 的危害是什么,线上 Full GC 的排查路径是怎样的?

  • Full GC 的停顿与危害
  • 排查 Full GC 的步骤与工具
  • 常见根因(老年代不足、晋升过多、元空间、大对象)

Full GC 的主要危害是 STW(Stop The World)停顿时间长,可能造成应用响应延迟激增、请求超时、甚至服务雪崩。排查路径通常是:先用 -Xlog:gc*(或 JDK 8 的 PrintGCDetails)与 jstat -gc 观察 Full GC 频率与老年代/元空间占用,定位是"内存不足"还是"回收失效";再用 jcmd 或 jmap 抓取堆 dump,用 MAT 分析按 Retained Set 找大对象与泄漏根,确认是晋升过快、大对象直接进老年代、元空间类加载器泄漏,还是静态集合持有大量引用。根据根因采取措施:调整堆大小(-Xmx/-Xms)、优化晋升阈值、减少大对象、修复内存泄漏、或切换 G1/ZGC 等收集器。

Full GC 的本质是"回收三件套(新生代、老年代、元空间)皆触及"的全局停顿。排查不能只盯着堆大小,而要区分"空间确实不够"与"空间被无效引用占满而无法回收",后者才是性能杀手。

// 启用 GC 日志(JDK 11+)
// -Xlog:gc*:file=gc.log:time,uptime,level,tags
// 观察 Full GC 频率
// jstat -gcutil <pid> 1000
// 抓堆 dump
// jcmd <pid> GC.heap_dump /tmp/heap.hprof
#
★★★

7. G1 GC 的 Mixed GC 与并发标记(Concurrent Mark)

G1 GC 的 Mixed GC 与并发标记(Concurrent Mark)是如何工作的?

  • G1 的并发标记流程
  • Mixed GC 的概念与触发
  • 与 Young GC 的区别

G1 的并发标记(Concurrent Mark)用于识别老年代中可以被回收的区域(Region),其流程与 CMS 类似:初始标记(STW,标记 GC Roots 直接可达对象,顺带完成一次 Young GC)、并发标记(与应用并发,标记存活对象,用 SATB 快照保证并发正确性)、最终标记(STW,处理并发标记期间遗留的引用)、清理阶段(STW,统计存活并回收完全空的小区)。Mixed GC 是 G1 特有的回收阶段,它既回收新生代,也回收老年代中"存活率低"的 Region(即确定有大量垃圾的区域),以在目标停顿内尽量回收老年代垃圾。Mixed GC 由并发标记完成后触发,标记周期由 IHOP(Initiating Heap Occupancy Percent)启动。

与 CMS 的"并发标记后必然是并发清除"不同,G1 把老年代回收拆成"并发标记找出可回收 Region" + "Mixed GC 分批回收",从而把长停顿分散到多个可预测停顿的 Mixed GC 中。这就是 G1 能实现"可预测停顿"的关键。

#
★★★

8. G1 GC 的 Region 模型与 Mixed GC

G1 GC 的 Region 模型是什么,Mixed GC 如何在其上工作?

  • Region 化堆内存模型
  • Region 的角色(Eden/Survivor/Old/Humongous)
  • Mixed GC 的回收范围

G1 将堆划分为固定大小的 Region(默认 1MB ~ 32MB,由 -XX:G1HeapRegionSize 或堆大小自动推导),每个 Region 动态扮演新生代(Eden/Survivor)或老年代的角色,并额外提供 Humongous Region 用于存放超过 Region 一半的大对象。因为 Region 是独立单元,G1 可以增量回收:Young GC 回收新生代 Region,Mixed GC 在回收新生代的同时回收老年代中存活率低的 Region。G1 用 RSet(Remembered Set)记录跨 Region 引用,避免全堆扫描,从而支持局部回收。Mixed GC 的回收顺序按"存活率从低到高"选择 Region,在停顿目标内尽可能多地回收,剩余留待下次。

Region 模型是 G1 的基石,它把"老年代"从单一连续空间拆成多个可独立回收的小区,使"部分回收老年代"成为可能。Mixed GC 正是利用 Region 的颗粒度,在停顿预算内权衡回收收益。

#
★★★

9. G1 中 Humongous 对象如何判定与分配(超过 Region 一半),它对 Mixed GC 与内存碎片有什么特殊影响

G1 中 Humongous 对象如何判定与分配(超过 Region 一半),它对 Mixed GC 与内存碎片有什么特殊影响?

  • Humongous 对象的判定标准
  • Humongous 分配方式
  • Humongous 对 Mixed GC 与碎片的影响

G1 中,当对象大小超过 Region 大小的一半(即 > Region_size/2)时,被判为 Humongous 大对象,直接分配到连续的 Humongous Region 中(可能占用多个 Region),不再经历正常的 Eden 晋升流程。Humongous 对象对 Mixed GC 的影响是:其一,它无法正常晋升,通常直接进入老年代,频繁产生会造成老年代快速膨胀;其二,Humongous Region 的回收只在并发标记后的清理/Mixed GC 中进行,且大对象占用多个连续 Region,容易造成碎片;其三,大对象在并发标记阶段被识别为存活时占用整块 Region,回收效率低。因此频繁产生大对象会加速堆耗尽并触发 Full GC。

Humongous 的判定是"大小 > Region_size/2",其分配是"连续 Region 直通老年代",这绕过了分代晋升,破坏了 G1 的 Region 弹性。所以大对象是 G1 性能的大敌,应通过对象池、分片或减小对象体积来规避。

#
★★★

10. G1 回收 Humongous 对象依赖什么时机,为何频繁产生大对象会加速堆耗尽并触发 Full GC

G1 回收 Humongous 对象依赖什么时机,为何频繁产生大对象会加速堆耗尽并触发 Full GC?

  • Humongous 回收时机
  • 大对象对堆耗尽的加速作用
  • 与 Full GC 的关联

G1 回收 Humongous 对象依赖并发标记周期:只有经过并发标记识别 Humongous Region 无存活对象,才有机会在随后的清理阶段或 Mixed GC 中回收。由于 Humongous 对象直接进入老年代且不经历正常的晋升年龄控制,回收更迟滞。频繁产生大对象会加速堆耗尽并触发 Full GC 的原因:一是大对象直接占用老年代 Region,绕过新生代,使老年代快速逼近 IHOP 触发并发标记;二是大对象需要连续 Region,若碎片化严重,即使有足够总空间也可能因找不到连续空间而失败,触发 Full GC 兜底;三是大对象生命周期长,存活时占用大量 Region,导致可回收空间少。因此大对象高频分配是 G1 的典型性能陷阱。

Humongous 的回收完全依赖并发标记,而并发标记由 IHOP 触发,属于"被动"回收。大对象直接进老年代 + 需连续空间 + 回收依赖标记,三者叠加使堆快速膨胀并极易触发 Full GC。

#
★★★

11. G1 的 MaxGCPauseMillis 与 -XX:InitiatingHeapOccupancyPercent

G1 的 -XX:MaxGCPauseMillis 与 -XX:InitiatingHeapOccupancyPercent(IHOP)各自的作用是什么,它们如何协作?

  • MaxGCPauseMillis 的停顿目标语义
  • IHOP 的并发标记触发阈值
  • 两者协作与权衡

-XX:MaxGCPauseMillis(默认 200ms)是 G1 的停顿目标,G1 会据此动态调整新生代大小与回收范围,努力让每次 GC 的停顿不超过该值;它并非硬性保证,而是软目标,G1 通过自适应调整 Eden 大小、Region 数量和 Mixed GC 的回收批次来逼近。而 -XX:InitiatingHeapOccupancyPercent(IHOP,默认 45)是触发并发标记的堆占用百分比阈值:当老年代(实际是堆对各代占用)达到该比例时,G1 启动并发标记周期,标记完成后结合 Mixed GC 回收老年代。两者协作:IHOP 决定"何时开始标记老年代",MaxGCPauseMillis 决定"每次 Mixed GC 回收多少、停顿多久"。若停顿目标过小,G1 会减小每次回收量,导致标记周期很长、老年代回收不及时;若 IHOP 过高,老年代可能等不及标记就触发 Full GC。

MaxGCPauseMillis 管"停顿粒度",IHOP 管"标记时机",两者一起决定 G1 的回收节奏。调优时需平衡:停顿目标小则吞吐受影响,IHOP 过高则 Full GC 风险上升,需结合业务压测迭代。

// 典型 G1 配置
// -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:InitiatingHeapOccupancyPercent=45
// -XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60
#
★★★

12. GC 日志解读(GC 原因、暂停时间、内存变化)

如何解读 GC 日志,从中提取 GC 原因、暂停时间与内存变化等信息?

  • GC 日志的字段与格式
  • 暂停时间与内存变化的解读
  • 不同收集器日志的差异

GC 日志通常包含触发原因、收集器类型、各代内存占用(使用量/容量)、耗时与耗时分解。例如 G1 的日志形如 [GC pause (G1 Evacuation Pause) (young) 512M->128M(1024M), 0.0123456 secs],其中"young"表示回收目标,"512M->128M"表示堆从 512M 降到 128M,"1024M"是堆容量,"0.0123 secs"是暂停时间。通过叠加多个 GC 事件,可观察:老年代是否持续增长(疑似泄漏)、Eden 是否频繁打满(分配过快)、暂停时间是否超目标(停顿问题)、以及元空间是否增长。JDK 9+ 用 -Xlog:gc* 统一日志,标签如 gc、gc+heap、gc+pause 等可细分;JDK 8 用 -XX:+PrintGCDetails 输出带 GC 原因与各代 detail 的文本。

读 GC 日志的核心是看"起因-过程-结果":原因(cause)判断是否异常触发,过程看暂停时间与各代变化,结果看回收后是否回到稳定水位。连续增长的老年代 + 频繁 Full GC 是内存泄漏的典型信号。

// JDK 8 启用 GC 日志
// -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
// JDK 11+ 统一日志
// -Xlog:gc*:file=gc.log:time,uptime,level,tags
#
★★★

13. GC 的基本算法(标记-清除、标记-整理、复制、分代)

请说明 GC 的基本算法:标记-清除、标记-整理、复制与分代,各自原理与适用场景?

  • 四种基本算法的原理
  • 各算法的优缺点
  • 分代收集如何组合

标记-清除分两阶段:先标记所有可达对象,再清除未标记的垃圾;缺点是会产生内存碎片,且清除效率依赖对象存活率。标记-整理在标记后把存活对象向一端移动,消除碎片,但移动对象需更新引用、成本高,适合老年代。复制算法把内存分为两块,只使用一块,回收时把存活对象复制到另一块并清空当前块;优点是简单高效、无碎片,缺点是可用内存减半、存活率高时复制开销大,适合新生代("朝生夕死")。分代收集把堆分为新生代(用复制/标记-复制,因为存活率低)与老年代(用标记-整理或标记-清除,因为存活率高),用不同算法各取所长,是 HotSpot 默认的宏观策略。

四种算法的取舍本质是"时间、空间、碎片"的平衡:复制算法快但费空间,标记-整理省空间但慢,标记-清除有碎片但简单。分代收集正是利用"大多数对象朝生夕死"的弱代假说,让新生代用复制、老年代用整理,兼顾效率与空间。

#
★★★

14. GC 算法在容器/cgroup 下的内存算账

GC 算法在容器/cgroup 环境下如何做内存算账,需要注意哪些问题?

  • JVM 在容器中的内存识别
  • cgroup 限制与 JVM 默认堆
  • 容器内存治理与 OOM

JDK 8u191+ 与 JDK 10+ 的 JVM 能识别 cgroup 内存限制,自动把 -Xmx 默认上限设为容器可用内存的一个比例(如可用内存的 1/4),避免 JVM 超出容器配额。但容器内存算账需要额外考虑:堆外内存(DirectBuffer、Metaspace、线程栈、JIT CodeCache、NMT 等)不计入 -Xmx,只受 cgroup 总内存限制;因此即使 -Xmx 设得合理,若堆外占用过大,容器仍可能因超过 cgroup limit 被内核 OOM-Kill。治理上应显式设置 -Xmx/-Xms、-XX:MaxMetaspaceSize、-XX:MaxDirectMemorySize,并预留堆外余量,同时用 -XX:ReservedCodeCacheSize、-XX:ActiveProcessorCount 匹配容器 CPU 数。

容器内存的关键是" -Xmx 只是堆上限,不是容器总用量"。在 cgroup 下,堆 + 元空间 + 堆外 + 线程栈 + CodeCache 的总和必须不超过容器 limit,否则触及 cgroup 上限可能被系统杀掉。因此要显式治理各项并预留缓冲。

// 容器内显式设置内存(避免仅依赖默认)
// -Xms4g -Xmx4g -XX:MaxMetaspaceSize=512m -XX:MaxDirectMemorySize=512m
// -XX:ActiveProcessorCount=4 -XX:ReservedCodeCacheSize=256m
#
★★★

15. GC 调优的 -Xmx/-Xms 设置与堆外内存(Off-Heap)的综合考量

GC 调优中如何设置 -Xmx/-Xms,以及如何与堆外内存(Off-Heap)做综合考量?

  • -Xmx 与 -Xms 的设置原则
  • 堆外内存的占用与治理
  • 堆与堆外的平衡

-Xms 是初始堆大小,-Xmx 是最大堆大小。生产环境建议将两者设为相同值(避免运行时堆扩容/收缩带来的额外 GC 与停顿),并依据应用的峰值堆占用留出约 30% 余量。堆外内存(DirectBuffer、Metaspace、线程栈、CodeCache)虽不占 -Xmx,但占进程总内存,因此综合考量时要:估算应用堆外峰值(如 Netty 的 DirectBuffer 池、Metaspace 类加载),为堆外预留足够余量,避免堆 + 堆外 + 元空间超过容器/机器物理内存;同时显式设置 -XX:MaxMetaspaceSize、-XX:MaxDirectMemorySize 等上限,防止堆外失控。好的做法是用 NMT 或监控工具长期采集堆外占用,再结合堆大小确定容器规格。

-Xmx/-Xms 解决"堆多大",堆外治理解决"总内存不超限"。两者必须一起算账:过大的堆挤压堆外,堆外失控又拖垮整体。调优目标是"堆内稳定 + 堆外有界 + 总体有余量"。

// 堆与堆外综合设置示例(8GB 容器)
// -Xms6g -Xmx6g -XX:MaxMetaspaceSize=512m -XX:MaxDirectMemorySize=1g
// -XX:ReservedCodeCacheSize=256m -XX:NativeMemoryTracking=summary
#
★★★

16. GC 调优的通用思路(减少分配、增大堆、调触发阈值)

GC 调优的通用思路是什么(减少分配、增大堆、调整触发阈值)?

  • 减少对象分配的策略
  • 增大堆的权衡
  • 调触发阈值的思路

GC 调优遵循"先优化代码,再调参数"的通用思路。第一层是减少分配:避免在循环中创建对象、使用对象池、复用缓冲、避免装箱、用 String 拼接优化等,从源头降低 GC 压力,这是最根本的优化。第二层是增大堆:当确实因容量不足导致 GC 频繁时放大 -Xmx/-Xms,但受内存限制,且堆过大会增加 Full GC 停顿与扫描成本。第三层是调触发阈值:调整新生代/老年代比例、-XX:MaxTenuringThreshold、G1 的 IHOP、-XX:NewRatio 等,让 GC 在更合适的时机触发,减少不必要的 Full GC。调优过程应始终以 GC 日志与监控数据为依据,每次只改一个参数并验证,避免盲目调参。

调优的本质是"在分配、容量、触发时机三个维度上降低成本"。代码层面的减分配收益最大且无副作用,参数调整是补充手段。核心原则是"一切以数据为准",用 GC 日志衡量每次改动的影响。

#
★★★

17. JDK 9+ 的 -Xlog:gc* 统一日志如何替代 JDK 8 的 -XX:+PrintGCDetails 等参数,迁移时标签与输出文件如何对应

JDK 9+ 的 -Xlog:gc* 统一日志如何替代 JDK 8 的 -XX:+PrintGCDetails 等参数,迁移时标签与输出文件如何对应?

  • 统一日志框架 -Xlog 的语法
  • 与 JDK 8 旧参数的映射
  • 标签体系与输出配置

JDK 9+ 用统一的 JVM 日志框架 -Xlog 取代了 JDK 8 碎片化的日志参数。语法为 -Xlog:[选项]:[输出]:[装饰器]:[标签],例如 -Xlog:gc*:file=gc.log:time,uptime,level,tags,其中"gc*"是通配的标签选择器,file 指定输出文件,装饰器控制时间戳等。迁移时,JDK 8 的 -XX:+PrintGCDetails 对应 -Xlog:gc*,-XX:+PrintGCDateStamps 对应装饰器 time/uptime,-XX:+PrintGCApplicationStoppedTime 对应 -Xlog:safepoint,-XX:+PrintGCHeapAtGC 对应 -Xlog:gc+heap=debug。标签按 four-part 体系(作用域、组件、标签、严重级别)组织,如 gc+heap、gc+promotion、gc+ergo 等,可细粒度控制输出。输出文件通过 file=路径 指定,必要时配合 -Xlog:disabled 关闭多余输出。

统一日志把"分散的 print 参数"收敛为"标签 + 输出 + 装饰器"的单一框架,粒度更细、可配置性更强。迁移关键是理解标签与旧参数的功能对应关系,而非简单复制参数名。

// JDK 8 → JDK 9+ 迁移对照
// -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
//   → -Xlog:gc*:file=gc.log:time,uptime,level,tags
// -XX:+PrintReferenceGC → -Xlog:gc+ref=debug
// -XX:+PrintGCApplicationStoppedTime → -Xlog:safepoint
#
★★★

18. JEP 439 Generational ZGC 与 JEP 404 Generational Shenandoah 在 JDK 21/25 中如何根据业务延迟特征选型,弱代假说验证如何反映在分代模式

JEP 439 Generational ZGC 与 JEP 404 Generational Shenandoah 在 JDK 21/25 中如何根据业务延迟特征选型,弱代假说验证如何反映在分代模式?

  • Generational ZGC 与 ZGC 分代
  • Generational Shenandoah 与弱代假说
  • 延迟特征驱动选型

JEP 439 在 JDK 21 为 ZGC 引入分代模式(Generational ZGC),通过为新生代/老年代分别管理,显著降低年轻对象频繁分配带来的停顿与内存开销,同时保持亚毫秒级低暂停;JEP 404 则在 JDK 21 为 Shenandoah 引入分代模式,并在 JDK 25 的 JEP 521 转正(Generational Shenandoah 成为默认)。弱代假说(大多数对象朝生夕死)在分代模式中体现为:新生代用更快、更小的回收周期处理大量短命对象,老年代用更专注的回收处理长命对象,从而减少对老年代的扫描与停顿,提升内存利用率与吞吐。选型上:若业务对延迟抖动极敏感(毫秒级、亚毫秒级),且堆大、追求极致低暂停,首选 ZGC 分代;若追求低暂停同时希望更低的额外线程/内存开销,且能接受 Shenandoah 的读屏障成本,可选 Shenandoah 分代;若对吞吐更敏感、可容忍一定停顿,则 G1 更合适。

两种分代收集器都依赖弱代假说优化,但 ZGC 用染色指针 + 多重映射,Shenandoah 用 Brooks 指针,实现路径不同。选型要结合延迟特征(P99 还是 P999)、堆规模、CPU 余量与内存预算综合判断,而非只看"低延迟"标签。

#
★★★

19. JEP 514/515 AOT Command Line Ergonomics 与 GC 关联

JEP 514/515 AOT Command Line Ergonomics 是什么,与 GC 有何关联?

  • JEP 514/515 的 AOT 命令行优化
  • GC 相关参数的自动选择
  • 与具体 GC 配置的关联

JEP 514(JDK 25,Ahead-of-Time Command-Line Ergonomics)改进了 AOT(Ahead-of-Time)缓存的命令行使用方式,提供一步式创建 AOT 缓存的流程,让 JVM 在指定选项更少时更智能地推导合理的默认值,减少手动配置负担;JEP 515(JDK 25,Ahead-of-Time Method Profiling)把训练运行收集的方法画像存入 AOT 缓存,帮助 JIT 在启动阶段更快编译热点方法。与 GC 的关联在于:当用户未显式指定 GC 收集器或堆参数时,JVM 会根据可用的内存、CPU 数和运行模式在众多 GC 参数间自动选择更协调的默认组合(JVM Ergonomics),从而让"少配置"也能获得更好的 GC 行为,降低误配风险。

Ergonomics 的本质是"让 JVM 的帮助更聪明",把过去需要人工对齐的 GC 参数组合自动化。它不改变 GC 本身,而是改变"GC 参数如何被默认推导",从而减少因缺省参数不匹配导致的 GC 问题。

#
★★★

20. JVM 内存溢出(OOM)的分类(堆/栈/方法区)

JVM 内存溢出(OOM)可分为哪几类(堆/栈/方法区),各自场景与触发原因?

  • 堆 OOM(Java heap space)
  • 栈 OOM(StackOverflowError / OutOfMemoryError)
  • 方法区/元空间 OOM(Metaspace)

JVM 的 OOM 主要有三类。其一,堆 OOM(java.lang.OutOfMemoryError: Java heap space):对象分配超过 -Xmx,常见于内存泄漏或超大对象,需用堆 dump + MAT 分析。其二,栈相关溢出:StackOverflowError 是线程栈调用深度超过栈容量(常见于无限递归,可用 -Xss 调整栈大小),OutOfMemoryError: unable to create new native thread 是创建线程失败(线程栈占用 native 内存,受 ulimit 与内存限制)。其三,方法区/元空间 OOM(OutOfMemoryError: Metaspace):类元数据超过 -XX:MaxMetaspaceSize,常见于动态生成类或类加载器泄漏,需检查类加载器是否被持续引用。此外还有 Direct buffer memory(堆外)与 unable to create native thread 等堆外类 OOM。

区分 OOM 类型是排查的第一步:堆 OOM 看对象与泄漏,栈 OOM 看递归与线程数,元空间 OOM 看类加载器。每种类型对应不同的排查工具与修复策略。

// 递归导致 StackOverflowError
void recurse() { recurse(); }
// 元空间 OOM 常见于动态代理类/字符串 intern 失控
// -XX:MaxMetaspaceSize=256m 限制元空间
#
★★

21. JVM 安全点(Safepoint)与 GC 暂停

JVM 安全点(Safepoint)是什么,它与 GC 暂停有何关系?

  • Safepoint 的概念
  • 线程进入 Safepoint 的机制(轮询)
  • Safepoint 与 GC 暂停的关系

Safepoint 是 JVM 中所有线程都到达的统一"安全停靠点",此时 JVM 可以做全局操作(如 GC、偏向锁撤销、类重定义、JIT 去优化等)。线程在具有安全点位置的指令(如方法返回、循环回边、分配点)上通过轮询(Safepoint Poll)检查是否要进入 Safepoint。GC 暂停(STW)就是让所有线程运行到 Safepoint 后停下,再执行根扫描、标记等操作。Safepoint 相关的问题包括:Safepoint Bias(某些线程长时间停留在安全点区域造成停顿过长)、轮询开销、以及需要长时间运行的代码导致无法及时到达 Safepoint。

Safepoint 是"协作式"的:GC 不是抢占式中断线程,而是等待每个线程到达安全的轮询点。这带来 0 或很短的排队延迟,但若某线程长时间不检查轮询点,会拖长停顿,这就是 Safepoint Bias 的根源。

#
★★

22. JVM 运行时数据区(PC、栈、堆、方法区、直接内存)

JVM 运行时数据区包括哪些部分(PC、栈、堆、方法区、直接内存),各自作用?

  • 线程私有的数据区(PC、虚拟机栈、本地方法栈)
  • 线程共享的数据区(堆、方法区)
  • 直接内存

JVM 运行时数据区分为线程私有与线程共享两类。线程私有:程序计数器(PC,记录当前执行字节码行号)、虚拟机栈(JVM Stack,存放栈帧,帧内有局部变量表、操作数栈、动态链接、返回地址)、本地方法栈(Native Method Stack,服务于 native 方法)。线程共享:堆(Heap,存放对象实例,是 GC 主战场)、方法区(Method Area,存放类元数据、常量池、静态变量,JDK 8 中以 Metaspace 实现,使用本地内存)。直接内存(Direct Memory)是堆外、不受 JVM 堆管理的内存,由 NIO 的 DirectBuffer 使用,受 -XX:MaxDirectMemorySize 限制。各区域分别对应不同 OOM 场景。

运行时数据区是理解 JVM 内存模型的基础。私有区(栈、PC、本地方法栈)随线程生命周期创建销毁,共享区(堆、方法区)是 GC 与 OOM 的主要发生地;直接内存虽是堆外,但也是内存治理的重要部分。

#
★★

23. Java 堆(Young/Old)与分代假设

Java 堆如何分代(Young/Old),分代假设是什么?

  • 新生代与老年代的结构
  • 分代假设(弱代假说)
  • GC 策略与分代的关系

Java 堆分为新生代(Young)与老年代(Old)。新生代又细分为 Eden、Survivor(S0/S1),存放刚创建的对象;对象熬过多次 Minor GC 后晋升到老年代(Old),存放长生命周期对象。分代假设(弱代假说)认为"绝大多数对象朝生夕死",即大部分对象在创建后很快变成垃圾。基于此假设,新生代用复制算法(存活率低,复制成本低),老年代用标记-整理/清除(存活率高),从而在效率与空间间取得平衡。分代收集让 Minor GC 只处理新生代、速度快,Full GC 才覆盖全堆,减少停顿。

分代是"利用对象生命周期规律优化 GC"的工程实践。弱代假说决定新生代用复制、老年代用整理,也决定了晋升、年龄阈值、Survivor 分配等机制。

#
★★

24. Parallel GC 适用场景(吞吐优先)

Parallel GC 适用于什么场景,其"吞吐优先"的特点如何体现?

  • Parallel GC 的机制(多线程并行)
  • 吞吐优先 vs 延迟优先
  • 适用场景

Parallel GC(Parallel Scavenge + Parallel Old)是 JDK 8 的默认收集器,特点是多线程并行回收,目标是最大化吞吐量(应用运行时间 / 总时间),通过 -XX:ParallelGCThreads 控制回收线程数。它采用"吞吐优先"策略:允许较长的停顿(相对的),但尽量减少 GC 总耗时,适合对延迟不敏感、追求吞吐的批处理/计算密集型场景(如科学计算、批量数据分析、后台任务)。与之相对,CMS、G1、ZGC 等更注重低停顿。Parallel GC 的牺牲是单次停顿可能较长,不适合交互性强的在线服务。

吞吐优先意味着"GC 总时间占比最小化",而非"单次停顿最小化"。Parallel GC 用多线程并行 + 自适应调节(吞吐量目标、GC 时间比)来逼近高吞吐,适合 CPU 密集、可容忍停顿的批处理。

// 显式启用 Parallel GC(JDK 8 默认)
// -XX:+UseParallelGC -XX:+UseParallelOldGC
// -XX:ParallelGCThreads=8 -XX:GCTimeRatio=9
#
★★

25. TLAB 与堆分配路径的协作,大对象绕过 TLAB 直接进入老年代的条件与影响

TLAB 与堆分配路径如何协作,大对象绕过 TLAB 直接进入老年代的条件与影响是什么?

  • TLAB 的分配机制
  • 大对象绕过 TLAB 的条件
  • 大对象直接进老年代的影响

TLAB(Thread Local Allocation Buffer)是每个线程在 Eden 中私有的分配缓冲,线程优先在 TLAB 内分配对象,避免竞争,提高分配效率;当 TLAB 空间不足时,对象会尝试在 TLAB 中剩余空间分配,或直接分配在 Eden 的公共区域(PLAB/Promotion LAB 等)。大对象的分路径是:若对象大小超过 TLAB 剩余空间甚至超过 TLAB 大小,会绕过 TLAB 直接分配;在 G1 中超过 Region 一半的大对象是 Humongous,直接分配到连续 Region 进入老年代;在 Parallel/Serial 中超过一定阈值(大对象阈值)的对象也可能直接进老年代,避免频繁复制。影响是:大对象直进老年代,跳过新生代的晋升与年龄控制,容易造成老年代碎片与快速膨胀,增加 Full GC 风险。

TLAB 解决"分配竞争",大对象直进老年代解决"复制成本"。两者协作的目标都是降低分配与回收成本,但大对象直进老年代带来碎片与膨胀隐患,需用对象池或减小对象体积治理。

#
★★

26. G1 的 Young GC 与 Mixed GC 触发阈值如何由 IHOP 与停顿目标共同决定

G1 的 Young GC 与 Mixed GC 触发阈值如何由 IHOP 与停顿目标共同决定?

  • Young GC 的触发条件
  • Mixed GC 的触发条件
  • IHOP 与停顿目标的协作

G1 的 Young GC 由 Eden 空间不足触发:当新生代 Region 无法容纳新对象时,G1 执行 Young GC,回收新生代并晋升存活对象到 Survivor/老年代。Mixed GC 则由并发标记完成 + 老年代堆积程度共同决定:当老年代占用达到 IHOP(InitiatingHeapOccupancyPercent,默认 45%)时,G1 启动并发标记周期,标记完成后若存在足够多可回收的老年代 Region,就触发 Mixed GC 分批回收。停顿目标(MaxGCPauseMillis)影响 Mixed GC 每次回收的 Region 数量与停顿长度:停顿目标越小,每次回收越少,周期拉长;反之回收越多。总之,Young GC 由 Eden 打满触发,Mixed GC 由 IHOP 触发标记 + 停顿目标决定回收节奏。

Young GC 是"空间驱动"(Eden 满),Mixed GC 是"标记驱动 + 停顿预算驱动"(IHOP 触发标记,停顿目标决定分批)。两者互补,共同构成 G1 的分代回收节奏。

#
★★

27. GC Roots 的枚举范围(栈、静态、JNI、活跃线程)如何决定可达性分析的起点与耗时

GC Roots 的枚举范围(栈、静态、JNI、活跃线程)如何决定可达性分析的起点与耗时?

  • GC Roots 的组成
  • 可达性分析的起点
  • 根枚举对耗时的影响

GC Roots 是可达性分析的起点,主要包括:虚拟机栈(栈帧中局部变量引用的对象)、本地方法栈(JNI 引用的对象)、方法区中的静态字段/常量池引用、活跃线程对象、以及被 JNI 引用的对象。可达性分析从这些 Roots 出发沿引用链遍历,凡不可达的对象即为垃圾。GC Roots 的数量与分布直接影响根枚举的耗时:根越多、引用链越深,扫描成本越高;GC 与并发收集器(如 G1)会通过让应用线程在 Safepoint 更新根来保证根快照一致性,因此根枚举阶段通常需要 STW。优化根枚举耗时的手段包括精简静态引用、避免大而深的冗余引用链、以及使用对大根扫描友好的收集器。

GC Roots 是"引用存在的起点",其范围决定可达性分析覆盖多少对象。根枚举与根扫描是 STW 停顿的主要成本之一,因此根的数量与引用链复杂度直接影响 GC 停顿。

#
★★

28. Shenandoah GC 的 Brooks 指针与并发整理

Shenandoah GC 的 Brooks 指针是什么,它如何实现并发整理?

  • Brooks 指针的原理
  • 读屏障与转发
  • 并发整理机制

Shenandoah GC 在每个对象头中增加一个"Brooks 指针"(forwarding pointer),用于记录对象在整理后移动到的位置。Shenandoah 通过读屏障(Read Barrier)在每次读取对象引用时检查访问指针,若对象已被移动,则通过 Brooks 指针找到新位置并修正引用,从而让应用线程与 GC 并发地读取对象而无需 STW。并发整理时,GC 线程把对象复制到新位置并更新 Brooks 指针,应用线程通过读屏障透明地跟随转发。这样 Shenandoah 实现了与 ZGC 类似的低暂停,但采用的技术是"指针内嵌在对象中 + 读屏障",而非 ZGC 的染色指针。

Brooks 指针是 Shenandoah 并发整理的核心:对象移动时只更新头部的转发指针,读屏障负责在读取时解引用转发,从而把"移动对象"变成对应用透明的操作,避免 STW。代价是每次读引用都有读屏障开销。

#
★★

29. System.gc() 的建议禁用与 -XX:+DisableExplicitGC

为什么建议禁用 System.gc(),-XX:+DisableExplicitGC 的作用是什么?

  • System.gc() 的语义与风险
  • 触发 Full GC 的负面影响
  • -XX:+DisableExplicitGC

System.gc() 会显式请求 JVM 执行一次 Full GC(即使收集器可能转化为老年代回收),是全局 STW 停顿,可能造成性能抖动。第三方库(如 Netty 的堆外 DirectBuffer 清理、某些 RPC 框架)可能引用 System.gc() 来触发回收,但频繁调用会拖慢系统。建议在生产环境用 -XX:+DisableExplicitGC 禁用 System.gc(),从而避免意外 Full GC 引起的停顿;同时保留对 DirectBuffer 的可靠清理手段(如显式 release、Cleaner)。注意:禁用后程序员无法再通过 System.gc() 主动触发 GC,应依赖自动 GC 与合理的堆配置。

System.gc() 的最大风险是"不可控的 Full GC 停顿"。禁用它(DisableExplicitGC)能消除第三方库或代码误触发的全局回收,代价是失去手动触发能力,但现代 JVM 应依赖自动回收策略。

// 禁用显式 System.gc()
// -XX:+DisableExplicitGC
// 注意:Netty 等库默认可能调用 System.gc() 清理堆外,需配合可靠的 DirectBuffer 释放
#
★★

30. TLAB(Thread Local Allocation Buffer)

TLAB(Thread Local Allocation Buffer)是什么,它的作用与配置如何?

  • TLAB 的机制与作用
  • TLAB 的分配流程
  • TLAB 相关参数

TLAB(Thread Local Allocation Buffer)是 JVM 为每个线程在 Eden 中预分配的私有分配缓冲。线程在 TLAB 内分配对象无需全局锁,极大降低并发分配竞争、提高分配吞吐。分配流程:线程先在自己的 TLAB 中"bump-the-pointer"分配;当 TLAB 空间不足时,若剩余空间足够大则申请新 TLAB,否则把剩余空间作为"浪费"并直接在 Eden 公共区分配。相关参数包括 -XX:TLABSize(初始大小)、-XX:ResizeTLAB(是否动态调整)、-XX:UseTLAB(是否启用)。TLAB 设计也影响了对象布局与 GC 的分配路径,配合 PLAB(Promotion LAB)处理晋升对象。

TLAB 是分配路径上的"线程私有缓存",解决多线程分配时的竞争与 CAS 开销。它是提升分配性能的基础设施,也是理解"堆分配高效"的重要原因。

#
★★

31. Metaspace 内存回收与类加载器泄漏的关系,动态生成类为何会导致元空间持续增长

Metaspace 内存回收与类加载器泄漏有何关系,动态生成类为何会导致元空间持续增长?

  • Metaspace 的回收机制
  • 类加载器泄漏
  • 动态生成类与元空间增长

Metaspace(JDK 8 起替代永久代)存放类元数据,使用本地内存,受 -XX:MaxMetaspaceSize 限制。Metaspace 的回收依赖于类加载器及类是否可被卸载:只有当某个类加载器及其加载的所有类都不可达(无任何引用)时,其类元数据才可能被回收。若类加载器被持续引用(如静态集合持有、ThreadLocal 持有、容器缓存),则其加载的类无法卸载,Metaspace 持续占用。动态生成类(如 CGLIB 代理、ASM 生成、反射代理、频繁的字符串 intern 相关)会创建新的类加载器或类,若未及时释放,会导致元空间持续增长甚至 OOM。治理需检查类加载器泄漏、避免重复生成类、设置 MaxMetaspaceSize 并用 dump 分析。

Metaspace 回收的单元是"类加载器 + 其类",只有类加载器整体不可达才可回收。动态生成类若反复创建新加载器且不释放,就会造成元空间泄漏,这是很多运行期字节码框架的坑。

#
★★

32. Young GC 与 Full GC 的触发条件

Young GC 与 Full GC 的触发条件分别是什么?

  • Young GC(Minor GC)的触发
  • Full GC(Major GC)的触发
  • 触发机制的差异

Young GC(Minor GC)主要触发条件是新生代 Eden 空间不足:当新对象无法在 Eden(含 TLAB)分配时,触发 Minor GC,回收新生代并把存活对象晋升到 Survivor/老年代。Full GC(Major GC)触发条件更复杂:老年代空间不足(晋升失败、老年代分配失败)、Metaspace 不足、显式 System.gc()、以及某些收集器(G1)在并发标记失败或大对象(Humongous)分配失败时触发。Full GC 会回收整个堆(新生代+老年代+元空间),停顿长。不同收集器触发细节不同,但总体是"空间不足驱动"。

Young GC 是"新生代空间不足"的局部回收,Full GC 是"全局空间不足或显式请求"的全局回收。理解触发条件有助于区分"频繁 Minor GC"与"频繁 Full GC"的不同根因。

#
★★

33. ZGC 在 JDK 21+ 的分代模式下,转发表与内存多重映射的开销相比非分代模式如何变化

ZGC 在 JDK 21+ 的分代模式下,转发表与内存多重映射的开销相比非分代模式如何变化?

  • ZGC 分代模式
  • 转发表(forwarding table)开销变化
  • 多重映射开销变化

ZGC 从 JDK 21 起提供分代模式(Generational ZGC,JEP 439),把堆分为新生代与老年代分别处理和回收。分代模式下,由于新生代对象存活率低、回收频繁且范围小,转发表(forwarding table)主要服务于分代回收的移动与转发,其规模与维护开销相比非分代模式在"单次回收"上更小、更聚焦(只处理本代对象),整体内存与 CPU 开销相对更低;内存多重映射(multi-mapping)用于把同一物理内存映射到多个地址空间以支持染色指针,分代模式仍保留该机制,但堆被代数切分后,映射的粒度与切换开销更加局部化。总体而言,分代模式利用弱代假说减少了对老年代与新生代的重复扫描,降低了转发表与多重映射的整体开销,同时保持亚毫秒级暂停。

分代 ZGC 的核心收益是"减少重复扫描老年代、聚焦新生代回收",从而降低转发表与多重映射在整体上的开销。它不改变 ZGC 的染色指针/多重映射/转发表机制,而是通过代数切分提升效率。

#
★★

34. ZGC 的 Sub-millisecond 暂停实现

ZGC 如何实现亚毫秒级(Sub-millisecond)暂停?

  • ZGC 的并发标记与整理
  • 染色指针与读屏障
  • STW 阶段的最小化

ZGC 实现亚毫秒暂停的关键是"几乎把所有工作都并发化"。ZGC 用染色指针(Colored Pointer)把对象的元信息(标记位、finalizable 位等)编码进指针的高位,结合读屏障(Load Barrier)在应用线程读取对象时同步更新引用,从而支持并发标记与并发整理。它的 STW 阶段被压缩到极小:初始标记、终局标记等 STW 阶段只扫描很少的根与元数据,而对象遍历、标记、指针修正、整理重定位全部并发进行。ZGC 的暂停时间与堆大小无关(不随堆增大而增长),只与 GC Roots 数量有关,因此能稳定在亚毫秒级甚至更低。

ZGC 的哲学是"把停顿从堆大小中解放出来"。染色指针 + 读屏障让 GC 与应用并发协作,STW 只做根相关的极小操作,因此暂停与堆规模解耦,实现亚毫秒级。

#
★★

35. ZGC 的染色指针与并发标记

ZGC 的染色指针(Colored Pointer)与并发标记是如何工作的?

  • 染色指针的结构
  • 并发标记的实现
  • 读屏障与自愈

ZGC 的染色指针把对象引用本身的若干高位位用作"颜色"(标记位、可终结位、以及预留的位移位),这些标记信息直接存放在指针中,无需给对象加锁或修改对象头。并发标记时,ZGC 通过染色指针访问对象,当读屏障发现对象当前状态(如未标记)与期望颜色不一致时,会"自愈"即更新引用让后续访问无色延迟。这样标记与整理可以与应用线程并发,无需 STW。染色指针的前提是对象地址必须对齐到一定边界(如 2MB/ZPages),由 ZGC 的堆布局(ZPage)保证,这也是地址空间内多重映射的基础。

染色指针把"标记信息"从对象头搬到指针位,配合读屏障和自愈,使并发标记/整理对应用透明。这是 ZGC 低暂停的技术基石,代价是消耗地址空间与额外的读屏障指令。

#
★★

36. ZGC 的染色指针与转发表(forwarding table)如何实现并发整理,多重映射会带来怎样的堆外内存开销

ZGC 的染色指针与转发表(forwarding table)如何实现并发整理,多重映射会带来怎样的堆外内存开销?

  • 染色指针与转发表
  • 并发整理机制
  • 多重映射的堆外开销

ZGC 并发整理时,把对象从旧位置复制到新位置,通过转发表(forwarding table)记录对象的新地址;应用线程通过读屏障访问对象时,若发现对象已被移动,则通过染色指针的某个位(M0/M1)结合转发表找到新位置并"自愈"更新引用。这样对象移动与访问并发进行,无需 STW。多重映射(multi-mapping)是 ZGC 把同一物理内存映射到多个虚拟地址空间(如 3 个视图),以便从不同视图访问时能利用染色指针的位而不改变物理地址,从而支持并发标记与整理。代价是堆外内存开销:每个视图都需要虚拟地址空间,且 ZGC 默认保留大量虚拟地址(如可能预留数倍于物理堆的地址空间),虽然物理内存不随之翻倍,但虚拟地址空间与页表开销以及调度的间接成本会增加。

转发表 + 读屏障自愈实现并发整理,多重映射提供地址空间视图以承载染色指针。多重映射主要消耗虚拟地址空间与页表管理开销,而非等量物理内存,这是 ZGC 在内存上没有"翻倍"但部署时需留意地址空间的因素。

#
★★

37. jstat -gc 与 jcmd GC.heap_info 的关键指标

jstat -gc 与 jcmd GC.heap_info 的关键指标有哪些,如何解读?

  • jstat -gc 的输出字段
  • jcmd GC.heap_info 的输出
  • 指标解读与诊断

jstat -gc 输出各代空间使用与 GC 计数,关键字段包括:S0C/S1C(Survivor 区容量)、S0U/S1U(使用量)、EC/EU(Eden 容量/使用)、OC/OU(老年代容量/使用)、MC/MU(Metaspace 容量/使用)、YGC/YGCT(Young GC 次数/时间)、FGC/FGCT(Full GC 次数/时间)等。jcmd GC.heap_info 则输出堆各区域(G1 为各 Region、ZGC 为各 ZPage)的容量、使用量、空闲量与对象统计,给出更细粒度的堆快照。解读方向:新生代频繁打满(EU 高)说明分配快;老年代持续增长(OU 上升)提示泄漏;FGC 频繁说明 Full GC 问题;通过对比 YGCT/FGCT 评估停顿成本。

jstat -gc 提供"时间序列"的动态指标,jcmd GC.heap_info 提供"静态快照"的空间明细。两者结合可判断分配速率、泄漏趋势与 GC 停顿,是线上排查的常用手段。

// jstat 观察 GC 动态指标(每 1 秒输出一次)
// jstat -gc <pid> 1000
// jcmd 查看堆快照
// jcmd <pid> GC.heap_info
#
★★

38. 分代收集中分配担保(HandlePromotionFailure)机制如何在晋升失败时触发 Full GC,JDK 8 前后规则有何变化

分代收集中分配担保(HandlePromotionFailure)机制如何在晋升失败时触发 Full GC,JDK 8 前后规则有何变化?

  • 分配担保机制
  • 晋升失败与 Full GC
  • JDK 8 前后的规则变化

分配担保(Handle Promotion Failure)指新生代 Minor GC 时,若存活对象无法全部放入 Survivor,则依赖老年代的空间来容纳晋升对象。JDK 8 之前,规则是:先检查老年代最大可用连续空间是否大于新生代对象总大小,若满足则采用冒险策略(Parallel Scavenge 的 HandlePromotionFailure),若担保失败则触发 Full GC;JDK 8 之后,该规则被简化统一——只要判断老年代可用空间是否足够容纳晋升对象,若不足则直接触发 Full GC(或使用 G1 等收集器的策略)。晋升失败(Promotion Failure)时,为保证正确性,JVM 会立即触发 Full GC 来整理并腾出空间。

分配担保的核心是"晋升前提是安全空间足够"。JDK 8 后趋向简化:直接把"老年代空间足以容纳晋升对象"作为硬条件,不足即 Full GC,减少了对历史不确定策略的依赖。

#
★★

39. 对象在堆中的内存布局(对象头、实例数据、对齐填充)

对象在堆中的内存布局是什么(对象头、实例数据、对齐填充)?

  • 对象头(Mark Word + Klass Pointer)
  • 实例数据
  • 对齐填充

对象在堆中的内存布局通常由三部分组成:对象头、实例数据、对齐填充。对象头(Object Header)包含 Mark Word(标记字,存哈希、锁状态、分代年龄、偏向锁标记等)和 Klass Pointer(类元数据指针,指向方法区中的类;开启压缩指针时可能压缩)。实例数据(Instance Data)是对象实际字段的值,按字段声明顺序与对齐规则排列,引用类型与基本类型各有布局。对齐填充(Padding)用于把对象总大小对齐到 8 字节(HotSpot 默认)或更大,便于指针寻址与压缩指针工作。理解对象布局对于计算内存占用、评估压缩指针(-XX:+UseCompressedOops)收益、排查过度对齐优化都很有价值。

对象布局决定内存占用与访问效率。对齐填充是"对齐而非浪费"(为压缩指针/寻址服务),对象头与指针压缩共同影响对象大小,进而影响堆使用与缓存命中率。

#
★★

40. G1 与 ZGC 的停顿模型差异,什么场景下 ZGC 的收益最大?

G1 与 ZGC 的停顿模型差异是什么,什么场景下 ZGC 的收益最大?

  • G1 的停顿模型(可预测、受堆影响)
  • ZGC 的停顿模型(亚毫秒、与堆解耦)
  • ZGC 收益最大的场景

G1 的停顿是可预测但受堆大小影响:停顿时间随堆中存活对象量与 Region 数量增长,通常在几十到几百毫秒,适合大多数服务。ZGC 的停顿是亚毫秒级且与堆大小解耦:停顿只与 GC Roots 数量相关,不随堆增长线性增加,适合大堆 + 低延迟场景。ZGC 收益最大的场景是:堆很大(如几十 GB 到上百 GB,若用 G1 停顿会过长)、且业务对延迟抖动极敏感(如交易、实时推荐、金融撮合),要求 P99/P999 稳定在毫秒级以下。但 ZGC 有额外开销(读屏障、染色指针),吞吐可能略低于 G1,且需较大堆才有收益,因此小堆 + 吞吐敏感场景用 ZGC 未必划算。

关键差异是"停顿是否随堆大小增长":G1 随堆增长,ZGC 与堆解耦。因此 ZGC 的收益在"大堆 + 低延迟"场景最大,小堆或吞吐优先场景 G1 更合适。

#
★★

41. 分代 GC 如何用卡表(Card Table)与记忆集(Remembered Set)记录跨代引用,写屏障(Write Barrier)在对象引用变更时如何标记脏卡,G1 的 Refinement 线程又如何把脏卡合并进 RSet 并用于 Young GC 根扫描

分代 GC 如何用卡表(Card Table)与记忆集(Remembered Set)记录跨代引用,写屏障如何标记脏卡,G1 的 Refinement 线程如何把脏卡合并进 RSet 并用于 Young GC 根扫描?

  • 卡表与记忆集的概念
  • 写屏障与脏卡标记
  • G1 的 Refinement 线程与 RSet

为避免 Minor GC 时全堆扫描老年代来找"老年代对象引用新生代对象"的跨代引用,分代 GC 用记忆集(Remembered Set)记录老年代中哪些位置持有指向新生代的引用。卡表(Card Table)是记忆集的一种实现:把堆划分为若干 512 字节的卡(Card),每张卡对应卡表中的一个字节;当老年代对象引用被修改(指向新生代)时,写屏障(Write Barrier)在写引用时把对应卡标记为"脏卡"(置 1)。G1 中,脏卡由 Refinement 线程(并发)异步处理:它们扫描脏卡,把卡内指向新生代对象的引用合并进对应 Region 的 RSet(Remembered Set)中。Young GC 时,G1 通过扫描各 Region 的 RSet 快速定位老年代中指向新生代的引用作为根,避免全堆扫描,从而提升 Young GC 效率。

卡表 + 写屏障 + RSet 是"用空间记录跨代引用、用屏障按需标记"的经典方案。写屏障在引用变更时标记脏卡,Refinement 线程异步把脏卡细化成 RSet 条目,Young GC 根扫描直接读 RSet,从而把"全堆扫描"变成"局部精确扫描"。

#
★★

42. 三色标记(Tri-color Marking)中黑/灰/白对象的判定标准是什么,并发标记阶段应用线程修改引用为何会产生漏标,CMS 的增量更新(Incremental Update)与 G1 的 SATB 分别通过拦截新增还是删除引用来修补

三色标记中黑/灰/白对象的判定标准是什么,并发标记阶段应用线程修改引用为何会产生漏标,CMS 的增量更新与 G1 的 SATB 如何修补?

  • 三色标记的判定
  • 并发漏标的原因
  • 增量更新 vs SATB

三色标记把对象分为:白色(未访问,可能为垃圾)、灰色(已访问但引用链未完全展开,需要继续扫描)、黑色(已访问且引用链完全展开,无需再扫)。并发标记时应用线程并发修改引用,可能产生漏标:若一个黑色对象新指向一个白色对象,而该白色对象原本只有通过灰色对象可达,且灰色对象到它的引用被删除,则白色对象会被误判为垃圾(漏标)。CMS 用增量更新(Incremental Update)修补:拦截"新增引用"——当黑色对象新增指向白色对象的引用时,把黑色对象重新标记为灰色,使其引用链被重新扫描。G1 用 SATB(Snapshot At The Beginning)修补:拦截"删除引用"——记录并发开始时的快照,把所有被删除的引用(以及借助屏障收集的旧值)保留下来,确保并发期间被删除引用的对象仍被当作存活,从而避免漏标。

漏标的本质是"并发修改破坏了标记一致性"。增量更新修补"新增引用把黑变灰",SATB 修补"所有可见旧引用都保留",两种策略都保证黑色对象不会指向未标记的白色对象,从而保证垃圾回收正确性。

#
★★

43. CompressedClassPointers 与元空间关系

CompressedClassPointers 与元空间(Metaspace)有何关系?

  • CompressedClassPointers 的作用
  • 与 Metaspace 中类元数据地址的关系
  • 压缩指针与寻址

CompressedClassPointers(压缩类指针)是 JVM 的一项优化,把对象头中指向类元数据(Klass)的指针从 64 位压缩为 32 位,从而减少对象头占用、提升内存利用与缓存命中率。它指向的是 Metaspace 中类元数据(Klass 结构)的地址。由于压缩需要类元数据驻留在足够小的地址空间内(通常限制在 4GB 以内),JVM 会为 Metaspace 预留一个紧凑的地址空间(CompressedClassSpace),以保证类指针可以压缩。因此 CompressedClassPointers 与 Metaspace 的关系是:压缩类指针依赖 Metaspace 中类元数据在受限地址空间内线性排列,一旦 Metaspace 地址空间耗尽或不再紧凑,压缩可能失效或触发相应调整。

CompressedClassPointers 是"压缩对象头中的类指针",它依赖 Metaspace 的紧凑地址空间。理解此关系可解释为何元空间使用压缩类指针、以及为何类元数据地址受限。

#
★★

44. G1 的 SATB(Snapshot At The Beginning)

G1 的 SATB(Snapshot At The Beginning)是什么,它如何工作?

  • SATB 的概念
  • SATB 与并发标记
  • SATB 的队列

SATB(Snapshot At The Beginning)是 G1 并发标记采用的技术,其思想是"记录并发开始时的对象引用快照"。在并发标记开始时,G1 通过 SATB 屏障把所有存活对象的引用(更准确地说,是并发标记期间被修改引用的旧值)记录下来,放入 SATB 队列。并发标记期间,即使应用线程删除了某个引用,被删除引用所指向的对象仍会被视为存活(因为快照中它本可达),从而避免漏标。G1 在最终标记(Remark)阶段处理 SATB 队列中残余的引用,完成标记。SATB 保证了"并发标记期间被删除引用的对象不会被误回收",是一种牺牲一点点精确度换取并发安全性的策略。

SATB 的取舍是"宁可多保留一些实际已死的对象,也不错回收存活对象"。它基于快照、记录删除引用,与 CMS 的增量更新(记录新增引用)形成互补,是 G1 并发标记正确性的关键。

#
★★

45. JEP 516 AOT Object Caching with Any GC(JDK 26)

JEP 516 AOT Object Caching with Any GC 是什么(JDK 26)?

  • AOT 对象缓存的机制
  • 与任意 GC 的兼容
  • 启动与内存收益

JEP 516(AOT Object Caching)是 JDK 26 的特性,允许在 JVM 启动时把预构建的 AOT(Ahead-of-Time)加载的对象图缓存到本地(如磁盘),在后续启动时直接加载,从而减少启动阶段的对象创建与类加载,加快冷启动。与"任意 GC"兼容意味着该缓存机制不依赖特定垃圾收集器,可与 G1、ZGC、Parallel 等共存,AOT 对象在被 GC 回收前会被当作不可变数据安全处理。其收益主要是缩短启动时间、降低启动期内存分配峰值,同时不牺牲运行期 GC 功能。

该特性把"启动对象"从代码执行中解放出来,变成可复用的 AOT 缓存,兼顾启动性能与 GC 兼容性。它属于"启动优化"范畴,而非运行期吞吐优化。

#
★★

46. JEP 522 G1 GC Throughput Improvement(JDK 26)

JEP 522 G1 GC Throughput Improvement 是什么(JDK 26)?

  • G1 吞吐改进的方向
  • 减少 GC 开销的手段
  • 对吞吐的影响

JEP 522(G1 GC Throughput Improvement)是 JDK 26 针对 G1 的吞吐优化特性,目标是降低 G1 在回收过程中的额外开销,提升整体吞吐。改进点通常包括:优化根扫描、RSet 更新、并发标记阶段的数据结构压缩、减少 GC 线程间同步与内存访问开销等,从而在维持可预测停顿的同时提升 GC 的吞吐(单位时间处理的对象量)。这使 G1 在吞吐敏感场景下也更有竞争力,减少对 Parallel GC 的依赖。

该特性是 G1 的"内功"优化,不改停顿模型,而是降低每单位回收的开销,让 G1 在相近停顿下吞吐更高,扩大 G1 的适用面。

#
★★

47. Shenandoah GC(OpenJDK)在 JDK 25 中与 ZGC 的低暂停时间对比

Shenandoah GC 在 JDK 25 中与 ZGC 的低暂停时间对比如何?

  • 两者都是低暂停收集器
  • 实现技术差异(Brooks 指针 vs 染色指针)
  • 暂停与开销对比

Shenandoah 与 ZGC 都是追求低暂停的并发收集器,暂停都能达到亚毫秒/毫秒级。区别在于实现:Shenandoah 用 Brooks 指针(对象头内转发指针)+ 读屏障实现并发整理,ZGC 用染色指针 + 多重映射 + 读屏障实现并发整理。JDK 25 中,Generational Shenandoah(JEP 521)转正为默认,进一步降低暂停与内存开销;ZGC 在 JDK 21 起默认分代。两者对比:ZGC 在超大堆上的暂停通常更稳定(与堆解耦),Shenandoah 在读写屏障开销上略低、附带内存更少(无多重映射的地址空间开销),但暂停受堆大小影响更大。实际选型要看堆规模、暂停目标与内存预算。

两者目标一致(低暂停),路径不同。Shenandoah 更"轻量"(无染色指针/多重映射的地址空间开销),ZGC 更"稳定"(暂停与堆解耦)。选型落在"堆规模 + 暂停阈值 + 内存开销"的权衡上。

#
★★

48. UseStringDeduplication 在 G1 中的实现

UseStringDeduplication 在 G1 中是如何实现的?

  • 字符串去重机制
  • G1 中的实现细节
  • 去重收益与限制

-XX:+UseStringDeduplication 是 G1 的字符串去重特性,用于减少堆中重复字符串对底层 char[] 的占用。G1 在 Young GC 时对逃过回收的字符串对象进行哈希,引用相同内容(相同 char[])的重复字符串改为共享同一个 char[],从而节省内存。它默认在到达老年代前对字符串去重,仅在 G1 下可用,通过 -XX:StringDeduplicationAgeThreshold 控制对象年龄阈值。去重会带来额外的哈希计算开销,但能显著降低重复字符串场景的内存占用(如大量相同 ID、状态值)。限制是它不能用于修改字符串内容的情况,且只在字符串内容相同时生效。

该特性利用"字符串内容不可变"的特性,通过共享 char[] 去重,是 G1 特有的内存优化。适合内容重复率高的场景,但需评估哈希开销。

// 启用 G1 字符串去重
// -XX:+UseG1GC -XX:+UseStringDeduplication -XX:StringDeduplicationAgeThreshold=3
#
★★

49. 内存泄漏与内存溢出的诊断(MAT、LeakCanary)

如何诊断内存泄漏与内存溢出(使用 MAT、LeakCanary)?

  • 内存泄漏 vs 内存溢出的区别
  • MAT 的堆分析
  • LeakCanary 的 Android 场景

内存泄漏(Leak)是对象不再被使用但仍被引用,导致无法被 GC 回收、内存持续增长;内存溢出(OOM)是分配失败,是泄漏或分配超限的结果。诊断工具:MAT(Memory Analyzer)是 Java 堆分析工具,通过加载堆 dump(hprof),用 Leak Suspects、Dominator Tree、查询 GC Roots 路径等定位泄漏对象及其引用链;LeakCanary 是 Android 的内存泄漏检测库,在 Activity/Fragment 销毁后自动检测其是否仍被引用,发现泄漏即可定位到持有者。线上排查流程:抓 heap dump → MAT 分析 Retained Heap 找大对象与泄漏根 → 对照业务代码确认修复。

泄漏诊断的关键是"找到谁在引用本该回收的对象"。MAT 用支配树与 GC Roots 路径定位引用链,LeakCanary 自动检测生命周期对象泄漏。两者先定位根因再修复,避免 OOM。

#
★★

50. 分代 Shenandoah 的内存收益与吞吐影响

分代 Shenandoah 的内存收益与吞吐影响如何?

  • 分代 Shenandoah 的内存收益
  • 吞吐影响
  • 与单代模式的对比

分代 Shenandoah(Generational Shenandoah,JDK 25 转正)把堆分为新生代与老年代,利用弱代假说:新生代对象存活率低,用更频繁、更局部的小回收处理,避免像单代模式那样频繁扫描整个堆,从而降低内存清理开销与暂停,提升内存利用率(减少对老年代的重复扫描)。吞吐影响上,分代模式通过减少全堆扫描与并发标记的开销,整体吞吐通常优于单代模式,但代价是引入分代管理(晋升、年龄、跨代引用)的额外复杂度与读屏障仍存在。综合来看,分代 Shenandoah 在内存与吞吐上都优于单代,是低延迟 + 内存效率的良好平衡。

分代的核心收益是"把回收聚焦到新生代",减少对老年代/全堆的重复扫描,从而在保持低暂停的同时提升内存与吞吐效率。这是弱代假说在 Shenandoah 上的成功应用。

#
★★

51. 对象晋升阈值(-XX:MaxTenuringThreshold)与动态年龄判定的机制?

对象晋升阈值(-XX:MaxTenuringThreshold)与动态年龄判定的机制是什么?

  • MaxTenuringThreshold 的语义
  • 动态年龄判定
  • 晋升与 Survivor 空间

-XX:MaxTenuringThreshold 是对象晋升的最大年龄阈值,对象在 Survivor 中每经历一次 Minor GC 年龄 +1,达到该阈值后晋升到老年代。这是"静态"阈值。但 JVM 还有"动态年龄判定":当 Survivor 中同龄对象总大小超过 Survivor 空间的一定比例(如 HPS 的一半)时,JVM 会提前把该年龄及以上的对象晋升,而不必等到 MaxTenuringThreshold。这样可在 Survivor 空间不足时自动调整,避免 Survivor 溢出。动态判定的目的是让晋升更贴合实际存活情况,平衡晋升成本与 Survivor 空间。

晋升阈值分静态(MaxTenuringThreshold)与动态(Survivor 空间比例触发提前晋升)两种。动态判定让晋升更灵活,避免在 Survivor 空间紧张时仍死守固定阈值。

#
★★

52. 如何用 JDK Flight Recorder + Async Profiler 定位 CPU 高与 GC 频繁问题?

如何用 JDK Flight Recorder(JFR)+ Async Profiler 定位 CPU 高与 GC 频繁问题?

  • JFR 的采集与事件
  • Async Profiler 的采样
  • 结合定位 CPU 高与 GC 频繁

JFR(JDK Flight Recorder)是内置的剖析与事件记录工具,可采集 GC 事件(jdk.GCPhase、jdk.GCAllocation)、CPU 采样、线程、锁等;Async Profiler 是基于异步采样的 CPU 剖析器,可生成火焰图。定位 CPU 高时:用 Async Profiler 采集 CPU 采样,生成火焰图找到热点调用栈;用 JFR 的 CPU 采样与 jdk.ThreadAllocationTotal 事件结合线程定位。定位 GC 频繁时:用 JFR 的 GC 事件(jdk.GCPhase、jdk.GCAllocation)看 GC 频率、暂停、分配速率,结合 jdk.ObjectAllocationSample 找分配热点,明确是"分配过多导致 GC 频繁"还是"GC 本身异常"。两者结合可把"CPU 高"与"GC 频繁"同时归因到具体代码路径。

技术要点是"纵深采样 + 归因"。Async Profiler 提供 CPU 火焰图,JFR 提供 GC 与分配事件,组合后可区分"应用 CPU 高"与"GC CPU 高"两种场景,并定位到热点代码。

// Async Profiler 采集 CPU 火焰图
// ./profiler.sh -d 30 -e cpu -f flamegraph.html <pid>
// JFR 采集(start 后 dump)
// jcmd <pid> JFR.start duration=60s filename=rec.jfr
#

53. 虚拟线程批量创建对 GC 的影响,栈块(StackChunk)缓存与堆占用

虚拟线程批量创建对 GC 有何影响(栈块 StackChunk 缓存与堆占用)?

  • 虚拟线程的栈块机制
  • 栈块缓存的复用
  • 对 GC 的影响

虚拟线程(Virtual Threads,JDK 21)的栈不在操作系统线程中,而是以可增长的"栈块"(StackChunk)形式保存在堆(或堆外)中,由 JVM 管理。虚拟线程批量创建时,若栈块频繁分配又释放,会带来对象分配与 GC 压力;JVM 通过栈块缓存(StackChunk 对象池)复用已释放的 StackChunk,减少分配与 GC 频率。对 GC 的影响:大量虚拟线程的栈块占用堆内空间,若长期存活或缓存不释放,会增加堆占用与 GC 回收成本;但缓存机制能显著降低短生命周期虚拟线程的分配压力。因此设计上应合理控制虚拟线程数量与栈块复用,避免海量持久虚拟线程拖垮堆。

虚拟线程的内存成本是"栈块",而非 OS 线程栈。缓存复用降低分配,但海量存活虚拟线程仍占堆空间,需关注堆占用与 GC 影响。

#

54. JDK 25 的 GC 演进全景(JEP 521 Generational Shenandoah 转正)

JDK 25 的 GC 演进全景是什么(JEP 521 Generational Shenandoah 转正)?

  • JEP 521 转正
  • 分代收集器格局
  • 未来演进方向

JDK 25 的 GC 演进重点是 JEP 521:Generational Shenandoah 正式转正(从实验性变为生产可用),成为 Shenandoah 的默认模式,利用分代提升内存与吞吐。至此,ZGC 与 Shenandoah 都已演进为分代模式,与 G1 一样利用弱代假说。JDK 25 的 GC 全景是:G1(默认,通用)、ZGC(分代,亚毫秒低延迟)、Shenandoah(分代,低延迟 + 内存效率)、Parallel(吞吐优先)、Epsilon(测试用)。演进方向是"分代化 + 低延迟 + 更智能的默认参数",同时配合 JEP 515 等 Ergonomics 减少手动配置。

JDK 25 的 GC 格局是"分代收集器成为主流",ZGC 与 Shenandoah 都转分代。这标志着弱代假说被主流低延迟收集器普遍接受,并朝"低延迟 + 内存效率 + 易配置"演进。

#

55. 对象晋升(Promotion)与年龄阈值

对象晋升(Promotion)与年龄阈值的关系是什么?

  • 晋升机制
  • 年龄阈值的计算
  • 晋升与 Survivor

对象晋升(Promotion)是新生代对象在经历多次 Minor GC 后进入老年代的过程。对象在 Eden 中创建,若存活会被复制到 Survivor(S0/S1),每次 Minor GC 存活则年龄 +1;当对象年龄达到 -XX:MaxTenuringThreshold(默认 15)时,晋升到老年代。另外,若 Survivor 空间不足或动态年龄判定(Survivor 中同龄对象超阈值)触发,也会提前晋升。晋升是分代收集的关键:让短命对象留在新生代快速回收,长命对象进入老年代避免反复复制。

晋升由"年龄"与"空间"共同决定。年龄阈值控制晋升时机,Survivor 空间决定是否提前晋升,两者保障新生代高效回收与老年代空间平衡。

#

56. JDK 25 中 jcmd GC.heap_dump 与 JFR 事件 jdk.GCPhase 的诊断价值

JDK 25 中 jcmd GC.heap_dump 与 JFR 事件 jdk.GCPhase 的诊断价值是什么?

  • jcmd GC.heap_dump 的用法
  • jdk.GCPhase 事件
  • 诊断价值

jcmd GC.heap_dump 用于生成堆 dump(hprof 文件),可搭配 MAT 分析对象占用、泄漏与引用链,是排查内存问题的核心手段。JFR 事件 jdk.GCPhase 记录 GC 各阶段(如标记、回收、清理、晋升)的起止时间与耗时,可精确分析 GC 停顿发生在哪个阶段、是否由具体阶段拖慢,从而判断是根扫描、标记、还是整理阶段耗时。两者结合:先用 GC.heap_dump 看"内存里有什么",再用 jdk.GCPhase 看"GC 时间花在哪",实现从现象到根因的定位。

GC.heap_dump 回答"堆里有什么",jdk.GCPhase 回答"GC 时间花哪"。在 JDK 25 中两者配合能高效完成内存与 GC 的联合诊断。

#

57. 堆外内存(DirectBuffer)的回收机制与常见泄漏排查方法?

堆外内存(DirectBuffer)的回收机制是什么,常见泄漏排查方法有哪些?

  • DirectBuffer 的回收机制(Cleaner)
  • 泄漏场景
  • 排查方法(NMT 等)

DirectBuffer 的底层内存由 Cleaner 机制回收:DirectBuffer 对象本身在堆上,其内部通过 Cleaner 关联一个 Runnable,当 DirectBuffer 对象变得不可达被 GC 回收时,Cleaner 回调释放底层 Native 内存。因此回收依赖"堆上 DirectBuffer 引用可回收" + "Cleaner 触发"。常见泄漏场景:DirectBuffer 被长期持有未释放、Netty 未 release 引用计数、线程局部缓存未清理、或 Cleaner 机制被禁用。排查方法:开启 -XX:NativeMemoryTracking=summary 查看堆外内存占用分布,用 jcmd VM.native_memory 对比;用 jstat/jcmd 观察 DirectBuffer 数量;用 Netty 的泄漏检测(-Dio.netty.leakDetection.level=paranoid);必要时用反射统计 Bits.totalCapacity 或通过 jmap 分析堆中 DirectBuffer 对象。

DirectBuffer 回收是"双阶段":堆引用回收 + Cleaner 释放。泄漏往往源于堆引用未释放或 Cleaner 未触发。NMT 是定位堆外占用最直接的监控手段。

// 启用 NMT 并查看堆外分布
// -XX:NativeMemoryTracking=summary
// jcmd <pid> VM.native_memory summary.diff
// Netty 泄漏检测
// -Dio.netty.leakDetection.level=paranoid