Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。
- 本节内容的目的有两个
- 第一,通过代码
验证Java虚拟机规范中描述的各个运行时区域存储的内容
。 - 第二,希望读者在工作中遇到实际的内存溢出异常时,
能根据异常的信息快速判断是哪个区域的内存溢出
,知道什么样的代码可能会导致这些区域内存溢出,以及出现这些异常后该如何处理。
- 第一,通过代码
Java堆溢出
- Java堆用于存储对象实例,只要
不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象
,那么在对象数量到达最大堆得容量限制后就会产生内存溢出异常。 Java堆内存溢出异常测试
:
/**
* 记得启动的时候加上VM参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject {
}
@Test
public void test() {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
- 运行结果:
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at test.HeapOOM.test(HeapOOM.java:21)
...
- Java堆内存的OOM异常是实际应用中常见的内存溢出异常情况。当出现Java堆内存溢出时,异常堆栈信息
java.lang.OutOfMemoryError
会跟着进一步提示Java heap space
。
虚拟机栈和本地方法栈溢出
- 关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出
StackOverflowError
异常。 - 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出
OutOfMemoryError
异常。
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出
StackOverflowError 异常
虚拟机栈和本地方法栈OOM测试
:
/**
* 记得启动的时候加上VM参数:-Xss128k
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
@Test
public void demo() {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
- 运行结果:
stack length:938
java.lang.StackOverflowError
at test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:14)
at test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15)
at test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15)
...
- 实验结果表明:在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是 StackOverflowError 异常。
OutOfMemoryError 异常
创建线程导致内存溢出异常
:一直创建方法变量线程,直到栈内存不足。
/**
* 记得启动的时候加上VM参数:-Xss2M
*/
public class JavaVMStackOOM {
private void doNotStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(() -> {
doNotStop();
});
thread.start();
}
}
@Test
public void demo() {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
- 运行结果:
Exception in thread "demo" java.lang.OutOfMemoryError: unable to create new native thread
方法区和运行时常量池溢出
- 这里
String.intern()
是一个Native方法,它的作用是,如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
/**
* 记得启动的时候加上VM参数:-XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimeConstantPoolOOM {
@Test
public void demo() {
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
- JDK1.6之前,该段程序会抛出OOM异常,在 OutOfMemoryError 后面跟随的提示信息是
PermGen space
,说明运行时常量池属于方法区的一部分。 - JDK1.7之后,运行这段程序就不会得到相同的结果,while循环将一直进行下去。
笔记来源:《深入理解Java虚拟机》第二章 2.4 实战:OutOfMemoryError异常(P50)。