照着这篇走一遍,你能拿到一套可落地的 Java 服务调优流程:从看指标、配参数、选 GC,到用命令行工具把内存泄漏揪出来。
第一步:先量,再调
不要一上来就改参数。先用 JDK 自带工具确认现状:
jps -l # 找到目标进程 PID
jstat -gcutil <pid> 1000 10 # 每秒一次,看 Eden/Old 使用率和 GC 耗时
jinfo -flags <pid> # 打印实际生效的 JVM 参数
jcmd <pid> GC.heap_info # 堆使用概览
重点看三列:YGC/YGCT(新生代次数与总耗时)、FGC/FGCT(Full GC 次数与耗时)、G(老年代占用百分比)。如果 FGC 每分钟都在涨、FGCT 单次超过 1 秒,才值得动手。
注意:jstat -gcutil 里的百分比是「已用/当前容量」,堆没扩到 -Xmx 时这个数会虚高,别只看百分比,要看 jstat -gc 的 OU/OC 绝对值。
第二步:把 GC 日志打开(这是唯一的黑匣子)
JDK 8:
-Xloggc:/data/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps \
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50M
JDK 9 及以上(统一日志框架):
-Xlog:gc*,gc+heap=info:file=/data/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=50M
注意:GC 日志文件别放容器临时目录,Pod 重建就没了;线上建议直接输出到挂载盘或 stdout 交给采集。
第三步:堆参数的常规姿势
-Xms4g -Xmx4g # 设为相同,避免运行期反复扩缩容
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
-Xss512k # 线程栈,线程数多时再下调
-XX:MaxDirectMemorySize=512m # 用 Netty/NIO 时务必显式设置
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump/
-Xms = -Xmx 是最常被忽略的一条:堆动态伸缩本身要停顿,还会让监控曲线失真。
注意:-XX:+HeapDumpOnOutOfMemoryError 在大堆(比如 32G)上会写出同等大小的 hprof 文件并卡住进程,容器磁盘要预留空间,否则 OOM 之后连现场都没了。
第四步:选 GC 并调关键旋钮
- 小堆、吞吐优先(离线批处理):
-XX:+UseParallelGC
- 4G 以上、延迟敏感(Web 服务):JDK 9+ 默认 G1,显式写
-XX:+UseG1GC
- 超大堆、要求停顿 10ms 级:
-XX:+UseZGC(JDK 15+ 生产可用,JDK 21+ 可加 -XX:+ZGenerational)
G1 常用调整:
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=8m \
-XX:InitiatingHeapOccupancyPercent=45 -XX:+ParallelRefProcEnabled
MaxGCPauseMillis 是期望值不是保证值,设得太小(比如 20ms)会让 G1 把新生代压得极小,Young GC 反而更频繁。
注意:切换 GC 前先在预发压测对比,ZGC 会额外占用约 20% 内存和更多 CPU,小机器上换过去可能更慢。
第五步:内存泄漏排查(四步定位)
- 确认泄漏而非压力:
jstat -gc <pid> 1000,若 Full GC 后 OU 持续走高且不回落,基本就是泄漏。
- 看对象画像:
jmap -histo:live <pid> | head -30
实例数异常大的类(比如某个 DTO、byte[]、HashMap$Node)就是线索。
- 抓堆快照:
jcmd <pid> GC.heap_dump /data/dump/heap.hprof
- 用 MAT 或 JProfiler 打开,先看 Leak Suspects 和 Dominator Tree,找「被谁 GC Root 引用」的路径。
高频泄漏源:静态 Map 当缓存无上限、ThreadLocal 用完不 remove()、监听器/回调注册后没注销、连接或流未关闭、把大对象塞进 String.intern()。
注意:jmap -dump:live 会触发一次 Full GC 并 STW,线上执行前先摘流量;生产优先用 jcmd GC.heap_dump,并提前用 -XX:+HeapDumpOnOutOfMemoryError 兜底。
第六步:CPU 飙高时顺手看一眼线程
top -Hp <pid> # 找出高 CPU 的线程 ID
printf '%x\n' <tid> # 转成十六进制
jstack <pid> | grep -A 30 <十六进制tid>
常见结论是死循环、正则回溯、或 GC 线程占满(此时 jstack 里会看到 GC task thread)。
小结
- 顺序不能反:先采集(jstat / GC 日志)→ 再判断 → 最后改参数。
-Xms = -Xmx、MaxMetaspaceSize、HeapDumpOnOutOfMemoryError 是必配三项。
- GC 选择看堆大小和延迟要求,G1 是默认稳妥解,ZGC 适合超大堆。
- 内存泄漏靠
jmap -histo 缩小范围、heap dump + MAT 定引用链,85% 的泄漏都在静态集合和 ThreadLocal 上。
- 任何参数改动都要有压测对比数据,没有数据的调优只是换个运气。