深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第 3 版)

#自动内存管理

#Java 内存区域与内存溢出异常

Java虚拟机运行时数据区

Java 虚拟机运行时数据区

#虚拟机栈和本地方法栈溢出

由于 HotSpot 虚拟机中并不区分虚拟机栈和本地方法栈,因此对于 HotSpot 来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由-Xss参数来设定。

关于虚拟机栈和本地方法栈,在《Java 虚拟机规范》中描述了两种异常:

  1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  2. 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。

Java 虚拟机规范》明确允许 Java 虚拟机实现自行选择是否支持栈的动态扩展,而 HotSpot 虚拟机的选择是不支持扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。

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
package xyz.onns.jvm;

/**
* VM Args: -Xss128k
*
* @author zzm
*/
public class JavaVMStackSOF {
private int stackLength = 1;

public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}

public void stackLeak() {
stackLength++;
stackLeak();
}
}
1
2
3
4
5
6
7
"D:\Program Files\java\jdk-13.0.1\bin\java.exe" -Xss128k "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2020.2\lib\idea_rt.jar=57372:D:\Program Files\JetBrains\IntelliJ IDEA 2020.2\bin" -Dfile.encoding=UTF-8 -classpath D:\weiyun\Code\java\jvm\out\production\jvm xyz.onns.jvm.JavaVMStackSOF
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

The Java thread stack size specified is too small. Specify at least 180k

Process finished with exit code 1

修改参数为-Xss180k

1
2
3
4
5
6
7
"D:\Program Files\java\jdk-13.0.1\bin\java.exe" -Xss180k "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2020.2\lib\idea_rt.jar=57575:D:\Program Files\JetBrains\IntelliJ IDEA 2020.2\bin" -Dfile.encoding=UTF-8 -classpath D:\weiyun\Code\java\jvm\out\production\jvm xyz.onns.jvm.JavaVMStackSOF
stack length:1854
Exception in thread "main" java.lang.StackOverflowError
at xyz.onns.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:23)
...

Process finished with exit code 1
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
31
32
33
34
35
36
37
38
39
40
41
42
43
package xyz.onns.jvm;

/**
* @author zzm
*/
public class JavaVMStackSOF2 {
private static int stackLength = 0;

public static void test() {
long unused1, unused2, unused3, unused4, unused5,
unused6, unused7, unused8, unused9, unused10,
unused11, unused12, unused13, unused14, unused15,
unused16, unused17, unused18, unused19, unused20,
unused21, unused22, unused23, unused24, unused25,
unused26, unused27, unused28, unused29, unused30,
unused31, unused32, unused33, unused34, unused35,
unused36, unused37, unused38, unused39, unused40,
unused41, unused42, unused43, unused44, unused45,
unused46, unused47, unused48, unused49, unused50,
unused51, unused52, unused53, unused54, unused55,
unused56, unused57, unused58, unused59, unused60,
unused61, unused62, unused63, unused64, unused65,
unused66, unused67, unused68, unused69, unused70,
unused71, unused72, unused73, unused74, unused75,
unused76, unused77, unused78, unused79, unused80,
unused81, unused82, unused83, unused84, unused85,
unused86, unused87, unused88, unused89, unused90,
unused91, unused92, unused93, unused94, unused95,
unused96, unused97, unused98, unused99, unused100;
stackLength++;
test();
unused1 = unused2 = unused3 = unused4 = unused5 = unused6 = unused7 = unused8 = unused9 = unused10 = unused11 = unused12 = unused13 = unused14 = unused15 = unused16 = unused17 = unused18 = unused19 = unused20 = unused21 = unused22 = unused23 = unused24 = unused25 = unused26 = unused27 = unused28 = unused29 = unused30 = unused31 = unused32 = unused33 = unused34 = unused35 = unused36 = unused37 = unused38 = unused39 = unused40 = unused41 = unused42 = unused43 = unused44 = unused45 = unused46 = unused47 = unused48 = unused49 = unused50 = unused51 = unused52 = unused53 = unused54 = unused55 = unused56 = unused57 = unused58 = unused59 = unused60 = unused61 = unused62 = unused63 = unused64 = unused65 = unused66 = unused67 = unused68 = unused69 = unused70 = unused71 = unused72 = unused73 = unused74 = unused75 = unused76 = unused77 = unused78 = unused79 = unused80 = unused81 = unused82 = unused83 = unused84 = unused85 = unused86 = unused87 = unused88 = unused89 = unused90 = unused91 = unused92 = unused93 = unused94 = unused95 = unused96 = unused97 = unused98 = unused99 = unused100 = 0;
}

public static void main(String[] args) {
try {
test();
} catch (Error e) {
System.out.println("stack length:" + stackLength);
throw e;
}
}
}
1
2
3
4
5
6
7
"D:\Program Files\java\jdk-13.0.1\bin\java.exe" -Xss180k "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2020.2\lib\idea_rt.jar=58065:D:\Program Files\JetBrains\IntelliJ IDEA 2020.2\bin" -Dfile.encoding=UTF-8 -classpath D:\weiyun\Code\java\jvm\out\production\jvm xyz.onns.jvm.JavaVMStackSOF2
stack length:87
Exception in thread "main" java.lang.StackOverflowError
at xyz.onns.jvm.JavaVMStackSOF2.test(JavaVMStackSOF2.java:30)
...

Process finished with exit code 1

无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,
HotSpot 虚拟机抛出的都是StackOverflowError异常。

#方法区和运行时常量池溢出

String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象的引用;否则,会将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。

在 JDK 6 或更早之前的 HotSpot 虚拟机中,常量池都是分配在永久代中,我们可以通过-XX:PermSize-XX:MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量。

自 JDK 7 起,原本存放在永久代的字符串常量池被移至 Java 堆之中。

为了让使用者有预防实际应用里出现类似于“迫使虚拟机产生方法区的溢出异常”的破坏性的操作,HotSpot 还是提供了一些参数作为元空间的防御措施,主要包括:

  • -XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
  • -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
  • -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。

#本机直接内存溢出

直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与 Java 堆最大值(由-Xmx指定)一致。

由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果读者发现内存溢出之后产生的 Dump 文件很小,而程序中又直接或间接使用了 DirectMemory(典型的间接使用就是 NIO),那就可以考虑重点检查一下直接内存方面的原因了。

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
package xyz.onns.jvm;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
*
* @author zzm
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;

public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
int i = 1;
while (true) {
System.out.println(i++);
unsafe.allocateMemory(_1MB);
}
}
}

很奇怪,在 IDEA 运行这段代码的时候我不会有异常抛出…我的 Java 版本是:

1
2
3
4
$ java --version
java 13.0.2 2020-01-14
Java(TM) SE Runtime Environment (build 13.0.2+8)
Java HotSpot(TM) 64-Bit Server VM (build 13.0.2+8, mixed mode, sharing)