当你面对一份JVM GC日志时,首先要学会像侦探一样寻找线索。我处理过上百个线上性能问题,发现90%的Full GC问题都能通过日志中的这几个关键指标定位:
**停顿时间(Pause Time)**是最直观的信号。正常情况下,Young GC的停顿应该在50ms以内,而Full GC通常在秒级。如果你看到类似[Full GC (Allocation Failure) 3.142s]这样的记录,就要警惕了。去年我们有个电商系统频繁出现2秒以上的Full GC,导致支付超时,就是从这里发现的。
内存变化曲线是第二个关键点。健康的系统内存使用应该像波浪线一样规律起伏。你需要特别关注老年代(Old Gen)的使用情况:
bash复制[PSYoungGen: 6144K->640K(9216K)]
[ParOldGen: 31744K->32256K(35840K)]
像上面这段日志,老年代在GC后反而增长了,说明有对象在异常晋升。我遇到过最典型的案例是一个缓存服务,由于没有设置合理的TTL,导致老年代被撑爆。
**GC原因(GC Cause)**是定位问题的金钥匙。常见的触发原因包括:
上周排查的一个案例就很有意思:日志里频繁出现[Full GC (Metadata GC Threshold),最后发现是有人用ASM动态生成类却没控制数量。通过-XX:MetaspaceSize=256M调整参数后问题立竿见影地解决了。
这是新手最容易踩的坑。当年轻代对象要晋升到老年代时,JVM会先检查老年代剩余空间是否足够。如果不够,就会触发Full GC。这种场景的日志特征非常典型:
bash复制[Full GC (Promotion Failed)
[PSYoungGen: 6144K->6144K(9216K)]
[ParOldGen: 31744K->31744K(35840K)]
去年双11大促前,我们的推荐系统突然出现性能抖动,就是这种case。根本原因是某个新上线的特征计算服务产生了大量大对象,直接越过了年轻代。解决方案是双管齐下:
-XX:NewRatio=2(默认值就是2,我们调整到1)-Xmn512m(原值256m)随着微服务架构流行,这类问题越来越多。它的典型日志是这样的:
bash复制[Full GC (Metadata GC Threshold)
[Metaspace: 256000K->256000K(257024K)]
我建议用以下参数来防御:
bash复制-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=512M
-XX:+TraceClassLoading
特别要注意的是,Spring、Hibernate这类框架以及任何使用字节码增强技术(比如JavaAgent)的场景都容易引发这个问题。去年有个团队使用MyBatis时,因为没配置mapperLocations,导致每次查询都重新解析XML,最终Metaspace爆炸。
有些对象天生就是"特权阶级",比如大数组、大字符串。当对象大小超过-XX:PretenureSizeThreshold(默认0,表示全部走年轻代)时,会直接进入老年代。这类问题的日志往往伴随着老年代的突然增长:
bash复制[ParOldGen: 20480K->24576K(35840K)]
解决方案有三板斧:
-XX:PretenureSizeThreshold=1M第三方库最爱干这事!日志里看到这种记录就要提高警惕:
bash复制[Full GC (System.gc())
快速验证方法是用jstack查调用栈。终极解决方案是:
bash复制-XX:+DisableExplicitGC
但要注意,有些NIO框架(比如Netty)依赖System.gc()来管理堆外内存,这时候可以用-XX:+ExplicitGCInvokesConcurrent让CMS或G1来并发处理。
这是CMS回收器特有的问题,日志特征非常明显:
bash复制[Full GC (Concurrent Mode Failure)
解决方法包括:
-XX:CMSInitiatingOccupancyFraction=75(默认68)根据多年大厂经验,我总结了一套通用参数模板:
bash复制# 基础内存设置
-Xms4g -Xmx4g -Xmn2g
# CMS专用配置
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=80
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+CMSScavengeBeforeRemark
# 通用防御配置
-XX:+DisableExplicitGC
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump.hprof
但切记,参数绝不是银弹。去年我们一个日活千万的应用,就因为盲目套用"优化参数"导致STW时间从200ms飙升到2s。一定要遵循"观察-调整-验证"的循环。
参数调优治标,代码优化治本。分享几个实战技巧:
对象池化:对于频繁创建的大对象,比如数据库连接、网络连接等,使用对象池可以显著减轻GC压力。我们有个交易系统通过改造StringBuilder池,Young GC频率下降了60%。
集合初始化指定大小:ArrayList、HashMap这些集合在扩容时会产生大量垃圾。建议根据业务场景初始化合适大小:
java复制// 不好的写法
List<User> users = new ArrayList<>();
// 优化后
List<User> users = new ArrayList<>(100);
避免在循环内创建对象:这是代码审查中最常发现的问题。比如:
java复制// 反例
for (Order order : orders) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// ...
}
// 正解
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (Order order : orders) {
// ...
}
没有监控的优化就是盲人摸象。建议所有线上应用都开启完整GC日志:
bash复制-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:/path/to/gc.log
对于Java 9+的应用,强烈推荐使用新的统一日志框架:
bash复制-Xlog:gc*=info:file=/path/to/gc.log:time,tags:filecount=5,filesize=100m
我们在大厂实践的最佳方案是:
关键告警阈值建议:
任何JVM参数调整都必须经过压测验证。我们团队的标准流程是:
bash复制jstat -gcutil <pid> 1000 10
记得去年有个惨痛教训:某次大促前调整了GC参数但没有充分压测,结果大促当天Full GC频率暴涨。现在我们的原则是:没有经过至少24小时压测验证的参数,绝不上生产。