cpu profile 不能直接发现死亡代码,但能识别因引用残留而被gc反复扫描的伪活跃死亡代码,表现为高频分配却零引用、gc压力高但无业务调用栈支撑。

分析 CPU Profile 采样本身不能直接发现“死亡代码”(即永远不会执行的代码),但它能高效暴露 GC 过程中被误判为“活跃”、实则已失效的逻辑路径——这类代码常表现为高频分配却零引用、高 GC 压力但无业务调用栈支撑,是典型的“伪活跃死亡代码”。关键不在于“找没执行”,而在于“识别不该活却一直被 GC 反复扫描的代码块”。
CPU 采样揭示 GC 负担异常来源
GC 的开销不仅来自回收动作本身,更来自对“存活对象图”的遍历与标记。若 CPU Profile 显示 GC 线程(如 G1 Concurrent Mark、ZGC Mark Loop)或应用线程在 分配热点附近持续高占用,需警惕:这些分配可能由一段已脱离业务主干、却因引用残留未被清除的代码触发。
- 在火焰图中定位
java.lang.Object.<init></init>或java.util.ArrayList.<init></init>等构造方法的顶层调用者——若其父调用栈中出现已废弃模块名、测试类名、或仅在冷启动/旧配置下才加载的 Bean,则该分配极可能属于“死亡代码” - 对比不同负载下的采样分布:若某段分配逻辑在正常流量下几乎不出现,却在 GC 阶段反复出现在标记线程的栈中(如
G1ConcurrentMarkThread::run→HeapRegion::oops_do→ 你的类),说明它正被 GC 扫描器“拖着走”,而非真实业务驱动
结合分配采样与引用链反查
JFR 的 jdk.ObjectAllocationInNewTLAB 和 jdk.ObjectAllocationOutsideTLAB 事件可与 CPU 方法采样对齐。当发现某方法频繁分配对象,但其返回值从未出现在任何有效调用者的局部变量或字段中时,该方法即为高嫌疑目标。
- 启用 JFR 分配采样:
-XX:StartFlightRecording=settings=profile,stacktrace=true,确保开启jdk.ObjectAllocationInNewTLAB - 在 JMC 中筛选“Allocation Size > 0”且“Stack Trace”中无下游消费痕迹的方法(例如:返回
List却从不被for遍历、不被add到容器、不参与日志拼接) - 使用
jcmd <pid> VM.native_memory summary</pid>辅证:若某类实例数长期高位滞留,但对应类的静态方法调用频次趋近于零,基本可判定其对象创建逻辑已“死亡”
识别被 GC 机制掩盖的无效生命周期管理
某些“死亡代码”不直接分配,而是通过注册监听器、定时任务、弱引用队列等机制维持对象间接存活,导致 GC 无法回收,徒增标记压力。CPU Profile 中这类行为常体现为:
- 周期性唤醒的线程(如
ScheduledThreadPoolExecutor$ScheduledFutureTask.run)反复执行空逻辑或仅调用已注销的回调 -
ReferenceQueue.poll()或PhantomReference.get()在火焰图中高频出现,但后续无实际清理动作 - 使用 JFR 查看
jdk.GCPhasePause事件,若某次 Full GC 后jdk.ObjectCount下降极少,再叠加上述 CPU 栈特征,说明大量对象因“幽灵引用”而苟活
验证与清理建议
确认后,不要仅删除代码,需验证其移除是否影响 GC 行为:
- 上线前用相同负载压测,对比 JFR 中
jdk.GCPhasePause的平均耗时与jdk.ObjectAllocationInNewTLAB总量变化 - 检查日志中是否仍有该模块的初始化记录(如 Spring 的
InitializingBean.afterPropertiesSet)、或监控埋点(如 Micrometer 的timer统计)归零 - 对疑似监听器类,搜索项目中所有
addListener(.*yourClass.*)调用,确认是否已被注释或条件屏蔽










