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 详情输出的功能
  • 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
2
3
4
5
6
7
8
9
10
11
12
[Global flags]
int ActiveProcessorCount = -1 {product} {default}
uintx AdaptiveSizeDecrementScaleFactor = 4 {product} {default}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product} {default}
uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product} {default}
uintx AdaptiveSizePolicyInitializingSteps = 20 {product} {default}
uintx AdaptiveSizePolicyOutputInterval = 0 {product} {default}
uintx AdaptiveSizePolicyWeight = 10 {product} {default}
uintx AdaptiveSizeThroughPutPolicy = 0 {product} {default}
uintx AdaptiveTimeWeight = 25 {product} {default}
bool AggressiveHeap = false {product} {default}
......

查看所有 JVM 参数的最终值

参数参数说明使用例子
-XX:+PrintFlagsFinal 打印所有 JVM 参数修改后的最终值 (1) java -XX:+PrintFlagsFinal -version
(2) java -XX:+PrintFlagsFinal -XX:MaxHeapSize=128m Test,其中 Test 是类名

执行 java -XX:+PrintFlagsFinal -version 命令后,输出的结果如下:

1
2
3
4
5
6
7
8
9
10
[Global flags]
int ActiveProcessorCount = -1 {product} {default}
uintx AdaptiveSizeDecrementScaleFactor = 4 {product} {default}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product} {default}
uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product} {default}
uintx AdaptiveSizePolicyInitializingSteps = 20 {product} {default}
uintx AdaptiveSizePolicyOutputInterval = 0 {product} {default}
uintx AdaptiveSizePolicyWeight = 10 {product} {default}
uintx AdaptiveSizeThroughPutPolicy = 0 {product} {default}
......

如果输出结果中有 := 或者 {command line},则表示该 JVM 参数的值被修改过了,而 = 或者 {default} 则表示该 JVM 参数的值没有被修改过(即使用的是默认值)。

查看 JVM 的默认初始化参数

参数参数说明使用例子
-XX:+PrintCommandLineFlags 打印 JVM 默认的简单初始化参数 java -XX:+PrintCommandLineFlags -version

执行 java -XX:+PrintCommandLineFlags -version 命令后,输出的结果如下:

1
2
3
4
-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 
java version "11.0.9" 2020-10-20 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.9+7-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.9+7-LTS, mixed mode)

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
2
3
4
5
6
7
8
9
10
[Global flags]
int ActiveProcessorCount = -1 {product} {default}
uintx AdaptiveSizeDecrementScaleFactor = 4 {product} {default}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product} {default}
uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product} {default}
uintx AdaptiveSizePolicyInitializingSteps = 20 {product} {default}
uintx AdaptiveSizePolicyOutputInterval = 0 {product} {default}
uintx AdaptiveSizePolicyWeight = 10 {product} {default}
uintx AdaptiveSizeThroughPutPolicy = 0 {product} {default}
......

方法二,适用于 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
2
VM Flags:
-XX:CICompilerCount=18 -XX:ConcGCThreads=12 -XX:ErrorFile=/root/java_error_in_idea_%p.log -XX:G1ConcRefinementThreads=48 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:HeapDumpPath=/root/java_error_in_idea_.hprof -XX:InitialHeapSize=524288000 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=2576351232 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=8769992 -XX:NonProfiledCodeHeapSize=121444124 -XX:ProfiledCodeHeapSize=121444124 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC

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
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GcTest {

public static void main(String[] args) {
// 创建一个占用大内存空间的字节数组 (50M 大小)
byte[] byteArray = new byte[50 * 1024 * 1024];

try {
TimeUnit.SECONDS.sleep(60);
} catch (Exception e) {
e.printStackTrace();
}
}

}
  • 然后设置 Java 应用程序启动的 JVM 参数,比如设置初始堆内存大小为 10m,最大堆内存大小为 10m,并输出详细 GC 收集日志信息。
1
-Xms10m -Xmx10m -XX:+PrintGCDetails
  • 在 Java 应用程序启动后,发现会出现以下的错误,同时还打印出 JVM 执行 GC 垃圾收集时的详细信息。这就是 OOM(Java 堆内存溢出),也就是堆内存空间不足。
1
2
3
4
5
6
7
[GC (Allocation Failure) [PSYoungGen: 1972K->504K(2560K)] 1972K->740K(9728K), 0.0156109 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]
......
[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]
......

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.java.interview.gc.HelloGC.main(HelloGC.java:9)
  • 这因为通过 -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
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 栈溢出
*/
public class StackOverflowErrorDemo {

public static void main(String[] args) {
stackOverFlowError();
}

public static void stackOverFlowError() {
stackOverFlowError();
}

}

程序执行的结果如下:

1
2
Exception in thread "main" java.lang.StackOverflowError
at com.java.interview.oom.StackOverflowErrorDemo.stackOverFlowError(StackOverflowErrorDemo.java:10)

OutOfMemoryError

Java heap space

错误概述

java.lang.OutOfMemoryError:java heap space 是堆内存不足错误。这是由于 Java 应用在运行期间,创建了大对象或者很多对象,导致 JVM 堆空间没有足够的内存进行存储。

错误重现

为了更快地让代码达到模拟效果,首先需要设置应用的 JVM 启动参数为 -Xms5m -Xmx5m。重现这个错误的步骤就是不断地反复拼接字符串,即创建多个 String 对象,直到启动 GC 回收堆内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 堆内存不足
*/
public class JavaHeapSpaceDemo {

/**
* 设置JVM启动参数:-Xms5m -Xmx5m
*/
public static void main(String[] args) {
String str = "";
while (true) {
str = str + new Random().nextInt(100) + new Random().nextInt(2000);
str.intern();
}
}

}

程序执行的结果如下:

1
2
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/jdk.internal.misc.Unsafe.allocateUninitializedArray(Unsafe.java:1271)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* GC 操作执行时间过长
*/
public class GcOverheadLimitErrorDemo {

/**
* 设置JVM启动参数: -Xms5m -Xmx5m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
*/
public static void main(String[] args) {
int i = 0;
List<String> list = new ArrayList<>();

try {
while (true) {
list.add(String.valueOf(i++).intern());
}
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}

}

程序执行的输出结果:

1
2
3
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3099)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.lang.management.ManagementFactory;

public class MaxDirectMemoryUtil {

/**
* 获取最大直接内存大小(兼容 JDK 8 及以下版本)
*/
public static long getMaxDirectMemory() {
long maxDirectMemory = sun.misc.VM.maxDirectMemory();
System.out.println("Max direct memory (JDK 8-): " + maxDirectMemory + " bytes");
return maxDirectMemory;
}

/**
* 获取最大直接内存大小(兼容 JDK 9 及以上版本)
*/
public static long getMaxDirectMemoryNew() {
// 这个值可能包括了堆内存的部分,所以如果需要获取真正的直接内存大小,建议将其值除以 2
long maxDirectMemoryNew = ManagementFactory.getPlatformMXBean(com.sun.management.OperatingSystemMXBean.class).getTotalPhysicalMemorySize();
System.out.println("Max direct memory (JDK 9+): " + maxDirectMemoryNew / 2 + " bytes");
return maxDirectMemoryNew / 2;
}

public static void main(String[] args) {
long maxDirectMemory = getMaxDirectMemoryNew();
String result = maxDirectMemory / (double) 1024 / 1024 / 1024 + "GB";
System.out.println(result);
}

}
错误重现

为了更快地让代码达到模拟效果,首先需要设置应用的 JVM 启动参数为 -Xms5m -Xmx5m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m,主要是使用 -XX:MaxDirectMemorySize=5m 设置能使用的本地内存(堆外内存)大小为 5M。重现这个错误的步骤就是通过 ByteBuffer 直接分配 6M 的本地内存空间。

1
2
3
4
5
6
7
8
9
10
11
/**
* 本地内存(直接内存)不足
*/
public class DirectBufferMemoryErrorDemo {

public static void main(String[] args) {
// 分配 6M 的本地内存空间
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6 * 1024 * 1024);
}

}

程序执行的输出结果:

1
2
3
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.base/java.nio.Bits.reserveMemory(Bits.java:175)
at java.base/java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:118)

Unable to create new native thread

错误概述

java.lang.OutOfMemoryError:unable to create new native thread 错误表示无法创建更多的新线程,也就是说创建线程的上限达到了。在高并发场景下,经常会出现该错误,准确来说该错误与对应的平台有关。

  • 错误原因

    • 应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
    • 服务器并不允许应用程序创建这么多线程,Linux 系统默认允许单个进程可以创建的线程数为 1024 个,如果超过这个数量就会报错上述错误
  • 解决方法

    • 想办法降低应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,更改代码将线程数降到最低
    • 对于部分应用,确实需要创建很多线程,且远超过 Linux 系统默认 1024 个线程的限制,可以通过修改 Linux 服务器配置,扩大 Linux 默认的线程数限制
错误重现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 无法创建更多的新线程
*/
public class UnableCreateNewThreadDemo {

public static void main(String[] args) {
int index = 0;
while (true) {
index++;
System.out.println("************** index = " + index);
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(index)).start();
}
}

}

程序执行的输出结果:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MetaspaceOutOfMemoryDemo {

// 静态类
static class OOMTest {

}

public static void main(String[] args) {
int i = 0;
try {
while (true) {
i++;
// 使用 Cglib 的动态字节码技术创建新类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMTest.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(0, args);
}
});
}
} catch (Exception e) {
System.out.println("创建多少次类后发生异常: " + i);
e.printStackTrace();
}
}

}
1
2
创建多少次类后发生异常: 201
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace