短信平台频繁 Full GC 问题排查与优化
目录
一、问题现象
- 服务:
lark-smsx [8080],短信项目 - 告警:
full gc count > 5/min - 监控图表显示Full GC频繁,峰值达40次,长期在告警阈值附近徘徊
- 之前通过修改JVM参数(CMS换G1)临时缓解,但未根治
二、排查过程
第一步:确认GC日志位置和版本
# 确认日志是否最新
tail -20 gc.log
发现日志头部CommandLine flags显示使用的是 CMS GC(-XX:+UseConcMarkSweepGC),而不是之前以为的G1,原来两台机器只有一台改了参数。
教训:排查前先确认当前环境的实际配置,不要想当然。
第二步:找到Full GC发生时间段的日志
grep "2026-04-15T08:3[0-9]" gc.log | head -30
发现报警时间段内Old区数据:
CMS-remark: 1292342K(1835008K) ← 回收前
CMS-remark: 1292342K(1835008K) ← 回收后几乎没变化
关键发现:CMS每轮跑完,Old区只回收了几KB,基本等于没回收。
第三步:定位Full GC触发瞬间
通过 grep -i "full" 找到full计数器,再用行号定位:
grep -n "full 9967\|full 9968\|full 9969\|full 9970" gc.log | head -20
sed -n '1811981,1812030p' gc.log
观察到Old区变化:
1294668K → sweep → 1294656K → sweep → 1294654K → sweep → 1294650K
每次CMS完整回收一轮,Old区只减少几KB,对象几乎全部存活,Old区迟早撑爆触发Full GC。
第四步:用jmap找异常对象
jmap -histo $(pgrep -f lark-smsx) | head -30
输出关键数据:
1: 7946763 764919576 [C
2: 7466588 298663520 java.util.LinkedHashMap$Entry
3: 7693042 184633008 java.lang.String
发现:746万个LinkedHashMap$Entry占285MB,异常明显。
第五步:排除误判,验证数据量
初步怀疑是 QuotaFilter 每次请求都调用 hgetAll 产生大量LinkedHashMap,但验证后发现:
redis-cli hlen quota # 返回 6
Redis里quota只有6条数据,和jmap里257个Entry/Map对不上,说明LinkedHashMap来源另有其他地方,初步分析方向有偏差。
教训:分析结论要用数据验证,不能只看代码推断。
第六步:jmap -dump + MAT深度分析
-histo 只能看到有什么对象,看不出谁持有这些对象,需要dump文件分析引用链:
# 确认磁盘空间(dump文件约等于堆实际使用量,这里约1.5G)
df -h /tmp
# 生成堆快照
jmap -dump:format=b,file=/tmp/heap.hprof $(pgrep -f lark-smsx)
下载到本地用 MAT(Eclipse Memory Analyzer) 打开,Leak Suspects报告显示:
java.io.DeleteOnExitHook 占堆内存94%
内部LinkedHashMap有600万+条 /tmp/jar_cacheXXXX.tmp 路径
MAT分析过程,使用柱状图其实就能发现这个DeleteOnExitHook的问题,浅堆为0,深堆很大。

用支配树能发现这里的jar_cachexxx是有问题的,但是还是没找到代码中在哪使用
Leak Suspects(可疑泄漏对象) 是 MAT 自动分析后给出的:最可能导致内存占用异常 / 内存泄漏的对象集合
其实在这MAT就已经发现问题了,后续我在代码中搜索deleteOnExit(),搜不到,就有了接下来的步骤。
第七步:Arthas追踪调用栈
项目代码里搜不到 deleteOnExit(),说明是框架内部调用,用Arthas动态追踪:
# 挂载到目标进程
java -jar arthas-boot.jar <pid>
# 开启 unsafe 模式(允许增强 JDK 类)
options unsafe true
# 追踪调用栈
stack java.io.File deleteOnExit -n 5
调用栈:
java.io.File.deleteOnExit()
← sun.net.www.protocol.jar.URLJarFile.retrieve() # JDK 创建 jar_cache 临时文件
← JarURLConnection.getContentLength() # 获取 JAR 内资源大小
← AbstractFileResolvingResource.contentLength() # Spring 读取资源的 Content-Length
← ResourceHttpRequestHandler.setHeaders() # 设置 HTTP 响应头
← ResourceHttpRequestHandler.handleRequest() # 服务静态资源
← InternalResourceView.renderMergedOutputModel() # Controller 视图转发
← HttpServlet.doHead() # 收到 HTTP HEAD 请求
ResourceHttpRequestHandler 服务静态文件 + InternalResourceView 做转发,这个组合在 Spring Boot 里几乎只有 Swagger UI 会触发 查看 celebi-server-1.8.7.jar 内部,发现 SwaggerConfiguration.class,确认 celebi 框架自动开启了 Swagger 常量池中发现配置开关 celebi.server.swagger.enabled,且 matchIfMissing=true(不配置就默认启用) 指向Swagger UI的静态资源请求。
三、根本原因
两个因素叠加导致问题:
| 因素 | 说明 |
|---|---|
| Spring Boot 1.5.x缺陷 | ResourceHttpRequestHandler 每次请求都通过 JarURLConnection 读取嵌套JAR内文件大小,不缓存,每次都创建临时文件并调用 deleteOnExit() |
| Celebi框架默认开启Swagger | 生产环境不需要Swagger,但 celebi.server.swagger.enabled 默认为true,Swagger UI静态资源暴露在外 |
完整触发链路:
监控/网关每秒发送 HEAD /swagger-ui.html
→ Spring ResourceHttpRequestHandler 服务 webjar 内静态文件
→ 每次请求打开 JarURLConnection(无缓存)
→ URLJarFile.retrieve() 创建临时文件
→ File.deleteOnExit() 写入 DeleteOnExitHook.files
→ LinkedHashSet永不清除,持续累积
→ 运行数天后累积600万条 → 285MB堆内存 → Full GC
四、解决方案
在生产环境配置中加入:
# 关闭Swagger(治本)
celebi.server.swagger.enabled=false
# 开启静态资源缓存(兜底)
spring.resources.chain.enabled=true
spring.resources.chain.cache=true
spring.resources.cache.period=86400
重启后 DeleteOnExitHook 不再增长,Full GC消失。
五、工具总结
| 工具 | 用途 | 本次使用场景 |
|---|---|---|
| gc.log | 查看GC历史记录、触发原因、各区内存变化 | 确认Full GC存在,发现Old区无法回收 |
| jmap -histo | 快速列出堆内所有类的实例数和内存占用 | 发现LinkedHashMap异常堆积 |
| jmap -dump | 导出完整堆快照供MAT分析 | 找到DeleteOnExitHook占94%堆内存 |
| MAT | 分析堆快照,找内存泄漏和引用链 | 定位到具体持有者和路径 |
| Arthas | 动态追踪方法调用栈,不需要重启 | 找到deleteOnExit()的调用来源 |
排查优先级:gc.log → jmap -histo → jmap -dump + MAT → Arthas,从轻到重,够用就不用下一级。
六、额外收获
CMS换G1治标不治本:另一台换成G1的机器根因相同,早晚也会Full GC,需要同步修复。
jmap -histo的局限性:只能看到对象数量和大小,看不出引用关系。当 -histo 结果无法直接定位问题时,必须上MAT分析引用链。
CMS GC和Full GC的区别:
- CMS GC:并发回收,和应用线程同时运行,不会停顿,频繁触发没关系
- Full GC:STW(Stop The World),应用完全停止,这才是报警的原因
CMSInitiatingOccupancyFraction=70触发的是CMS GC,不是Full GC