一提到 JVM 参数调优,脑海中第一眼浮现就是面试八股文,但在日常的开发中,有时间一个不起眼的参数总能给你带来意想不到的效果。
正所谓技多不压身,今天让我们一起来揭开 JVM 调优的神秘面纱。
一、参数配置
1. 配置方式
在虚拟机中 -X 和 -D 都可用于指定 JVM 参数,但它们在使用方式和作用范围上有一些区别。
(1) -X 方式
-X 用于指定非标准的 JVM 参数,这些参数通常是供具体的 JVM 实现或特定的虚拟机选项使用的。
例如 -X<parameter>,其中 <parameter> 是具体的 JVM 参数名称。
(2) -D 方式
-D 用于设置系统属性,提供了一种在应用程序中传递配置信息的机制,可以在应用程序中通过 System.getProperty() 方法获取。
例如 -D<property>=<value>,其中 <property> 是属性名称,<value> 是属性值。
(3) 基本格式
JVM 配置参数通常由 -XX: 开头,基本格式如下:
- 若用于配置是否启用,由
+ -分别控制开启与关闭。 - 若用于配置大小或数值变量,遵循
<option>=<value>格式。
| 配置 | 作用 |
|---|---|
| -XX:+option | 表示开启 option 选项。 |
| -XX:-option | 表示关闭 option 选项。 |
| -XX:option=value | 表示将 option 值赋为 value。 |
2. 优先级
在 Java 命令行运行 JAR 包时,JVM 参数可以在 -jar 选项之前或之后指定,这两种方式会影响参数的解析和执行顺序。
- 参数在
-jar之前,则会在JVM启动前解析和应用。- 参数在
-jar之后,则会在解析JAR包时被应用。- 若
JVM参数与JAR包的命令行参数存在冲突,JVM参数的优先级更高。
如下示例中方式一的 -Xmx512m 优先级高于方式二中的 -Xmx1024m。
# 方式一
java -Xmx512m -jar myapp.jar
# 方式二
java -jar myapp.jar -Xmx1024m
二、常用配置
1. 内存参数
JVM 中内存常见配置参数如下,其中 -Xss 在相同物理内存下,减小这个值能生成更多的线程,但是操作系统对一个进程内的线程数还是有限制并不能无限生成,经验值在 3000~5000 左右。
对于 -Xmn 用于调整年轻代内存空间,对于当下使用对象广泛的 Appel 式回收而言对系统性能影响较大,官方推荐配置为整个堆的 3/8。
| 参数 | 作用 |
|---|---|
| -Xms1G | 堆内存初始值。 |
| -Xmx2G | 堆内存最大值。 |
| -Xss512k | 每个线程的堆栈大小,默认 1MB。 |
| -Xmn1024m | 设置年轻代大小,增大年轻代后,将会减小年老代大小。 |
同时在默认的 JVM 服务启动中,如果没有手动指定堆内存值,则默认的 -Xms 与 -Xmx 取值方式如下:
- 最小值(-Xms): 默认为物理内存大小的
1/64,但不超过1GB。 - 最大值(-Xmx): 默认为物理内存大小的
1/4,但不超过32GB。
2. JVM参数
针对于 JVM 的内存堆 (Heap) 以及垃圾回收,常见的配置参数如下:
| 参数 | 作用 |
|---|---|
| -XX:ConcGCThreads=4 | CMS 垃圾回收器并行线程线,推荐值为 CPU 核心数。 |
| -XX:ParallelGCThreads=8 | 新生代并行收集器的线程数。 |
在基于标记复制的 Appel 式回收方式中较为关键的 JVM 参数为下述三类:
| 参数 | 描述 |
|---|---|
| -XX:SurvivorRatio=8 | 设置新生代中 Eden 与 Survivor 的比例,默认为 8:2。 |
| -XX:MaxTenuringThreshold=15 | 设置 To Space 最大复制次数,取值 0~15,默认为 15。 |
| -XX:PretenureSizeThreshold=1m | 设置忽略 MaxTenuringThreshold 直接晋升到老年代的对象大小。 |
3. 系统参数
常见的系统参数配置参数如下:
# 防止虚拟机阻塞
-Djava.security.egd=file:/dev/./urandom
4. GC参数
常见的虚拟机垃圾回收(GC)参数配置参数如下:
开启日志后生成的 GC 文件推荐 gceasy 在线解析网站,可以生成详细的分析报告。
| 参数 | 作用 |
|---|---|
| -XX:+PrintGCDateStamps | 打印 gc 发生的时间戳。 |
| -XX:+PrintTenuringDistribution | 打印 gc 发生时的分代信息。 |
| -XX:+PrintGCApplicationStoppedTime | 打印 gc 停顿时长。 |
| -XX:+PrintGCApplicationConcurrentTime | 打印 gc 间隔的服务运行时长。 |
| -XX:+PrintGCDetails | 打印 gc 详情,包括 gc 前/内存等。 |
| -Xloggc:./gclogs/gc.log | 指定 gc log 的路径,支持相对路径与绝对路径。 |
三、服务模式
1. 启动模式
自 Java 9 开始 JDK 默认使用的垃圾收集器取决于所选择的 JVM 启动模式,有两种启动模式可供选择:
服务器模式不同
JDK版本默认垃圾收集器如下:
Server模式,这是JDK默认的启动模式,适用于长时间运行的服务器应用程序。Java 8及之前的版本,默认使用Parallel(基于标记复制) 垃圾收集器。Java 9及之后默认使用的垃圾收集器是G1垃圾收集器。
客户端模式不同
JDK版本默认垃圾收集器如下:
Client模式,这是面向桌面应用程序和短期运行的应用程序的启动模式。Java 8及之前的版本,默认使用Serial(基于标记清除)垃圾收集器。Java 9及之后的版本,默认使用Serial/Serial Old垃圾收集器组合。
2. 模式查看
那如何查看当前 JVM 的启动方式呢?方法其实很简单直接通过 java -version 命令即可。
可以看到上图的最后一行中有 64-Bit Server VM 字样,即代表当前 JVM 默认的启动模式为服务器模式,简单的讲即当前通过 java -jar 命令运行的程序其 JVM 模式为服务器模式。
而针对正在运行中的程序,则可以通过 jcmd 命令进行查看,命令格式如下:
jcmd <pid> VM.version
执行后可以看到返回的信息和上述 java -version 结果类似,同样为 Server VM。
如果你不想以默认的模式启动,则可以在执行运行命令时通过 -client 或 -server 指定模式。
# 以客户端模式启动
java -client -jar test.jar
# 以服务器模式启动
java -server -jar test.jar
3. 应用选择
我们知道 JVM 有着一系列垃圾回收器例如 CMS、G1 与 ZGC 等,那在实际中 JVM 又是如何选择呢?
其实很简单,JVM 使用的垃圾回收器跟 JDK 版本息息相关,对应的版本参照下表:
| JDK 版本 | 默认回收器 | 可选回收器 |
|---|---|---|
| JDK 8 | Parallel GC | Serial GC、CMS |
| JDK 9 | G1 GC | Parallel GC、CMS |
| JDK 11 | G1 GC | Serial GC、Parallel GC |
| JDK 17 | G1 GC | ZGC、Shenandoah GC |
可以看到在 JDK 8 之后 G1 GC 成为了默认 JVM 垃圾回收器,同时从 JDK 11 开始 CMS 已被标记为弃用。而 ZGC 在 JDK 11 中首次引入,经过多个版本的迭代,在 JDK 17 中已经发展完善拥有客观的性能。
如果想要更换默认生效的垃圾回收器,可以通过 -XX:+Use 命令进行指定。
例如在 JDK 17 中 JVM 默认生效的垃圾回收器为 G1 GC,那么在启动程序时,可通过下述命令将默认生效的垃圾回收器替换为 ZGC。
# 使用 ZGC
java -XX:+UseZGC -jar app.jar
四、JDK工具
安装 JDK 环境之后,在其 bin 目录下提供了丰富工具可用于查看程序运行状况,下面介绍几类常用的命令。
1. 版本
在不同的 JDK 版本中使用方式略有差异,尤其在 JDK 11 之后工具进行了重新整合,后面介绍的几种方式都是基于 JDK 8 版本而言。
例如 jinfo 命令在 JDK 8 之前执行方式如下:
jinfo <options> <pid>
而在 JDK 11 之后的版本,需要通过下述方式执行,可以看到需通过 jhsdb 执行,同时通过 --pid 参数用于指定进程号。
jhsdb jinfo <option> --pid <pid>
同时 <option> 的取值也略有不同,需要将 - 替换为 --,例如下述示例中 JDK 11 之后需要将 -flags 替换为 --flags。
# JDK 8
jinfo -flags 12345
# JDK 11 以上
jhsdb jinfo --flags --pid 12345
2. Jinfo
jinfo 是 JDK中的一个命令行工具,用于实时查看和调整 JVM 的配置信息。
通过 jinfo 可以获取和修改正在运行的 Java 进程的虚拟机配置参数,对于调优和诊断 Java 应用程序是很有帮助的。
# 查看进程的虚拟机参数信息
jinfo <options> <pid>
其中 options 可选参数参考下表:
| 方法 | 作用 |
|---|---|
| -flag | 显示所有可用的虚拟机参数及其当前值。 |
| -flag (name) | 显示虚拟机指定参数的值。 |
| -flag [+|-] (name) | 启用或禁用指定的标志。 |
| -sysprops | 显示 Java 系统属性的值。 |
例如下图中即显示进程 22788 进程所有虚拟机参数。
同时可以通过下述方式动态调整进程的执行虚拟机参数。
# 开启配置
jinfo -flag +<name> <pid>
# 关闭配置
jinfo -flag -<name> <pid>
3. JMap
jmap 是 JDK 中的一个命令行工具,用于生成 Java 进程的内存转储快照。
这个快照通常称为 heap dump,它是 Java 堆内存的详细信息,包括对象的数量、类型、分布等,对于分析内存泄漏和性能问题非常有用。
jmap [options] <pid>
其中 options 可选参数参考下表:
| 参数 | 作用 |
|---|---|
| -heap | 打印堆内存的概要信息,包括堆的使用情况和配置。 |
| -histo | 打印堆内存中对象的直方图,显示每个类的实例数量和占用的内存大小。 |
| -F | 当进程无响应时,强制执行。 |
| -dump | 生成堆内存信息文件。 |
通过 -heap 得到堆内存信息如下图所示,其中参数参考下表:
| 参数 | 描述 |
|---|---|
| MinHeapFreeRatio | 设置堆中最小空闲空间的比例。 |
| MaxHeapFreeRatio | 设置堆中最大空闲空间的比例。 |
| MaxHeapSize | JVM 堆的最大大小,即堆内存的最大限制。 |
| NewSize | 年轻代的初始大小。 |
| MaxNewSize | 年轻代的最大值。 |
| OldSize | 老年代的初始大小。 |
| NewRatio | 年轻代与老年代的大小比例,默认 1:2。 |
| SurvivorRatio | Eden 与 Survivor 的比例,默认 8:2。 |

针对 -dump 生成文件,这里推送一个在线文件分析网站:heaphero。
jmap -dump:file=<export_path> <pid>
# 生成堆栈信息日志
jmap -dump:format=b,file=./dump.bin 1234
4. Jstack
jstack 是 JDK中的一个命令行工具,用于生成 Java 进程的线程转储(thread dump)。
线程转储是一个描述 Java 虚拟机中所有线程当前状态的快照,包括线程的堆栈信息。注意这里的 pid 需要为十六进制形式,在 Linux 中可通过 printf '%x\n' <pid> 命令将对应 pid 转为十六进制。
# 查询进程堆栈信息
jstack [options] <pid>
# <line num>: 限制显示行数
# <pid_16>: 进程转十六进制
jstack <pid> | grep -A <line num> <pid_16_hex>
其中 options 可选参数参考下表:
| 参数 | 作用 |
|---|---|
| -l | 除了线程堆栈外,还显示关于锁的附加信息,这将显示每个锁的拥有者和等待者。 |
| -e | 打印线程的锁信息。 |
| -F | 当进程无响应时,强制生成线程转储,在进程卡死或无响应时非常有用。 |
| -m | 打印 Java 和本地 C/C++ 帧的混合堆栈。 |
5. Jstat
jstat 是 JDK 中的一个命令行工具,用于监视 Java 虚拟机 (JVM) 的各种统计信息。
它提供了对堆内存、垃圾回收、类装载、JIT 编译等方面的实时性能数据,帮助开发人员和系统管理员诊断和分析 Java 应用程序的性能问题。
# <pid>: jps process id
# <interval>: time interval, ms
jstat -gc <pid> <interval>
# Simple info
jstat -gcutil <pid> <interval>

| 列 | 描述 |
|---|---|
| S0C | 年轻代中第一个 Survivor 的容量 (KB)。 |
| S1C | 年轻代中第二个 Survivor 的容量 (KB)。 |
| S0U | 年轻代中第一个 Survivor 目前已使用空间 (KB)。 |
| S1U | 年轻代中第二个 Survivor 目前已使用空间 (KB)。 |
| EC | 年轻代中 Eden 的容量 (KB)。 |
| EU | 年轻代中 Eden 目前已使用空间 (KB)。 |
| OC | 老年代的容量 (KB)。 |
| OU | 老年代目前已使用空间 (KB)。 |
| MC | 元数据区的容量 (KB)。 |
| MU | 元数据区目前已使用空间 (KB)。 |
其中 GC 垃圾回收相关列描述如下:
| 列 | 描述 |
|---|---|
| YGC | 从应用程序启动到采样时年轻代中 GC 次数。 |
| YGCT | 从应用程序启动到采样时年轻代中 GC 所用时间(秒)。 |
| FGC | 从应用程序启动到采样时老年代 Full GC 次数。 |
| FGCT | 从应用程序启动到采样时老年代 Full GC 所用时间(秒)。 |
| GCT | 从应用程序启动到采样时 GC 用的总时间(秒)。 |

| 列 | 描述 |
|---|---|
| S0 | 年轻代中第一个 Survivor 已使用的占当前容量百分比。 |
| S1 | 年轻代中第二个 Survivor 已使用的占当前容量百分比。 |
| S0CMX | 年轻代中第一个 Survivor 的最大容量 (KB)。 |
| S1CMX | 年轻代中第二个 Survivor 的最大容量 (KB)。 |
| E | 年轻代中 Eden 已使用的占当前容量百分比。 |
| ECMX | 年轻代中 Eden 的最大容量 (KB)。 |
| OGCMN | 老年代中初始化(最小)的大小 (KB)。 |
| OGCMX | 老年代的最大容量 (KB)。 |
| OGC | 老年代当前新生成的容量 (KB)。 |
| O | 老年代已使用的占当前容量百分比。 |
| NGCMN | 年轻代中初始化(最小)的大小 (KB)。 |
| NGCMX | 年轻代的最大容量 (KB)。 |
| NGC | 年轻代中当前的容量 (KB)。 |
| DSS | 当前需要 Survivor 的容量 (KB)( Eden 区已满)。 |
| TT | 持有次数限制。 |
| MTT | 最大持有次数限制。 |
参考文档