文章目录
一、看数据图,寻找降本增效的可能性!
拉 7 天的数据看

Eden 区 + Survivor 区 + Old 区 图有了,看起来 Eden 的 GC 频繁,Old 有足够的空间。

GC 次数频繁,GC 耗时较长。

定位到 7 天内有 1次 Full GC 的位置。

CPU 使用率较低,内存使用率一般。
二、分析情况,寻找可以优化的点…
- CPU 核可以压榨,CPU 使用率只有 10%,可以用来优化运行效率。
- 大部分的对象在 Eden 就消亡,Old Gen 的空间有大量的冗余,并且由于 Eden 过早晋升,导致 Old Gen 内存越来越大,发生 GC,可以增加 Eden 的占比来优化。
- 元空间冗余非常多,可以缩小一部分。
- Survivor 区可以适当缩小比值,给 Eden 区。
- 每分钟大概有 200ms 的 GC 耗时,吞吐率 99.67%,服务是 IO 交互性,需要提升至 99.99%。
我们的初始参数:
JAVA_OPTS="-Xmx5734m -Xms5734m -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -Xmn2120m -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintGCDetails"
三、参数优化确定了!小步快走看效果。
- 7.81 GB 的 Pod 内存,JVM 堆可以设置到 7 GB,线上非堆内存 280MB 我们预留 0.81GB(828MB)是足够的,所以我们可以先确定参数
-Xmx7168m。 - 活跃对象,看 Old Gen Full GC 后,剩余大概 785 MB,我们预留 3 倍(应对突发流量浮动垃圾)活跃对象空间给 Old Gen,也就是 2355 MB,剩余的给 Young Gen,确定参数
-Xmn4813m。 - 我们看生产的 Survivor 区平均占用 22.5 MB,根据 Eden 最大值 与 Survivor 最大值比值为 8(End:S0:S1 = 8:1:1),与默认比值一致,我们发现对象 99% 存活时间 < 1次 Minor GC,我们可以先缩小 S 区为 16:1:1,这样 S 区会有 267 MB 足够让对象经过 15 次 MinorGC 的过滤再到 Old,但是考虑到小步快走,先设置为
-XX:SurvivorRatio=12,后续观察。 - 由于每次 Minor GC 的量变大,预计每次收集 2~3 G,所以收集也得加快,CPU 为 4C 规格,我们使用 CPU 总核数的线程来加速 GC(毕竟 STW,一般最好与CPU核心数量相等),并发收集参数
-XX:ParallelGCThreads=4,后续看情况再调整。 - Meta 使用 156 MB,服务是没有动态代理业务,我们设置 256 MB 即可,
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m。剩余的非堆内存也足够 Code Cache、OS 使用了。 - 为了更好地后续调优,加入参数
-XX:+PrintTenuringDistribution监控晋升年龄分布。
计划的参数:
JAVA_OPTS="-Xmx7168m -Xms7168m -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -Xmn4813m -XX:SurvivorRatio=12 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintTenuringDistribution"
四、压测验证,GC 次数减少 50%,接口吞吐提升 22.5%
注意 JVM 参数修改不要和发版一起,在平时时间叫运维改动即可。我们可以用测试环境配置与 Prod 相同的 K8s 配置、JVM 参数。
找资源消耗量大的核心接口进行压测,如何确定我们要压测的核心接口?我 dump 发现不出大对象接口,所以准备通过一些数据多的接口来压测,最终选用了“数据同步”接口。
压测参数:
Number of Threads (users): 1000: 设置 1000 个并发用户,保证系统短时间内大量内存爆涨Ramp-up period (seconds): 5:5秒内启动所有用户,给被压测系统预热的时间Loop Count: 10:重复 10 轮
优化前:


| 指标 | 值 | 问题等级 | 分析 |
|---|---|---|---|
| 平均响应时间 Average | 37,880 ms | ⚠️ 致命 | 平均响应时间37.8秒(用户等待超时) |
| 最差响应时间 Max | 483,104 ms | ⚠️ 致命 | 最差响应时间483秒(≈8分钟) 部分请求完全阻塞 |
| 吞吐量 Std. Dev. | 23,705.65 | ⚠️ 高危 | 响应时间波动剧烈(±23.7秒),系统极不稳定 |
| 错误率 Error % | 0.03% | ⚠️ 中危 | 开始出现错误(约3个失败请求),可能是超时或资源耗尽 |
优化后:


| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 平均响应时间 | 37,880 ms | 36,920 ms | ↓ 2.5% |
| 最差响应时间 | 483,104 ms | 149,293 ms | ↓ 69% |
| 吞吐量 | 18.6/sec | 22.8/sec | ↑ 22.5% |
| 错误率 | 0.03% | 0.00% | 归零 ✅ |
五、关键结论
- Pod 可用内存可以压榨,预留空间给 OS 操作即可,其他给 Heap(
-Xmx与-Xms) + Meta(-XX:MetaspaceSize=与-XX:MaxMetaspaceSize=),避免堆外内存溢出,要保证 Heap > NoHeap(Meta + Code Cache + …) + OS Ops。 - IO 交互型应用,例如互联网服务接口,大多数对象都是存活极短时间,在使用了分代收集的情况下(例如 CMS),Old 的大小应当为活跃对象(Full GC 后存活的)的 2~3 倍左右,考虑到浮动垃圾问题最好在 3 倍左右,剩下的都可以分给 Young 区,这样可以减少过早晋升带来的 GC 频繁问题。
- Young 区中,E 区和 S 区的比值(
-XX:SurvivorRatio=)一般都是需要小步快走的,如果是 IO 交互型应用,S 区可以适当缩小,但是不宜过小而导致 S 区不足晋升 Old 区。 - 每次 GC 的量变大的情况下,加大 GC 线程数,
-XX:ParallelGCThreads=,一般最好与CPU核心数量相等。 - 具体问题具体分析,JVM 调优是实战慢慢调整,慢慢经验积累的。
六、参考资料
《2020 美团技术年货-后台篇》
《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》
《剑指 JVM:虚拟机实践与性能调优》


被折叠的 条评论
为什么被折叠?



