Java 虚拟机入门教程之三 JVM 参数调优
大纲
- Java 虚拟机入门教程之一 JVM 内存结构
- Java 虚拟机入门教程之二 JVM 垃圾收集
- Java 虚拟机入门教程之三 JVM 参数调优
- Java 虚拟机入门教程之四 JVM 四种引用
- Java 虚拟机入门教程之五 JVM 性能优化
- Java 虚拟机入门教程之六 JVM 类加载机制
JVM 的参数类型
标配指令
标配指令都以 -
开头,这些参数是不同版本的 HotSpot 虚拟机都支持的。
参数 | 参数说明 | 使用例子 |
---|---|---|
-version | 查看版本号 | java -version |
-help | 查看命令帮助手册 | java -help |
-showversion | 查看版本信息 | java -showversion |
非标准指令
非标准指令都以 -X
开头,这些参数通常跟特定版本的 HotSpot 虚拟机对应,但差别不是特别大。
参数 | 参数说明 | 使用例子 |
---|---|---|
-Xint | 解释执行 | java -Xint -version |
-Xcomp | 第一次使用就编译成本地代码 | java -Xcomp -version |
-Xmixed | 混合模式 | java -Xmixed -version |
不稳定参数
不稳定参数都以 -XX:
开头,这些参数通常跟特定版本的 HotSpot 虚拟机对应。特别注意,在不同版本的 HotSpot 虚拟机中,这一类参数的区别(变化)非常大,详细的文档资料也相对较少。
Boolean 类型参数
- 参数格式:-XX:
+
或者-
某个属性,+
表示开启,-
表示关闭 - 使用例子:
-XX:-PrintGCDetails
,关闭 GC 详情输出的功能
- 参数格式:-XX:
Key-Value 类型参数
- 参数格式:-XX:Key=Value
- 使用例子:
-XX:MetaspaceSize=256m
,设置元空间的大小
提示
-Xss
、-Xms
与-Xmx
还是属于 XX 参数,只是都取了别名而已。-Xss
等价于-XX:ThreadStackSize
,用于设置单个线程的栈空间大小,一般默认为 512k ~ 1024k。-Xms
等价于-XX:InitialHeapSize
,用于设置初始堆空间的大小,默认只会使用到最大物理内存的 1/64。-Xmx
等价于-XX:MaxHeapSize
,用于设置堆空间的最大值,默认只会使用到最大物理内存的 1/4。
查看 JVM 参数
查看所有 JVM 参数的默认值
参数 | 参数说明 | 使用例子 |
---|---|---|
-XX:+PrintFlagsInitial | 打印所有 JVM 参数的默认值 | (1) java -XX:+PrintFlagsInitial -version (2) java -XX:+PrintFlagsInitial Test,其中 Test 是类名 |
执行 java -XX:+PrintFlagsInitial -version
命令后,输出的结果如下:
1 | [Global flags] |
查看所有 JVM 参数的最终值
参数 | 参数说明 | 使用例子 |
---|---|---|
-XX:+PrintFlagsFinal | 打印所有 JVM 参数修改后的最终值 | (1) java -XX:+PrintFlagsFinal -version (2) java -XX:+PrintFlagsFinal -XX:MaxHeapSize=128m Test,其中 Test 是类名 |
执行 java -XX:+PrintFlagsFinal -version
命令后,输出的结果如下:
1 | [Global flags] |
如果输出结果中有 :=
或者 {command line}
,则表示该 JVM 参数的值被修改过了,而 =
或者 {default}
则表示该 JVM 参数的值没有被修改过(即使用的是默认值)。
查看 JVM 的默认初始化参数
参数 | 参数说明 | 使用例子 |
---|---|---|
-XX:+PrintCommandLineFlags | 打印 JVM 默认的简单初始化参数 | java -XX:+PrintCommandLineFlags -version |
执行 java -XX:+PrintCommandLineFlags -version
命令后,输出的结果如下:
1 | -XX:G1ConcRefinementThreads=48 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=522493504 -XX:MaxHeapSize=8359896064 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC |
JVM 常用的调优参数
参数 | 参数说明 | 默认值 |
---|---|---|
-Xms | 初始堆空间的大小 | 默认为物理内存的 1/64,等价于 -XX:initialHeapSize |
-Xmx | 堆空间大小的最大值 | 默认为物理内存的 1/4,等价于 - XX:MaxHeapSize |
-Xss | 单个线程的栈空间大小 | 默认为 512k ~ 1024k,等价于 -XX:ThreadStackSize |
-Xmn | 新生代的大小 | 新生代默认占堆空间的 1/3,而老年代默认占堆空间的 2/3 |
-XX:MetaspaceSize | 元空间的大小 | 默认的元空间大小只有 20 兆 (M) |
-XX:+PrintGCDetails | 输出详细 GC 收集日志信息 |
JVM 参数调优案例: -Xms1024m -Xmx1024m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal -XX:+PrintGCDetails
提示
- 最好将
-Xms
和-Xmx
调整成一致,防止 JVM 频繁执行垃圾收集(GC)。 - 使用
jinfo -flag ThreadStackSize PID
查看 JVM 参数时,会发现-XX:ThreadStackSize = 0
,这是因为该值的大小取决于平台,Linux (64-bit)、OS X (64-bit)、Oracle Solaris (64-bit) 平台上的值为 1024KB,而 Windows 取决于虚拟内存的大小。 - 元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现,不过元空间与永久代之间最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。因此,默认情况下元空间的大小仅受本地内存(物理内存)限制。由于默认的元空间大小只有 20 多兆 (M),为了防止在频繁地实例化对象时,导致元空间出现 OOM 现象,因此可以将元空间的大小设置大一些。
JVM 高级的调优参数
堆内存知识回顾
堆内存结构的划分
JVM 的堆内存从 GC 的角度可以细分为:新生代(包括 Eden 区、SurvivorFrom 区和 SurvivorTo 区)和老年代,如下图所示:
Minor GC 的执行过程
Java 中 GC 分为两种:Minor GC(发生在新生代),Full GC(发生在新生代 + 老年代 + 元空间),其中 Minor GC 的执行过程如下:
第一步:Eden、From Survivor 复制到 To Survivor,且将对象的年龄加一
首先,当 Eden 区满的时候会触发第一次 GC ,将还活着的对象拷贝到 From Survivor 区。当 Eden 区再次触发 GC 的时候,会扫描 Eden 区和 From Survivor 区,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接拷贝到 To Survivor 区域(如果有对象的年龄已经达到了老年的标准,则拷贝到老年代区),同时将这些对象的年龄加一。
第二步:清空 Eden、From Survivor
然后,清空 Eden 和 From Survivor 中的对象,而且在拷贝操作执行完成之后 From Survivor 区和 To Survivor 区会交换角色,谁空谁就是 To Survivor 区。
第三步:From Survivor 和 To Survivor 交换
最后,From Survivor 区和 To Survivor 区交换角色,原 To Survivor 成为下一次 GC 时的 From Survivor 区。部分对象会在 From Survivor 区和 To Survivor 区中复制来复制去,如此交换 15 次(由 JVM 参数 -XX:MaxTenuringThreshold
决定,这个参数值默认是 15)后,最终如果还是存活,就晋升到老年代区中。
-XX:NewRatio
参数 | 参数说明 | 默认值 |
---|---|---|
-XX:NewRatio | 配置新生代和老年代在堆内存中的占比 | 默认为 -XX:NewRatio=2,即新生代占 1,老年代 占 2,即新生代占整个堆内存的 1/3 |
比如设置成 -XX:NewRatio=4
,则表示新生代占 1,老年代占 4,即新生代占整个堆内存的 1/5。也就是说,NewRadio 值就是设置老年代的占比,剩下的 1 是新生代的占比。值得一提的是,如果新生代的内存空间特别小,会造成 JVM 频繁地进行 GC 操作。
-XX:SurvivorRatio
参数 | 参数说明 | 默认值 |
---|---|---|
-XX:SurvivorRatio | 配置新生代中 Eden 和 SurvivorFrom、SurvivorTo 的内存空间比例 | 默认为 -XX:SuriviorRatio=8,即 Eden:SurvivorFrom:SurvivorTo = 8:1:1 |
比如设置成 -XX:SurvivorRatio=4
,则表示 Eden:SurvivorFrom:SurvivorTo = 4:1:1。也就是说,SurvivorRatio 值就是设置 Eden 区的比例占多少,SurvivorFrom 和 SurvivorTo 的比例相同。
-XX:MaxTenuringThreshold
参数 | 参数说明 | 默认值 |
---|---|---|
-XX:MaxTenuringThreshold | 用于设置对象经过多少次垃圾收集后,仍然存活而晋升到老年代 | 默认值是 15,并且设置的值必须在 0 ~ 15 之间 |
SurvivorFrom 区和 SurvivorTo 区交换后,原 SurvivorTo 成为下一次 GC 时的 SurvivorFrom 区。部分对象会在 SurvivorFrom 区和 SurvivorTo 区中复制来复制去,如此交换 15 次(由 JVM 参数 MaxTenuringThreshold
决定,这个参数值默认是 15)后,最终如果还是存活,就存入到老年代区中。-XX:MaxTenuringThreshold
这个阈值可以帮助 JVM 调优,尤其是对那些生命周期较长的对象来说。通过调节这个参数,可以控制对象在新生代和老年代之间的晋升频率,从而影响垃圾收集的性能。更高的值意味着对象更不容易晋升到老年代,而更低的值则可能增加老年代的压力。值得一提的是,如果 MaxTenuringThreshold
的值设置为 0 的话,则年轻对象不会经过 Survivor 区,直接进入老年代。
其他不常用的高级调优参数
JVM 参数调优的最佳实践
如何查看 Java 进程的 JVM 参数
面试题
如何查看一个 Java 应用程序,它的某个 JVM 参数是否开启,或者某个 JVM 参数的值是多少?
方法一,适用于 Java 进程未运行
- 在 Java 应用程序启动之前,添加以下任意一个 JVM 启动参数。当应用程序启动后,可以将相应的 JVM 参数全部打印出来。
参数 | 参数说明 | 使用案例 |
---|---|---|
-XX:+PrintFlagsInitial | 打印所有 JVM 参数的默认值 | java -XX:+PrintFlagsInitial -version |
-XX:+PrintFlagsFinal | 打印所有 JVM 参数修改后的最终值 | java -XX:+PrintFlagsFinal -version |
-XX:+PrintCommandLineFlags | 打印 JVM 默认的简单初始化参数 | java -XX:+PrintCommandLineFlags -version |
- 比如,执行
java -XX:+PrintFlagsFinal -version
命令后,JVM 参数的打印结果如下:
1 | [Global flags] |
方法二,适用于 Java 进程正在运行
- 首先使用
jps
命令查看 Java 应用程序的进程 ID (PID),比如是 30886
1 | jps -l |
- 使用
jinfo
命令查看 Java 进程的某个 JVM 参数是否开启
1 | jinfo -flag PrintGCDetails 30886 |
- 命令行的输出结果如下,其中
+
号表示开启,-
号表示关闭
1 | -XX:-PrintGCDetails |
- 使用
jinfo
命令查看 Java 进程的某个 JVM 参数的值是多少
1 | jinfo -flag MetaspaceSize 30886 |
- 命令行的输出结果如下
1 | -XX:MetaspaceSize=21807104 |
- 使用
jinfo
命令查看 Java 进程的所有 JVM 参数
1 | jinfo -flags 30886 |
- 命令行的输出结果如下
1 | VM Flags: |
jinfo 命令的使用总结
- jinfo 命令的使用方式一:
jinfo -flag 配置项 进程ID
,可查看 Java 应用程序的某个 JVM 参数。比如,jinfo -flag MetaspaceSize 30886
,查看 Java 应用程序的元空间大小。 - jinfo 命令的使用方式二:
jinfo -flags 进程ID
,可查看 Java 应用程序的所有 JVM 参数。比如,jinfo -flags 30886
。
模拟 JVM 发生 OOM 现象
- 首先编写下述的一段代码,创建一个大对象,触发 JVM 执行垃圾收集。
1 | public class GcTest { |
- 然后设置 Java 应用程序启动的 JVM 参数,比如设置初始堆内存大小为 10m,最大堆内存大小为 10m,并输出详细 GC 收集日志信息。
1 | -Xms10m -Xmx10m -XX:+PrintGCDetails |
- 在 Java 应用程序启动后,发现会出现以下的错误,同时还打印出 JVM 执行 GC 垃圾收集时的详细信息。这就是 OOM(Java 堆内存溢出),也就是堆内存空间不足。
1 | [GC (Allocation Failure) [PSYoungGen: 1972K->504K(2560K)] 1972K->740K(9728K), 0.0156109 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] |
- 这因为通过
-Xms10m
和-Xmx10m
只给 JVM 堆内存设置了 10M 的大小,但在 Java 代码中又创建了一个 50M 的数组对象,所以就会出现堆内存不足的现象,而且在 JVM 执行垃圾收集的时候,触发了两种 GC 类型:GC 和 Full GC。
1 | [GC (Allocation Failure) [PSYoungGen: 1972K->504K(2560K)] 1972K->740K(9728K), 0.0156109 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] |
- GC (Allocation Failure) 表示对象的内存空间分配失败,那么就需要触发新生代的垃圾收集,各个参数对应的示意图如下:
1 | [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 648K->630K(7168K)] 648K->630K(9728K), [Metaspace: 3467K->3467K(1056768K)], 0.0058502 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] |
- Full GC 大部分发生在老年代,当老年代的内存空间不足时,就会出现 OOM 异常,各个参数对应的示意图如下:
- 从 JVM 输出的详细 GC 收集日志信息,可以发现如下规律:
Java 内存溢出错误
JVM 经典错误分类
JVM 中常见的两个错误:
栈溢出
:StackoverFlowError堆溢出
:OutofMemoryError
除此之外,详细的错误信息如下:
- java.lang.StackOverflowError
- java.lang.OutOfMemoryError:java heap space
- java.lang.OutOfMemoryError:GC overhead limit exceeeded
- java.lang.OutOfMemoryError:Direct buffer memory
- java.lang.OutOfMemoryError:unable to create new native thread
- java.lang.OutOfMemoryError:Metaspace
特别注意 StackOverflowError 和 OutOfMemoryError 都是属于 Error,而不是属于 Exception:
StackoverFlowError
错误概述
java.lang.StackOverflowError
是栈溢出错误。最简单的一个递归调用,就会造成栈溢出,也就是深度的方法调用。栈的大小一般是 512K,不断地递归调用,直到栈被撑破为止,就会造成栈溢出的错误。
错误重现
1 | /** |
程序执行的结果如下:
1 | Exception in thread "main" java.lang.StackOverflowError |
OutOfMemoryError
Java heap space
错误概述
java.lang.OutOfMemoryError:java heap space
是堆内存不足错误。这是由于 Java 应用在运行期间,创建了大对象或者很多对象,导致 JVM 堆空间没有足够的内存进行存储。
错误重现
为了更快地让代码达到模拟效果,首先需要设置应用的 JVM 启动参数为 -Xms5m -Xmx5m
。重现这个错误的步骤就是不断地反复拼接字符串,即创建多个 String 对象,直到启动 GC 回收堆内存。
1 | /** |
程序执行的结果如下:
1 | Exception in thread "main" java.lang.OutOfMemoryError: Java heap space |
GC overhead limit exceeded
错误概述
java.lang.OutOfMemoryError:GC overhead limit exceeeded
错误是指 GC 操作执行时间过长,这里时间过长的定义是,超过了 98% 的时间用来执行 GC 操作,并且回收了不到 2% 的堆内存(如下图所示)。这一般是连续多次 GC 都只回收了不到 2% 堆内存的极端情况下,才会抛出该错误。假设不抛出 GC overhead limit 错误会造成什么情况呢?那结果就是 GC 回收的这点堆内存很快会被再次填满,迫使 GC 再次执行,这样就形成了恶性循环,CPU 的使用率一直都是 100%,而 GC 却没有任何效果。
错误重现
为了更快地让代码达到模拟效果,首先需要设置应用的 JVM 启动参数为 -Xms5m -Xmx5m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
。重现这个错误的步骤就是不断地向 List 中插入新的 String 对象,直到启动 GC 回收堆内存。
1 | /** |
程序执行的输出结果:
1 | Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded |
Direct buffer memory
错误概述
java.lang.OutOfMemoryError:Direct buffer memory
这个错误一般是由 NIO 引起的,比如使用 Netty 网络框架等。简而言之,堆内存充足,但本地内存(堆外内存)不足的时候,就会出现该错误。编写 NIO 程序的时候,会经常使用 ByteBuffer 来读取或写入数据,这是一种基于通道 (Channel) 与 缓冲区 (Buffer) 的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块堆外内存的引用进行操作。这样能在一些业务场景中显著地提高性能,因为避免了在 Java 堆和 Native 堆中频繁来回复制数据。
ByteBuffer.allocate(capability)
:第一种方式是分配 JVM 堆内存,属于 GC 管辖范畴,由于需要拷贝内存数据,所以执行速度相对较慢ByteBuffer.allocteDirect(capability)
:第二种方式是分配 OS 本地内存(堆外内存),不属于 GC 管辖范畴,由于不需要拷贝内存数据,所以执行速度相对较快
如果不断地分配本地内存,而堆内存又很少使用,那么 JVM 就不需要执行 GC,同时 DirectByteBuffer 对象就不会被回收,这时候虽然堆内存充足,但本地内存可能已经用光(撑爆)了,当再次尝试分配本地内存就会出现 OutOfMemoryError 错误,程序也随之崩溃。在 Java 中,可以使用以下代码获取最大本地内存(直接内存)大小。
1 | import java.lang.management.ManagementFactory; |
错误重现
为了更快地让代码达到模拟效果,首先需要设置应用的 JVM 启动参数为 -Xms5m -Xmx5m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
,主要是使用 -XX:MaxDirectMemorySize=5m
设置能使用的本地内存(堆外内存)大小为 5M。重现这个错误的步骤就是通过 ByteBuffer 直接分配 6M 的本地内存空间。
1 | /** |
程序执行的输出结果:
1 | Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory |
Unable to create new native thread
错误概述
java.lang.OutOfMemoryError:unable to create new native thread
错误表示无法创建更多的新线程,也就是说创建线程的上限达到了。在高并发场景下,经常会出现该错误,准确来说该错误与对应的平台有关。
错误原因
- 应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
- 服务器并不允许应用程序创建这么多线程,Linux 系统默认允许单个进程可以创建的线程数为 1024 个,如果超过这个数量就会报错上述错误
解决方法
- 想办法降低应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,更改代码将线程数降到最低
- 对于部分应用,确实需要创建很多线程,且远超过 Linux 系统默认 1024 个线程的限制,可以通过修改 Linux 服务器配置,扩大 Linux 默认的线程数限制
错误重现
1 | /** |
程序执行的输出结果:
1 | Exception in thread "main" java.lang.OutOfMemoryError: unable to ceratenew native thread |
更改 Linux 系统最大线程数的限制
Metaspace
Metaspace 错误概述
java.lang.OutOfMemoryError:Metaspace
是元空间内存不足错误。Matespace 元空间使用的是本地内存,-XX:MetaspaceSize
指定的元空间初始化大小是 20M。元空间就是方法区,存放的是类信息、静态变量、常量池等内容。对于 Hotspot 虚拟机而言,为了与 Java 堆区分开来,方法区还有一个别名 Non-Heap (非堆)。值得一提的是,在 JDK 7 及以前,方法区的名称叫 “永久代”,在 JDK 8 及以后,方法区的名称叫 “元空间”。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。永久代、元空间二者并不只是名字变了,内部结构也调整了,详细介绍可以看 这里。
JDK 1.7 及以前的方法区内存不足错误
在 JDK 1.7 及以前,方法区又叫永久代,因此它的内存不足错误是 java.lang.OutOfMemoryError: PermGen space
。
Metaspace 错误重现
为了更快地让代码达到模拟效果,首先需要设置应用的 JVM 启动参数为 -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m -XX:+PrintGCDetails
。重现这个错误的步骤就是不断往元空间创建类,直到类占据的空间超过 Metaspace 指定的空间大小为止。
1 | public class MetaspaceOutOfMemoryDemo { |
1 | 创建多少次类后发生异常: 201 |