背景
中秋前的周五晚上,订单服务开始出现间歇性超时,监控上 RT 从 80ms 涨到 1.2s,同时观察到 Full GC 频率从每天两次变成每十分钟一次。这篇文章记录完整的排查过程。
第一步:先看 GC 日志
jstat -gcutil $(pgrep -f order-service) 1000 20
Old 区使用率在 92% 上下反复横跳,每次 Full GC 只能回收 3% 左右,典型的"回收不动"。
第二步:dump 堆
jmap -dump:live,format=b,file=/tmp/heap.hprof $(pgrep -f order-service)
用 MAT 打开后,Dominator Tree 第一位是一个 1.8G 的 ConcurrentHashMap,占整个堆的 70%。
第三步:找到谁在往里写
顺着引用链找到业务代码里的本地缓存:key 是「用户 ID + 日期」,但过期清理任务因为一次重构被删掉了。每天新增约 40 万条,跑了两周就撑爆了老年代。
结论
- 本地缓存必须有过期策略,最好用带容量上限的实现(Caffeine)。
- JVM 参数里加
-XX:+HeapDumpOnOutOfMemoryError,出事时能省很多时间。 - 给堆加个容量告警,别等 Full GC 找上门。
这次问题本身不复杂,难的是从「接口慢」推导到「缓存没清」,中间花了大半天。