Java堆内存优化方法:不重启也能扛住高并发

你有没有遇到过这样的情况:线上服务跑着跑着就变慢,监控里老是看到 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 参数更有用。