开放问答
Java 定时任务跑完堆内存阶梯式不回落:p99 每 4 小时恶化一次,如何定位是泄漏还是缓存驻留?
该问题的正文、回答与评论。
提问者 程寒山-377 · 状态 open
JavaG1GC定时任务内存泄漏性能调优Guava
【背景】 我做了 10 年性能优化,最近带团队复盘一个 Java 17(G1,-Xmx6g)的订单中台服务,单机 8C16G,QPS 约 3k。服务内有一个每 4 小时跑一次的库存对账定时任务:拉取上游全量 SKU(约 40 万条)到内存做 diff,单条约 2KB,峰值临时对象约 1GB。
【现象】
- 日常 P50 稳定在 12ms,但每次对账任务结束后 30 分钟内,P99 从 45ms 涨到 220ms 左右,持续约 1–2 小时后才缓慢恢复;
- 用
jcmd VM.heap_info看,任务期间 old gen 占用冲到 5.2g,任务结束后只回落到 3.8g 左右,不再继续下降——呈现出明显的「阶梯式」曲线,每 4 小时抬一档; - GC 日志显示任务结束后 1 小时内 young GC 频率从 2 次/分钟涨到 8 次/分钟,full GC 没出现,但单次 young GC 停顿从 3ms 涨到 25ms。
【已尝试的排查与结果】
- 抓了任务结束后的 heap dump(约 4GB),用 MAT 做 Dominator Tree:Top 5 对象里没有明显的泄漏,最大的 retained 是 Guava Cache(约 1.8g,
maximumSize=20000按 weight 算应该只占 800MB)和 HikariCP 连接池里的 pending 查询结果集(约 300MB,正常); - 怀疑是 Guava Cache 的
weigher算错了——weight 按 JSON 字节算,但实际对象图里有反序列化的嵌套对象(SKU → 规格 → 属性树),weigher 没算到嵌套层,于是缓存实际驻留远超预算;这个还没验证,因为运行时不好直接遍历 cache 的 entry 算真实内存; - 另一个嫌疑:对账任务里用了
CompletableFuture并行处理 40 万条 SKU,每个 future 的 completion 链上挂了一个AtomicReference<Exception>+ 一个 1KB 的 trace 上下文,如果 task 没被显式清理,40 万个 future 的残留引用可能撑住 200–400MB——但任务结束后 GC 应该能回收,为什么没回收?
【期望得到的答案形态】
- 有没有运行时直接测量 Guava Cache 真实驻留内存的方法?(不是 dump 后离线分析,而是
jcmd/ JFR / 自定义 MBean 之类,能告诉我 cache 现在到底占多少); - G1 的「旧对象晋升后不主动回缩」是不是已知行为?有没有参数或 JFR 事件能让我看到对象晋升的具体路径(哪条调用链把临时对象 promote 到了 old gen)?
- 如果是 future 残留引用,用 JFR 的
jdk.ObjectAllocationInNewTLAB+jdk.ObjectAllocationOutsideTLAB事件能不能精确到 40 万个 future 各自的 retained size?还是说 JFR 只能看分配点、看不到引用链?
欢迎有类似「定时任务 + 堆不回落」线上经验的同僚分享定位思路,尤其是不用 heap dump 就能定位的方法(我们 dump 一次要 8 分钟,线上不敢随便抓)。
已采纳
回答者
回答正文
评论
暂无评论。