JVM学习笔记(二) 实战OutOfMemoryError异常

来源:互联网 发布:access数据库拆分字段 编辑:程序博客网 时间:2024/05/21 17:32

在 JVM 中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有可能发生 OutOfMemoryError 异常。本博客通过若干实例来验证 OOM 发生的场景。写本博客的目的,希望在以后的工作中遇到 OOM 时,能根据异常的信息快速判断出是哪个区域的内存溢出,知道是什么代码引起的,以及该如何处理。本博客代码部分摘自《深入理解Java虚拟机 第二版》

1. 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<OOMObject> list = new ArrayList<OOMObject>();        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。

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

在 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();    }}

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

方法区用于存放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”

4. 本机直接内存溢出

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)

5. 小结

本博客简单介绍了JVM各个区域内存溢出的原因,接下来我会结合实战,使用 Eclipse Memory Analyzer (MAT) 来对 JVM 的内存泄露和内存溢出进行分析。

0 0
原创粉丝点击