上一篇讲了 JVM 的内存区域全景,这篇进入最实用的部分——怎么配参数、怎么用命令排查问题。所有内容摘自当年的实战笔记。
一、参数配置速查
1.1 堆内存
| 参数 | 作用 | 建议 |
|---|---|---|
-Xms(-XX:InitialHeapSize) | 设置 Java 程序启动时初始堆大小 | 一般直接把初始大小设置等同于最大堆大小,减少 GC 次数提高性能 |
-Xmx(-XX:MaxHeapSize) | 设置可获得的最大堆大小 | 视物理内存和应用类型定,常见 4G / 8G |
-Xmn | 设置新生代大小 | 一般新生代大小为整个堆大小的 1/3 或 1/4 |
-XX:SurvivorRatio=8 | 设置新生代中 Eden / Survivor 的空间比例 | 默认为 8(即 Eden:Survivor = 8:1) |
-XX:NewSize / -XX:MaxNewSize | 设置老年代 / 新生代的比值 | 一般设置 3 或者 2 |
-XX:NewRatio | old 区与 young 比 | 默认为 2(即 2:1) |
冷知识:当年
-Xms和-Xmx设成一样大是有讲究的——避免堆在运行时动态扩容,JVM 不必在运行时反复申请内存。这条经验到 2017 年依然适用。
1.2 栈与方法区
| 参数 | 作用 | 备注 |
|---|---|---|
-Xss(-XX:ThreadStackSize) | 每个线程堆栈大小 | 减小能生成更多线程;增大能压入更多栈帧(更深方法调用)。默认为 1024K,建议修改为 512K |
-XX:MetaspaceSize / -XX:MaxMetaspaceSize | 配置 Metaspace 扩容时触发 FullGC 的阈值 | Java 8 新增,替代 PermGen |
-XX:PermSize=64m / -XX:MaxPermSize=64m | 配置方法区大小 | Java 7 及之前;系统生产大量的类时需要配置加大 |
-XX:+UseCompressedClassPointers | 使用压缩类指针 | 64 位 JVM 默认开启 |
-XX:CompressedClassSpaceSize | 压缩类空间大小设置 | 1G 左右 |
Java 8 的最大变化:PermGen 被 Metaspace 取代,Metaspace 使用本地内存(Native Memory)而不是 JVM 堆。这意味着再大的 Metaspace 都不会立刻抛 OOM,但会挤占系统内存,需要关注
-XX:MaxMetaspaceSize防止意外。
1.3 JIT 编译
| 参数 | 作用 |
|---|---|
-XX:CompileThreshold | 代码执行多少次才触发 JIT 编译,使用 client 模式默认为 1500,使用 server 模式默认为 10000 |
-XX:InitialCodeCacheSize | CodeCache 初始大小 |
-XX:ReservedCodeCacheSize | 最大 CodeCache 大小 |
CodeCache 是存放 JIT 编译后机器码的内存区域。设得太小,会出现「CodeCache 满了」的 Warning,触发 JIT 关闭退化为解释执行——性能断崖式下跌。
1.4 GC 与对象晋升
| 参数 | 作用 |
|---|---|
-XX:PretenureSizeThreshold | 大对象直接进入 old 区的阈值,默认为 0(不开启) |
-XX:MaxTenuringThreshold | 经历多少次 GC 从 young 进入 old 区,默认为 15 |
-XX:TargetSurvivorRatio | 动态调节从 young 进入 old |
-XX:-UseTLAB | 不使用 TLAB(默认使用) |
-XX:PrintTenuringDistribution | 发生 young GC 时打印一下存活的对象的年龄分布情况 |
1.5 OOM 应急
| 参数 | 作用 |
|---|---|
-XX:+HeapDumpOnOutOfMemoryError | 发生 OOM 后导出堆内存镜像 |
-XX:HeapDumpPath=./ | 发生 OOM 后导出堆内存镜像的文件保存位置 |
强烈建议线上服务加上这一对。OOM 一旦发生,进程会立即退出——如果没有 heap dump,你连「为什么 OOM」都查不出来。dump 文件用 MAT 打开即可分析。
二、常用命令
2.1 jps(JVM Process Status)
jps -l # 查看启动的 java 进程和 main 的包名类名
jps -v # 查看启动的 java 进程及其启动参数
最常用的命令之一。jps 等价于 Linux 的 ps,但只显示 Java 进程。
2.2 jinfo(运行时 JVM 参数)
jinfo -flag MaxHeapSize <pid> # 查看 pid 进程的最大堆内存
jinfo -flag ThreadStackSize <pid> # 查看 pid 的线程堆栈大小
jinfo -flag UseG1GC <pid> # 是否使用 G1 GC
jinfo -flags <pid> # 查看 pid 下被手动赋过值的参数(可看出当前使用的什么垃圾收集器)
实战技巧:
jinfo -flags只显示显式赋过值的参数。如果想看所有参数(包括默认值),用jcmd <pid> VM.flags。
2.3 jstat(GC / 类加载 / JIT 统计)
jstat -class <pid> [间隔毫秒] [总次数] # 查看类加载信息
jstat -gc <pid> [间隔毫秒] [总次数] # 查看 GC 信息(参数详细说明如下)
jstat -gcutil <pid> # 查看各个区域的使用率
jstat -compiler <pid> [间隔毫秒] [总次数] # 查看编译信息
类加载信息(jstat -class):
| 字段 | 含义 |
|---|---|
| Loaded | 加载类的个数 |
| Bytes | 加载总大小(K) |
| Unloaded | 卸载类的个数 |
| Time | 花费在加载和卸载类上的时间 |
GC 信息(jstat -gc):
| 字段 | 含义 |
|---|---|
| S0C、S1C | S0 和 S1 的总容量(kB) |
| S0U、S1U | S0 和 S1 的使用量 |
| EC、EU | Eden 区总容量与使用量 |
| OC、OU | Old 区总容量与使用量 |
| MC、MU | Metaspace 区总容量与使用量 |
| CCSC、CCSU | 压缩类空间总容量与使用量 |
| YGC、YGCT | YoungGC 次数与时间 |
| FGC、FGCT | FullGC 次数与时间 |
| GCT | 总 GC 时间 |
生产环境最常用的命令是
jstat -gcutil -h10 <pid> 1000:每秒打一行 GC 摘要,连续观察内存增长趋势。
2.4 jmap(堆内存)
jmap -dump:format=b,file=./xxx.hprof <pid> # 导出进程号为 pid 的内存映像文件
jmap -histo[:live] <pid> # 显示堆中对象的统计信息(类、对象数量、内存大小)
jmap -heap <pid> # 打印堆摘要信息(GC 算法、堆配置信息和各区域内存使用情况)
jmap -clstats <pid> # 打印类加载器信息
实战技巧:
jmap -histo:live会触发一次 Full GC——线上慎用。需要堆 dump 推荐用-XX:+HeapDumpOnOutOfMemoryError让 JVM 自动出 dump,而不是线上手动 jmap。
2.5 jstack(线程栈 / CPU 飙高 / 死锁)
top -Hp <pid> # Linux 下,查看进程中的所有线程
printf '%x\n' <tid> # 将 10 进制的线程号转为 16 进制
jstack <pid> > xxx.txt # 将当前进程中的线程信息导入文件中
CPU 飙高问题定位(4 步走):
top查看 CPU 使用率高的进程top -Hp <pid>查看该进程中的所有线程,找到 CPU 高的线程 ID(10 进制)printf '%x\n' <tid>把线程号转为 16 进制(jstack 导出的线程 ID 是 16 进制)jstack <pid> | grep -A 20 <16 进制 tid>找到该线程在做什么
死锁问题:
jstack <pid> > xxx.txt导出线程栈- 文件最末端会显示死锁信息——
Found one Java-level deadlock关键字定位 - 顺藤摸瓜看两个线程相互等待的锁
2017 年冷知识:当年 jstack 导出文件经常出现「pid 在 jstack 和 top 中不一致」的情况——这是因为 Linux 线程 ID 是操作系统层面的,而 JVM 内部用的是另一个 ID。要用 16 进制转换对齐。
三、javac 与 javap
javac -g:vars xxx.java # 编译时生成局部变量表数据,然后在 javap 时才能看到
javac -classpath <path> *.java # 指定编译的路径,然后可接多个 java 文件一起编译
实战技巧:遇到线上只有 class 文件没有源码的情况,用
javap -c -p ClassName反编译字节码。-p显示 private 成员,-c显示字节码指令。
四、参数配置实战模板
2017 年某生产服务的典型配置(仅供参考,实际值要按应用类型定):
java \
-Xms4g -Xmx4g \ # 堆大小固定,避免运行时扩容
-Xmn1g \ # 新生代 1G,占堆的 1/4
-XX:SurvivorRatio=8 \ # Eden:Survivor = 8:1
-XX:MetaspaceSize=256m \ # 元空间初始 256M
-XX:MaxMetaspaceSize=512m \ # 元空间最大 512M
-XX:+UseG1GC \ # 使用 G1 垃圾收集器
-XX:MaxGCPauseMillis=200 \ # 期望最大 GC 停顿 200ms
-XX:+HeapDumpOnOutOfMemoryError \ # OOM 自动 dump
-XX:HeapDumpPath=/var/log/oom/ \ # dump 路径
-Xloggc:/var/log/gc.log \ # GC 日志
-XX:+PrintGCDetails \ # 详细 GC 日志
-XX:+PrintGCDateStamps \ # GC 日志带时间戳
-jar app.jar
G1 的 MaxGCPauseMillis 是当年 G1 的核心调优点——G1 会基于这个目标动态调整年轻代大小,尝试满足停顿目标。设得过小(如 50ms)会导致 G1 退化、吞吐量下降。
五、小结
参数配置是 JVM 调优的「静态」部分,命令是「动态」部分。两者结合:
- 上线前:用参数把堆、方法区、JIT、GC 都定下来
- 运行中:用 jstat / jmap / jstack 持续观察
- 出事后:第一时间
jstack抓线程栈、jmap抓堆 dump
下一篇「一文拿下 JVM(下):逃逸分析与代码优化」讲JIT 阶段最重要的优化——为什么有些对象不会进入堆、有些同步被悄悄消除。