在 JVM 中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有可能发生 OutOfMemoryError 异常。本博客通过若干实例来验证 OOM 发生的场景。写本博客的目的,希望在以后的工作中遇到 OOM 时,能根据异常的信息快速判断出是哪个区域的内存溢出,知道是什么代码引起的,以及该如何处理。本博客代码部分摘自《深入理解Java虚拟机 第二版》
Java堆用来存储对象,因此只要不断创建对象,并保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清楚这些对象,那么当对象数量达到最大堆容量时就会产生 OOM。
限制堆大小为 20M,不可扩展,通过参数 -XX:+HeapDumpOnOutOfMemoryError 让虚拟机在内存溢出时 Dump 当前堆内存快照以便做分析。
/**
* java堆内存溢出测试
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject{}
public static void main(String[] args) {
List list = new ArrayList();
while (true) {
list.add(new OOMObject());
}
}
}
运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid7164.hprof …
Heap dump file created [27880921 bytes in 0.193 secs]
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2245)
at java.util.Arrays.copyOf(Arrays.java:2219)
at java.util.ArrayList.grow(ArrayList.java:242)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
at java.util.ArrayList.add(ArrayList.java:440)
at com.jvm.oom.HeapOOM.main(HeapOOM.java:17)
堆内存 OOM 是经常会出现的问题,异常信息会进一步提示 Java heap space
要解决这个问题,一般是通过内存分析工具(如Eclipse Memory Analyzer)对 Dump出来的堆存储快照进行分析,到底是内存泄露( Memory Leak ) 还是内存溢出(Memory Overflow)。这里简单介绍下内存溢出和内存泄漏的区别。
内存泄露:指程序中间动态分配了内存,但在程序结束时没有释放这部分内存,从而造成那部分内存不可用的情况,重启计算机或者JVM可以解决,但也有可能再次发生内存泄露,内存泄露和硬件没有关系,它是由软件设计缺陷引起的。
内存溢出:是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。
在 HotSpot 虚拟机中不区分虚拟机栈和本地方法栈,栈容量只由 -Xss 参数设定。关于虚拟机栈和本地方法栈,在 Java 虚拟机规范中描述了两种异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。
下面测试单线程的情况。
/**
* 虚拟机栈和本地方法栈内存溢出测试,抛出stackoverflow exception
* VM ARGS: -Xss128k 减少栈内存容量
*/
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;
}
}
}
运行结果:
stack length = 11420
Exception in thread “main” java.lang.StackOverflowError
at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
… 后续异常信息略
以上代码在单线程环境下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配时,抛出的都是 StackOverflowError 异常。
如果测试环境是多线程环境,通过不断建立线程的方式可以产生内存溢出异常,代码如下所示。但是这样产生的 OOM 与栈空间是否足够大不存在任何联系,在这种情况下,为每个线程的栈分配的内存足够大,反而越容易产生OOM 异常。这点不难理解,每个线程分配到的栈容量越大,可以建立的线程数就变少,建立多线程时就越容易把剩下的内存耗尽。这点在开发多线程的应用时要特别注意。如果建立过多线程导致内存溢出,在不能减少线程数或更换64位虚拟机的情况下,只能通过减少最大堆和减少栈容量来换取更多的线程。
/**
* JVM 虚拟机栈内存溢出测试, 注意在windows平台运行时可能会导致操作系统假死
* VM Args: -Xss2M -XX:+HeapDumpOnOutOfMemoryError
*/
public class JVMStackOOM {
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) {
JVMStackOOM oom = new JVMStackOOM();
oom.stackLeakByThread();
}
}
方法区用于存放Class的相关信息,对这个区域的测试,基本思路是运行时产生大量的类去填满方法区,直到溢出。使用CGLib实现。方法区溢出也是一种常见的内存溢出异常,在经常生成大量Class的应用中,需要特别注意类的回收情况,这类场景除了使用了CGLib字节码增强和动态语言外,常见的还有JSP文件的应用(JSP第一次运行时要编译为Java类)、基于OSGI的应用等。
/**
* 测试JVM方法区内存溢出
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class MethodAreaOOM {
public static void main(String[] args) {
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.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject{}
}
运行结果:
Exception in thread “main”
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread “main”
DirectMemory 容量可通过 -XX:MaxDirectMemorySize 指定,如不指定,则默认与Java堆最大值一样。测试代码使用了 Unsafe 实例进行内存分配。由 DirectMemory 导致的内存溢出,一个明显的特征是在Heap Dump 文件中不会看见明显的异常,如果发现 OOM 之后 Dump 文件很小,而程序直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。
/**
* 测试本地直接内存溢出
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
* @author Administrator
*/
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);
}
}
}
运行结果:
Exception in thread “main” java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.jvm.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:21)
本博客简单介绍了JVM各个区域内存溢出的原因,接下来我会结合实战,使用 Eclipse Memory Analyzer (MAT) 来对 JVM 的内存泄露和内存溢出进行分析。