跳至正文
来两杯美式
返回

一文拿下 JVM(中):参数配置与常用命令

By 来两杯美式
发布于

上一篇讲了 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:NewRatioold 区与 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:InitialCodeCacheSizeCodeCache 初始大小
-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、S1CS0 和 S1 的总容量(kB)
S0U、S1US0 和 S1 的使用量
EC、EUEden 区总容量与使用量
OC、OUOld 区总容量与使用量
MC、MUMetaspace 区总容量与使用量
CCSC、CCSU压缩类空间总容量与使用量
YGC、YGCTYoungGC 次数与时间
FGC、FGCTFullGC 次数与时间
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 步走):

  1. top 查看 CPU 使用率高的进程
  2. top -Hp <pid> 查看该进程中的所有线程,找到 CPU 高的线程 ID(10 进制)
  3. printf '%x\n' <tid> 把线程号转为 16 进制(jstack 导出的线程 ID 是 16 进制)
  4. jstack <pid> | grep -A 20 <16 进制 tid> 找到该线程在做什么

死锁问题

  1. jstack <pid> > xxx.txt 导出线程栈
  2. 文件最末端会显示死锁信息——Found one Java-level deadlock 关键字定位
  3. 顺藤摸瓜看两个线程相互等待的锁

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 调优的「静态」部分,命令是「动态」部分。两者结合:

下一篇「一文拿下 JVM(下):逃逸分析与代码优化」讲JIT 阶段最重要的优化——为什么有些对象不会进入堆、有些同步被悄悄消除。


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
JVM
第 2 / 6 篇
查看系列全部文章
  1. 01.一文拿下 JVM(上):内存区域全景
  2. 02.一文拿下 JVM(中):参数配置与常用命令
  3. 03.一文拿下 JVM(下):逃逸分析与代码优化
  4. 04.一文拿下 JVM(番外):OOM 排查与 MAT / JVisualVM 实战
  5. 05.一文拿下 JVM(终章):对象创建的 7 个步骤
  6. 06.一文拿下 JVM(锁篇):synchronized 锁的升级与三种锁对比

上一篇
一文拿下 JVM(下):逃逸分析与代码优化
下一篇
一文拿下 JVM(上):内存区域全景