你有没有遇到过这样的情况:线上服务跑着跑着就变慢,监控里老是看到 Full GC 频繁触发,堆内存使用率长期卡在 95% 以上,重启一下暂时缓解,过两小时又拉响警报?这不是服务器不够强,很可能是堆内存没调对。
别盲目加-Xmx,先看对象去哪儿了
很多同学一上来就改 -Xmx4g,以为堆越大越稳。其实错得挺远——堆大了,GC 暂停时间反而更长,尤其 CMS 或 G1 在大堆下容易产生碎片或退化成 Serial GC。真正该盯的是:哪些对象在堆里赖着不走?
用 jstat -gc <pid> 看一眼年轻代回收频率和老年代增长速度。如果每次 YGC 后老年代都涨一截,大概率是对象提前晋升——比如大数组、缓存没设上限、日志对象反复 new 但没及时释放。
三个实操见效的优化点
1. 控制对象生命周期,别让缓存吃光堆
比如用 HashMap 做本地缓存,没加 size 限制和过期机制,用户 ID 一多,几万条缓存直接占掉几百 MB。换成 Caffeine,加上最大容量和写后 10 分钟过期:
Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> loadData(key));2. 小心“假小对象”,String.substring 和 ArrayList.subList
JDK 7u6 之前,substring 会共享原字符串的 char[] 数组,一个 1MB 的日志字符串,只取最后 10 个字符,结果整个数组还挂在堆里。现在虽已修复,但类似陷阱还有:ArrayList.subList 返回的是原 list 的视图,若长期持有 subList,等于锁死了整个底层数组。
3. 日志和调试代码,上线前务必清理
开发时随手写的 logger.debug("request: {}", JSON.toJSONString(req)),在高并发下每秒打几百次,JSON 序列化生成大量临时 String 和 Map 对象,全堆里堆着。上线前 grep 一遍 debug 日志,或统一用 if (log.isDebugEnabled()) 包一层。
调参不是玄学,试试这几个组合
如果你用的是 JDK 8 + G1,别再硬套网上流传的“万能参数”。根据实际压测反馈调整:
- 老年代占用持续超 45%,加
-XX:G1HeapWastePercent=5让它更早触发 Mixed GC; - 发现 GC 后存活对象猛增,检查是不是有隐形强引用(比如静态 Map 缓存了 Request 对象),而不是急着调
-XX:MaxGCPauseMillis; - 年轻代太小(
-Xmn设得太低)会导致频繁 YGC,但太大又拖慢复制过程,建议设为堆总大小的 1/4~1/3,观察jstat输出的 YGC 时间和次数是否平衡。
优化不是一次到位的事。每天抽 10 分钟看一眼 jstat 输出,比背十套 JVM 参数更有用。