了解OutOfMemoryError异常 - 深入Java虚拟机读后总结

JVM中的异常发生

  Java虚拟机规范中除了程序计数器外,其他几个运行时区域都有发生OutOfMemoryError异常的可能。

  本章笔记通过代码来验证Java虚拟机规范中描述的各个运行时区域存储的内容、以及在以后遇到实际的内存溢出异常时,能根据异常的信息快速判断是哪个区域出现的内存溢出、怎样的代码可能会导致这些区域的内存溢出、以及这些问题该如何处理。

  1. Java堆溢出:Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径避免垃圾回收机制清除对象,就会在对象数量到达最大堆的容量限制后产生内存溢出异常。-Xms设置堆最小值、-Xmx设置堆最大值、-XX:+HeapDumpOnOutOfMemoryError设置当虚拟机出现OOM异常时,dump出当前内存堆转存快照。其中当把堆最小值与最大值设置相同时候,堆为不可扩展。

    /**
     * -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
     */
    public class HeapOOM {
         static class OOMObject {
               Object obj = new Object();
         }
         public static void main(String[] args) {
               List<OOMObject> list = new ArrayList<OOMObject>();
                while( true) {
                    list.add( new OOMObject());
               }
         }
    }

    要解决这个区域的异常,可以通过内存映像分析工具堆dump出来的快照进行分析,重点是确认内存是出现了泄漏(Memory Leak)还是内存溢出(Memory Overflow)。如果是内存泄漏通过工具查看相关引用链。如果是内存溢出,应当检查虚拟机的堆参数,与机器物理内存对比确认是否可以调大,代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况。

  2. 虚拟机栈和本地方法栈溢出:Java虚拟机中关于虚拟机栈和本地方法栈描述了两种异常
    StackOverflowError:线程请求的栈深度大于虚拟机所允许的最大深度时抛出
    OutOfMemoryError:虚拟机在扩展栈时无法申请到足够的内存空间时抛出
    -Xoss参数设置本地方法栈内存容量,-Xss参数设置虚拟机栈内存容量。
    /**
     * -Xss128k
     * 使用-Xss减小栈内存容量.
     * 定义大量本地变量,增加方法帧中本地变量表长度.
     */
    public class JavaVMStackSOF {
         private int stackLength = 1;
         public static void main(String[] args) {
               JavaVMStackSOF javaVMStackSOF = new JavaVMStackSOF();
                try {
                    javaVMStackSOF.test();
               } catch(java.lang.StackOverflowError e) {
                    System. out.println(javaVMStackSOF. stackLength);
               }
         }
         public void test() {
               ++ stackLength;
               test();
         }
    }

    单线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配时,虚拟机抛出的都是StackOverflowError异常。如果通过建立线程的方式可以产生内存溢出异常,但这样产生的异常与栈空间是否足够大并没有任何的关系,因为这种情况下,给线程的栈分配的内存越大,越容易产生内存溢出异常。原因:栈的生命周期与线程相同,每个线程分配到的栈容量越大,可以创建的线程数量就会越少。另外,虚拟机中剩余内存计算方法计算如下:剩余内存 = 操作系统限制进程内存 - 最大堆容量 - 最大方法区容量。程序计数器消耗内存很小,可忽略、若虚拟机进程本身不计算的话,剩下的就是虚拟机栈和本地方法栈分了。因此如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆(我认为应该也可以减少最大方法区)和减少栈容量来换取更多的线程了。

  3. 运行时常量池溢出:运行时常量分配在方法区内,可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制常量池的容量。实验代码在jdk 1.7.0_72上实验失败。
    /**
     * -XX:PermSize=10M -XX:MaxPermSize=10M
     * @author Administrator
     *
     */
    public class RuntimeConstantPoolOOM {
         public static void main(String[] args) {
               List<String> list = new ArrayList<String>();
                int i = 0;
                while( true) {
                    list.add(String. valueOf(i++).intern());
               }
         }
    }

    运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是"PermGen space",说明运行时常量池属于方法区的一部分。

  4. 方法区溢出:方法区溢出是一种常见的内存溢出异常,可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,在经常动态生成大量Class的应用中,需要特别注意类的回收状况。动态生成Class可以使用CGLib字节码增强、动态JSP文件、基于OSGi的应用。下面的代码就是借助CGLib使方法区出现内存溢出异常
    /**
     * -XX:PermSize=10M -XX:MaxPermSize=10M
     */
    public class JavaMethodAreaOOM {
         public static void main(String[] args) {
                while( true) {
                    Enhancer enhancer = new Enhancer();
                    enhancer.setSuperclass(OOMObject. class);
                    enhancer.setUseCache( false);
                    enhancer.create();
               }
         }
         static class OOMObject {
         }
    }
  5. 本机直接内存溢出:直接内存(DirectMemory)容量可以通过-XX:MaxDirectMemorySize指定,如果不指定MaxDirectMemorySize默认值与Java堆的最大值一样。下面的代码使用Unsafe功能申请分配内存。
    /**
     * -Xmx20M -XX:MaxDirectMemorySize=10M
     */
    public class DirectMemoryOOM {
         private static final int _1MB = 1024*1024;
         public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
               Field unsafeField = sun.misc.Unsafe.class.getDeclaredFields()[0];
               unsafeField.setAccessible( true);
               sun.misc.Unsafe unsafe = (sun.misc.Unsafe)unsafeField.get( null);
                while( true) {
                    unsafe.allocateMemory( _1MB);
               }
         }
    }

方法入参建议使用JavaBean传递

  当方法参数很多时候,不便于阅读和维护,因此建议使用一个JavaBean来包装入参。这个建议的另外一方面因素请先考虑下面这个问题

public class MyBean {
    private final double a;
    private final double b;

    public MyBean(double a, double b) {
        this.a = a;
        this.b = b;
    }

    public double getA() {
        return a;
    }

    public double getB() {
        return b;
    }
}
public class StackOverflowTestA {

    private static int invokeMethodCount = 0;

    public static void method(MyBean myBean) {
        ++invokeMethodCount;
        System.out.println(invokeMethodCount);
        //method(new MyBean(myBean.getA(), myBean.getB()));
        method(myBean);
    }

    public static void main(String[] args) {
        try {
            method(new MyBean(1L, 2L));
        } catch(Throwable e) {
            System.out.println("调用次数 : " + invokeMethodCount);
        }
    }

}
public class StackOverflowTestB {

    private static int invokeMethodCount = 0;

    public static void method(long a, long b) {
        ++invokeMethodCount;
        System.out.println(invokeMethodCount);
        method(a, b);
    }

    public static void main(String[] args) {
        try {
            method(1L, 2L);
        } catch(Throwable e) {
            System.out.println("调用次数 : " + invokeMethodCount);
        }
    }
}

  我们都知道Java虚拟机栈的空间有限,当出现无限递归的代码时就会发生StackOverflowError错误。
  那么请思考上面代码中StackOverflowTestA和StackOverflowTestB哪一个递归的深度更深一些?如果StackOverflowTestB中method方法入参变为两个short类型呢?

时间: 2024-10-13 09:46:38

了解OutOfMemoryError异常 - 深入Java虚拟机读后总结的相关文章

Java内存区域 - 深入Java虚拟机读后总结

Java虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,有各自的创建时间和销毁时间,有的区域随着虚拟机进程的启动而存在,有的区域则是依赖用户线程的启动和结束进行建立或销毁.Java虚拟机第二版规定,虚拟机管理的内存包含以下几个运行时数据区域 程序计数器:程序计数器(Program Counter Register)是一块较小的内存空间,作用可以理解为是当前线程所执行的字节码的行号指示器.Java虚拟机的多线程是通过线程切换以及分配处理器执行时间来

JVM【第五回】:【OutOfMemoryError异常之Java堆溢出】

Java堆用于存储对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清楚这些对象,就会在对象数量到达最大堆的容量限制后产生内存溢出异常. 代码清单中限制Java堆的大小为20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便时候进行分析. 在Eclipse中的Run Conf

《Effective Java》------读后总结

<Effective Java>--读后总结 这本书在Java开发的行业里,颇有名气.今天总算是粗略的看完了...后面线程部分和序列化部分由于心浮气躁看的不仔细.这个月还剩下一周,慢慢总结消化. 1.静态工厂方法代替构造器 静态工厂方法有名称,能确切地描述正被返回的对象. 不必每次调用都创建一个新的对象. 可以返回原返回类型的任何子类对象. 创建参数化类型实例时更加简洁,比如调用构造 HashMap 时,使用 Map < String,List < String > m =

《像计算机科学家一样思考Java》—— 读后总结

本书属于入门级的Java书籍,与其他的向编程思想.核心技术不同的是,这本书不是按部就班的讲解java变成知识,而是随着语言的深入慢慢增加知识点. 这本书以一个语言开发者的角度,深入浅出的讲解了Java语言的机制. 比如语言最基本的变量和方法,到后续的深入,功能的增加,逐渐的加大难度与知识点. 本书内容 程序语言 一门编程语言,在学习之前要看它是高级语言.还是低级语言.低级语言更接近计算机底层,但是不容易编写和理解,比如汇编.还要看它是解释型的还是需要编译的.比如html,css都是属于解释型的,

Java虚拟机系列一:一文搞懂 JVM 架构和运行时数据区

前言 之前写博客一直比较随性,主题也很随意,就是想到什么写什么,对什么感兴趣就写什么.虽然写起来无拘无束,自在随意,但也带来了一些问题,每次写完一篇后就要去纠结下一篇到底写什么,看来选择太多也不是好事儿,更重要的是不成体系的内容对读者也不够友好.所以以后的博客尽量按系列来写,不过偶尔也会穿插其他的内容.接下来一段时间我会把写博客的重点放在 JVM (Java Virtual Machine) 和 JUC (java util concurrent ) 上,对 Java 虚拟机和 Java 并发编

Java虚拟机 运行时数据区

Java在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途.创建和销毁的时间,有一些是随虚拟机的启动而创建,随虚拟机的退出而销毁,有些则是与线程一一对应,随线程的开始和结束而创建和销毁. Java虚拟机所管理的内存将会包括以下几个运行时数据区域 程序计数器(Program Counter Register) 它是一块较小的内存空间,它的作用可以看做是当先线程所执行的字节码的信号指示器. 每一条JVM线程都有自己的PC寄存器,各条线程之间互不影响,独立存

(转)《深入理解java虚拟机》学习笔记3——垃圾回收算法

Java虚拟机的内存区域中,程序计数器.虚拟机栈和本地方法栈三个区域是线程私有的,随线程生而生,随线程灭而灭:栈中的栈帧随着方法的进入和退出而进行入栈和出栈操作,每个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这三个区域的内存分配和回收都具有确定性.垃圾回收重点关注的是堆和方法区部分的内存. 常用的垃圾回收算法有: (1).引用计数算法: 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器都为0的对象就是不再被使用的,垃

深入理解Java虚拟机:JVM高级特性与最佳实践(一):java 内存区域与内存异常

如需转载,请标明转自何处 运行时数据区域: java 虚拟机在执行java程序的过程中会把他管理的内存化为若干个不同的数据区域.这些区域都有各自的用途,销毁与创建的时间,有的区域随着进程的启动而存在,有的则依赖用户的线程的启动和结束而创建和销毁,java虚拟机管理的内存分为一下几个数据区域: 1.程序计数器:当前线程所执行的字节码的行号指示器,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支.循环.跳转.异常处理线程恢复都依赖与它. java 虚拟机的多线程是通过线程的轮流切换并分配

深入理解JAVA虚拟机之异常诊断

常见的JAVA虚拟机HotSpot虚拟机运行时数据库由5部分构成:方法区,堆,虚拟机栈,本地方法栈,程序计数器.下面列举各个部分可能出现的异常及其出现原因. 1.方法区存放的已被虚拟机加载的类型信息,常量.静态变量.即时编译器编译后的代码缓存等数据.可能出现的异常有OutOfMemoryError,原因可能是创建了过多的常量(不太可能,因为自JDK7起,原本存放在永久代中的字符串常量池被移至Java堆中,故JDK7前的运行池常量溢出报错由OOM:PerGem space变为了OOM:Java h