JVM OutOfMemoryError异常

《深入理解Java虚拟机》


  在Java虚拟机中,除了程序计数器外,其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。关于虚拟机启动参数,如果使用控制台命令执行程序,直接跟在Java命令之后书写就行;如果使用Eclipse IDE,可在如下图配置:右键工程–Properties–选择Run/Debug Settings–新建New–Ok
  
Eclipse配置VM参数

  • JVM 堆溢出
    Java 堆用于存储对象的实例,只要不断的创建对象,当对象数量达到最大堆容量限制后就会产生内存溢出异常。
    设置Java堆大小为10M,不可扩展(将堆的最小值-Xms和最大值-Xmx相等即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError在出现内存溢出异常时Dump出当前内存堆转储快照以便事后分析。
    代码1 Java 堆内存溢出异常测试
/**
 * VM Args: -verbose:gc -Xms10M -Xmx10M -XX:+PrintGCDetails -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_pid4756.hprof ...
Heap dump file created [13038231 bytes in 0.098 secs]
  • Java栈和本地方法栈溢出
      在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此,对于HotSpot来说,-Xoss(设置本地方法栈大小)虽存在但无效栈容量只有-Xss参数设定
    对于Java栈和本地方法栈,Java虚拟机规范中描述了两种异常:
      - 若线程请求的栈深度大于虚拟机所允许的最大深度,抛出StackOverflowError异常
      - 若虚拟机在扩展时无法申请足够的内存空间,抛出OutOfMemoryError异常
     (1)使用-Xss参数减少栈内存的容量,抛出StackOverflowError,异常出现时输出栈深度相应缩小
     (2)定义大量的本地变量,增大此方法帧中本地变量表的长度,抛出StackOverflowError,异常出现时输出栈深度相应缩小
     
    代码2 Java 栈溢出异常测试
/**
 * 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:995
Exception in thread "main" java.lang.StackOverflowError
    at com.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9)
    at com.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
    ... ...

  在单个线程下,无论是由于栈帧太大还是虚拟机栈容量大小,但内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
在多线程下,通过不断的建立线程的方式可以产生内存溢出OutOfMemoryError异常

代码3 Java 栈 多线程内存溢出异常测试

/**
 * VM Args: -Xss2M
 */
public class JavaVMStackSOF {
    public 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) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        oom.stackLeakByThread();
    }
}

运行上述代码有较大的风险,可能会导致操作系统的假死
运行结果:

Exception in thread "main" java.lang.OutOfMemoryError:unable to create new native thread
  • 方法区溢出
      可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小
    方法区用于存放Class的相关信息,类名、访问修饰符、常量池、字段描述、方法描述等,基本思路是产生大量的类去填满方法区,指到溢出。借助CGLib直接操作字节码生成大量的动态类。
    代码4 借助CGLib使方法区内存溢出
/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class JavaMethodAreaOOM {
    static class OOMObject{
    }
    public static void main(String[] args){
        ErrHandler handler = new ErrHandler();

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

运行结果:

java.lang.OutOfMemoryError: PermGen space
Dumping heap to java_pid2824.hprof ...
Heap dump file created [3934632 bytes in 0.032 secs]
... ...
Exception: java.lang.OutOfMemoryError thrown from the 
UncaughtExceptionHandlerin thread "main"
  • 本机直接内存溢出
      DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定默认与Java堆最大值(-Xmx)一样,下列代码越过了DirectByteBuffer类,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是只有rt.jar中的类才能使用Unsafe的功能)。
      虽然使用DirectByteBuffer分配内存也会抛出异常,但它抛出异常时没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory().
    代码5 使用 unsafe 分配本机内存
/**
 * sun.misc.Unsafe;
 * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M 
 */
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.test.DirectMemoryOOM.main(DirectMemoryOOM.java:17)

由DirectMemory导致的内存溢出,如果发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,可以考虑检查是不是这方面的原因。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章