OutOfMemoryError 异常

在 Java 虚拟机规范的描述中,除了程序计数器外,虚拟机的其他几个运行时区域都有发生 OutOfMemoryError 异常的可能。

前言

一本好书,每读一遍都会有不同的感受。写读书笔记,一来是便于平时查阅,毕竟技术书籍都比较厚,不方便随时携带;二来是督促自己多读书,理论与实践结合才能不断提升自己。

【深入理解Java虚拟机】阅读笔记: 2.4 OutOfMemoryError 异常

Java 堆溢出

Java 堆用于存储对象实例,只要不断地创建对象,并且保证 GC Roots 到对象之间有可达路径,避免垃圾回收机制清除这些对象,那么在达到最大堆的容量限制后就会产生内存溢出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {

static class OOMObject {
}

public static void main(String[] args) {
// 使用 List 持有对象,避免被回收
List<OOMObject> list = new ArrayList<OOMObject>();

while (true) {
list.add(new OOMObject());
}
}
}

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

Java 虚拟机规范描述的两种异常:本质上是堆同一件事情的两种描述

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

实验情况:

  1. 使用 -Xss 参数减少栈内存的容量。结果:抛出 StackOverflowError 异常
  2. 定义了大量的本地变量,增大此方法帧中本地变量表的长度。结果:抛出 StackOverflowError 异常

如下为第 1 点测试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* VM Args: -Xss160k
*/
public class JavaVMStackSOF {

private int stackLength = 1;

public void stackLeak() {
stackLength++;
stackLeak();
}

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;
}
}
}

创建线程导致内存溢出异常:通过不断地建立线程产生内存溢出异常,这样产生的内存溢出异常与栈空间是否足够大不存在联系。相反,为每个线程的栈分配的内存越大,越容易产生内存溢出异常

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
/**
* VM Args: -Xss2M
* 注意: 请谨慎运行,可能会导致主机卡死
*/
public class JavaVMStackOOM {

private void dontStop() {
while (true) {
}
}

public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}

public static void main(String[] args) throws Throwable{
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}

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

运行时常量池是方法区的一部分

JDK 1.6 及之前的版本中,常量池分配在永久代,可通过-XX:PermSize 和 -XX:MaxPermSize 直接限制其容量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimeConstantPoolOOM {

/**
* JDK 1.6及之前版本 OutOfMemoryError: PermGen space
*/
public static void main(String[] args) {
// 使用 List 保持着常量池引用,避免 Full GC 回收常量池行为
List<String> list = new ArrayList<>();

// 10MB 的 PermSize 在 integer 范围内足够产生 OOM 了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
  1. 在 JDK 1.6 中,intern() 方法会把首次遇到的字符串实例复制到永久代中,返回其引用
  2. 在 JDK 1.7 中,intern() 不会复制实例,只是在常量池中记录首次出现的实例引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class RuntimeConstantPoolOOM {

/**
* JDK 1.6 结果为: false false
* JDK 1.7 结果为: true false
*/
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);

String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}

方法区用于存放 Class 的相关信息,测试的基本思路是运行时产生大量的类去填满方法区,直到溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class JavaMethodAreaOOM {

static class OOMObject {
}

public static void main(final String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.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(o, objects);
}
});
enhancer.create();
}
}
}

本机直接内存溢出

  1. 使用 DirectByteBuffer 分配内存导致抛出内存溢出异常时,实际并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,手动抛出异常
  2. 直接通过反射获取 Unsafe 实例,调用 unsafe.allocateMemory() 可直接向系统进行内存分配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
*/
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);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}

参考

周志明. 深入理解Java虚拟机