深入理解JVM之OOM

2017-08-03  本文已影响0人  田真的架构人生

做Java程序开发,难免会遇到OutOfMemory,导致的原因也是不尽相同,下面我们来捋一捋OOM出现的场景。
一,堆空间不足,这是最容易,也是最常见的OOM。了解Java内存结构的同学应该知道,Java里面创建的对象大部分都是位于堆空间的,当创建的对象太多,而堆空间不足时,很容易抛出OOM,如下:

import java.util.ArrayList;
import java.util.List;

/*VM args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 * 
 * -Xms:堆的最小值;-Xmx:堆的最大值
 * 
 * */
public class HeapOOM {

    static class OOMObject {
    }

    public static void main(String[] args) {
        List list = new ArrayList();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

运行结果:

Exception in thread \"main\" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:2760)
    at java.util.Arrays.copyOf(Arrays.java:2734)
    at java.util.ArrayList.ensureCapacity(ArrayList.java:167)
    at java.util.ArrayList.add(ArrayList.java:351)
    at test.java.VM.OOM.HeapOOM.main(HeapOOM.java:19)

二,直接分配内存溢出,Java提供了一些可以直接操作内存和线程的低层次操作(native)-sun.misc.Unsafe,其被JDK广泛用于自己的包中,如java.nio和java.util.concurrent。
但是丝毫不建议在生产环境中使用这个Unsafe,从名字就可以看出,这个API十分不安全、不轻便、而且不稳定。

import java.lang.reflect.Field;

import sun.misc.Unsafe;

/*VM args:-Xmx10m -XX:MaxDirectMemorySize=5m
 * 
 * */
public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws Exception {
        Field unsafeField = Unsafe.class.getDeclaredField(\"theUnsafe\");
        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 test.java.VM.OOM.DirectMemoryOOM.main(DirectMemoryOOM.java:19)

三,方法区溢出,方法区主要存放类的信息、静态变量、Field、Method信息等,当不停地有类动态创建并加载时,方法区也能产生OOM。

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/*VM args: -XX:PermSize=5m -XX:MaxPermSize=5m
 * 
 * */
public class JavaMethodAreaOOM {
    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 method.invoke(obj, args);
                    return proxy.invokeSuper(obj, args);
                }
            });
            OOMObject object = (OOMObject) enhancer.create();
        }
    }

    static class OOMObject {
    }
}

运行结果:

Exception in thread \"main\" java.lang.OutOfMemoryError: PermGen space

四,常量池溢出,常量池属于方法区,存放一些常量值(如static final),还有一些文本形式出现的符号引用,如:类和接口的全限定名、字段的名称和描述符。

import java.util.ArrayList;
import java.util.List;

/*VM rags:-XX:PermSize=5m -XX:MaxPermSize=5m
 * 
 * 
 * */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        //使用List保持着常量池引用,避免Full GC回收常量池行为
        List list = new ArrayList();

        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());//将String对象加入常量池
        }
    }
}

运行结果:

Exception in thread \"main\" java.lang.OutOfMemoryError: PermGen space
    at java.lang.String.intern(Native Method)
    at test.java.VM.OOM.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:17)
Exception in thread \"Reference Handler\" java.lang.OutOfMemoryError: PermGen space
    at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:123)

五,Stack溢出,JVM方法栈为线程私有,当方法执行时会被创建,当方法执行完毕,其对应的栈帧所占用的内存也会被自动释放。
当方法栈的深度不足时,会抛出StackOverflowError,不过只要不出现无穷递归,栈的深度不会太大。

/*VM args:-Xss128k
 * 
 * 在单线程下,无论是由于栈帧太大,还是虚拟机容量太小,
 * 当内存无法分配的时候,虚拟机抛出的都是StackOverflowError
 * 如果测试不限于单线程,通过不断创建线程的方式倒是可以产生内存溢出异常(详见JavaVMStackOF2)
 * */
public class JavaVMStackOF {
    private int stackLength = 1;

    public void stackLeak() {
        stackLength++;//其实,即使没有操作数,也会throw StackOverflowError
        stackLeak();
    }

    public static void main(String[] args) throws Throwable {
        JavaVMStackOF oom = new JavaVMStackOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println(\"stack length:\" + oom.stackLength);
            throw e;
        }
    }
}

运行结果:

stack length:1007Exception in thread \"main\" java.lang.StackOverflowError

    at test.java.VM.OOM.JavaVMStackOF.stackLeak(JavaVMStackOF.java:13)
    at test.java.VM.OOM.JavaVMStackOF.stackLeak(JavaVMStackOF.java:14)

补充:

/*VM args:-Xss10m
 * 
 * 通过不断创建新线程达到OutOfMemoryError
 * 物理内存-Xmx(最大堆容量)-MaxPermSize(最大方法区容量)=虚拟机栈+本地方法栈,程序计数器消耗内存较小,可以忽略。
 * -Xss10m,分配给每个线程的内存。所以-Xss越大,越容易出现OutOfMemoryError(可创建的线程越少)。
 * 
 * 栈深度在虚拟机默认情况下,达到1000~2000完全没问题,对于正常的方法调用(包括递归),这个深度完全够用了。
 * 但是,如果是建立过多线程导致 的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆容量和减少栈容量来换取更多的线程。
 * 如果没有这方面的经验,这种通过“减少内存”的手段来解决内存溢出的方式会比较难以想到!
 * */
上一篇下一篇

猜你喜欢

热点阅读