JVM OOM 分析

JVM 相关知识

Posted by Song on January 1, 2020

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

直接内存溢出