OutOfMemoryError异常以及各区内存溢出

   

    在java虚拟机的规范描述中,除了程序计数器外虚拟机内存的其他几个运行时区域都会发生OutOfMemoryError异常的可能。在Java语言里,可作为GC Roots对象的包括如下几种:

    a.虚拟机栈(栈桢中的本地变量表)中的引用的对象

    b.方法区中的类静态属性引用的对象

    c.方法区中的常量引用的对象

    d.本地方法栈中JNI的引用的对象

java堆溢出

        该区用于存储对象的实例,只要不断的创建对象并且保证GC Roots到对象之间有可达路径来避免垃圾回收清除这些对象,在对象数量达到最大堆的容量限制后就会产生内存溢出,堆中的OOM异常是实际中最常见的内存溢出异常,当出现时不仅会提示OOM的Error还会进一步给出Java heap space。对于这个区域的异常关键在于确认内存中的对象是否是必要的,要分清是内存泄漏还是内存溢出

        1、内存泄漏memory leak:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),或者申请到的那块内存你自己也不能再访问,而系统也不能再次将它分配给需要的程序。就相当于你租了个带钥匙的柜子,你存完东西之后把柜子锁上之后,把钥匙丢了或者没有将钥匙还回去,那么结果就是这个柜子将无法供给任何人使用,也无法被垃圾回收器回收,因为找不到他的任何信息。

扫描二维码关注公众号,回复: 4292509 查看本文章

        2、内存溢出 out of memory:指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。 一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出。比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。

        3、二者的关系:内存泄漏的堆积最终会导致内存溢出,内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。

        4、内存泄漏的分类(按发生方式来分类):

              1.常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。 

             2.偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。 

             3.一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。 

             4.隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。 

    

    什么情况下会发生内存泄露:

     1、静态集合类引起内存泄漏:像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。

    2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。

    3、监听器:在释放对象的时候却没有去删除这些监听器,增加了内存泄漏的机会。

    4、各种连接:比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。

    5、内部类和外部模块的引用:内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如: public void registerMsg(Object b); 这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。

    (1)非静态内部类(成员内部类)

    (2)静态内部类(嵌套内部类)

    (3)局部内部类(定义在方法内或者作用域内的类,好似局部变量,所以不能有访问控制符和static等修饰)

    (4)匿名内部类(没有名字,仅使用一次new个对象即扔掉类的定义)。匿名内部类的类型可以是如下几种方式:接口匿名内部类、抽象类匿名内部类、类匿名内部类

    优先使用静态内部类而不是非静态的,因为非静态内部类持有外部类引用可能导致垃圾回收失败。如果你的静态内部类需要宿主Activity的引用来执行某些东西,你要将这个引用封装在一个WeakReference中,避免意外导致Activity泄露,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收 只被弱引用关联的对象,只被说明这个对象本身已经没有用处了。

    6、单例模式:不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏。

    7、handler引起的内存泄漏

        一些处理上的思路:通过工具查看泄露对象到GC Roots的引用链,找到泄露对象是通过怎样的路径与GC Roots产生关联并导致垃圾收集器无法自动回收,这样就可以确定出泄露代码的位置。如果不是泄露就是内存中的对象还必须存活,那就应该调整虚拟机的堆参数,与机器物理内存对比看是否可以调大,从代码上检查是否存在对象生命期过长、持有状态时间过长,来减少内存的消耗。

第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)

第二步,检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。

第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

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

对于这两块空间java虚拟机规范描述了两种异常:

         1.如果线程请求的栈深度大于虚拟机所允许的最大深度将抛出StackOverflowError异常

        2.如果虚拟机在扩展栈时无法申请到足够的内存空间将抛出OutOfMemoryError异常

        虽然把异常分为两种情况看似更加严谨但是却存在互相重叠的地方,当栈空间无法继续分配的时候究竟是内存太小问题还是栈空间使用过多,在单线程的尝试中均无法让虚拟机产生OutOfMemory异常,获得的都是StackOverflowError异常,再单个线程下无论是因为栈帧过大还是虚拟机栈容量太小,当内存无法分配的时候虚拟机抛出的都是StackOverflowError异常,虚拟机抛出的都是StackOverflowError异常。如果测试的时候不限于单线程,通过不断的建立线程的方式倒是可以产生内存溢出异常,在这种情况下为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。

        原因就是操作系统分配给每个进程的内存是有限制的,虚拟机提供了参数来控制java堆和方法区的这两部分内存的最大值,剩余内存减去最大堆容量以及最大方法区容量,程序计数器消耗内存可以忽略并且虚拟机进程本身所占的内存不在计算之内,剩下的内存就由本地方法栈和虚拟机栈瓜分,每个线程分配到的栈容量越大可以建立的线程数量自然就越少,建立线程时就容易把剩下的内存耗尽。

        栈深度在大多数情况下达到1000~2000完全没有问题,对于正常的方法调用这个深度足够使用,但是在建立多线程导致的内存溢出在不能减少线程数或者更换64位虚拟机的情况下,就只能减少最大堆容量和减少栈容量来换取更多线程。

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

        运行时常量池是方法区的一部分所以这两个放在一起,说到常量池就要说到一个方法那就是String.intern(),这个方法是一个Native方法它的作用是如果字符串常量池以及包含一个等于此String对象的字符串,则返回代表池中并且返回此String对象的引用,在1.6版本之前的版本中由于常量池分配在永久代可以通过参数限制方法区大小,从而间接限制其中常量池的容量,当运行时常量池溢出时会给出PermGen space说明运行时常量池属于方法区,但是使用1.7就不会出现这种情况(因为1.7之后开始逐渐去永久代)。

public static void main(String[] args){

    String str1 =new StringBulider("hhh").append("ooo").toString();

    System.out.println(str1.intern()==str1);

    String str2 = new StringBulider("ja").append("va").toString();

    System.out.println(str2.,intern() == str2);

}

      这段代码在jdk1.7和jdk1.6中运行得到的结果是不一样的,1.6版本中会得到两个false而在1.7会得到一个true和一个false。因为在1.6中intern()方法会把首次遇到的字符串实例复制到永久代中返回的也是永久代中的这个字符串实例的引用,而由StringBulider创建的字符串实例实际在堆中所以不是同一个引用将返回false,而1.7版本的intern()的实现不会在复制实例,只是在常量池中记录首次出现的实例引用,因此intern()和StringBuilder创建的实例是一个,对str2比较返回false的原因是java这个字符串在执行StringBulider.toString()之前出现过了,字符串常量池中已经有了它的引用不符合首次出现的原则。

      方法区用于存放Class的信息,对于该区的测试基本上就是运行时产生大量的类去填满方法区,可以通过GGlib操作字节码直接在运行时生成大量的动态类。在当前的主流框架中,如Spring,Hibernate对类进行加强时都会使用GGlib这类字节码技术,方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉判定条件比较苛刻。

本机直接内存溢出

       DirectMemory容量可以通过MaxDirectMemorySize指定,如果不指定则默认与java最大值一样,直接通过反射获取Unsafe实例进行内存分配,因为使用DirectByteBuffer分配内存也会抛出内存溢出异常,但是抛出异常时并没有真正向操作系统申请分配内存而是计算得知内存无法分配于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory(),由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会有明显的异常,如果OOM之后发现Dump文件很小而程序中又直接或者间接使用了NIO就可以考虑是不是直接内存溢出了。

猜你喜欢

转载自blog.csdn.net/ZytheMoon/article/details/82954245