OOM 异常
在 Java 虚拟机规范中,除了程序计数器没有 OOM 之外,其他内存区域都有 OOM 的可能性
Java 堆溢出
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test_heap_oom() {
try {
ArrayList<Object> list = new ArrayList<>();
while (true) {
list.add(new Object());
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
1
2
3
4
5
6
7
8
9
OpenJDK 64-Bit Server VM warning: MaxNewSize (16384k) is equal to or greater than the entire heap (16384k). A new max generation size of 15872k will be used.
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 com.song.androidstudy.testoom.LoopMethodTest.test_heap_oom(LoopMethodTest.java:37)
- 若是内存泄露,根据 dump 内存信息,检查 GC roots引用链。
- 若是内存溢出,检查大内存的生命周期,减少对内存的消耗
单元测试中,堆大小设置为 -Xms16m -Xmn16m -Xmx16m
虚拟机栈溢出
java 虚拟机规范栈中两种异常情况
- 如果线程请求的栈深度大于虚拟机所允许的栈最大栈深度,将抛出 StackOverflowError 异常
- 如果虚拟机在扩展栈时无法申请到足够内存空间,则抛出 OutOfMemoryError 异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int count = 0;
private void baseMethod() {
count++;
baseMethod();
}
@Test
public void test_loop() {
try {
baseMethod();
} catch (Throwable throwable) {
System.out.println("count = " + count);
throwable.printStackTrace();
}
}
通过无限递归,消耗栈内存大小
当前两种描述有重叠地方,都是对内存不足的两种描述。实验结果为单线程下无论栈帧太大还是虚拟机容量太小,当内存无法分配时候,虚拟机抛出都是 StackOverflowError 异常。当不断创建线程方式可以产生内存溢出常量,单个线程栈分配的栈内存越大越容易产生内存溢出。遇到这种情况,可以采用 64 位虚拟机或者扩展栈内存大小,或者减小单个线程的栈容量,来获取更多线程。
方法区溢出
类信息,字段描述,访问空字符,常量池,方法描述等保存在方法区,若动态不断创建类,则会 OOM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static class OOMObject {
}
@Test
public void test_method_area() {
while (true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invoke(obj, args);
}
});
enhancer.create();
}
}
运行时常量池溢出
通过 String 的 intern 函数可以将字符串复制到常量池中
- jdk1.6 中,intern 函数复制字符串到常量池(永久代)中,然后返回常量池中对象引用
- jdk1.7 以上,intern 函数对首次出现字符串实例,只是在常量池中记录实例的引用(因为常量池从永久代移除,只需要在常量池中保持引用即可),并未复制到常量池,返回是堆中引用。非首次出现这返回常量池中引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void test_intern() {
String test1 = new StringBuffer("haha").append("哈哈").toString();
System.out.println(test1==test1.intern());
String test3 = new StringBuffer("haha").append("哈哈").toString();
System.out.println(test3==test3.intern());
String test2 = new StringBuffer("ja").append("va").toString();
System.out.println(test2==test2.intern());
}
// jdk1.7 输出结果
true // 因为常量池中不存在 "haha哈哈" 字符串,故复制到字符串常量池的,且返回常量池中对象引用,和堆上引用不相等
false // 不符合首次出现原则,常量池中存在
false // "java" 字符串已经存在常量池中,intern()直接返回常量池中字符串对象的引用
// jdk1.6 输出结果
false // 因为常量池中不存在 "haha哈哈" 字符串,故复制到字符串常量池的,且返回常量池中对象引用,和堆上引用不相等
false // 常量池中已经存在
false // "java" 字符串已经存在常量池中,intern()直接返回常量池中字符串对象的引用
由于 java jdk1.7 版本中的字符串常量池从永久代中移除,故无法演示基于常量池的 OOM
直接内存溢出
略