总结Java程序内存溢出原因

目录

内存溢出和内存泄漏

直接内存溢出

堆溢出

方法区溢出


      这篇日志总结下Java程序中的发生内存溢出的一些原因,我们知道JVM堆空间十分重要,大部分对象在创建时都是放在堆中(除了一些逃逸对象是栈上分配),例如新生代存放在eden区中。随着对象的不断创建和老年代对象的不断产生,如果垃圾回收不能及时释放内存,最终堆内存被耗尽,新对象创建时由于内存不足,申请空间失败,导致内存溢出,在Java程序中内存溢出分几种,直接内存溢出,堆溢出和永久区溢出,在总结这几种内存溢出情况前,先分清内存溢出和内存泄漏这两者的区别和关联。

 

内存溢出和内存泄漏

      内存泄漏(memory leak)指的是程序执行过程中,在申请内存,使用完毕后没有释放资源,内存堆积越来越多,最后堆空间被占用完,内存泄漏大致可以分为4种:

  1. 一次性内存泄漏:指的是由于逻辑实现部分的问题,该部分代码只会被执行一次,但一执行就会有一片内存发生泄漏。
  2. 常发性内存泄漏:指的是这部分代码会被执行多次,每一次执行时都会导致一片内存发生泄漏。
  3. 偶然性内存泄漏:指的是这部分代码在某些特定场合下执行才会发生泄漏,这类情况比较难发现。
  4. 最后一种是隐式的内存泄漏:指的是对于一些需要运行很久的程序,例如服务器程序,程序运行过程种不断地申请内存,一直到程序运行结束后才把所有的空间释放掉,如果中途GC没有及时回收可用空间,最总也会导致内存泄漏,当然也有可能程序运行到结束后也没有出现泄漏,这样的情况称为隐式内存泄漏,泄漏有可能发生,也可能不发生。

      内存溢出上面讲了是怎么一回事,就是没有足够的空间分配给新的对象了,内存泄漏与溢出两者的关系,就是内存泄漏最后会导致内存溢出。

 

直接内存溢出

      对于使用了NIO的Java程序,代码可以向操作系统申请一块内存,称为“直接内存”,不同于堆内存,直接内存不交由JVM管理,优点是对直接内存的访问速度要快于Java堆内存,这样我们就可以把一些经常被访问到的代码,对象等放到直接内存里,提高整个程序的执行效率。但是如果不及时GC可回收内存,随着内存的不断申请,最终也会导致内存溢出,来看一个例子:

public static void main(String[] args) {

      for(int i=0; i<1024; i++) {

         System.out.println(""+i+"次申请内存.");

         ByteBuffer.allocateDirect(1024*1024);

}

      程序很简单,使用ByteBuffer.allocateDirect()不断向操作系统申请直接内存,且不显式地调用System.gc()进行垃圾回收,使用参数-Xmx128m –XX:PrintGCDetails来执行程序,按道理来说,随着内存的不断申请又不回收释放,最后会因为无法申请到足够的空间而导致OutOfMemoryError内存溢出。不过..我在自己的Windows和Ubuntu下执行,发现程序都能顺利执行,不管你代码申请的空间有多大,因为GC一直有在作用:

总结Java程序内存溢出原因

总结Java程序内存溢出原因

      原因是两个系统都是64位,在64位系统下对程序的可用最大内存是没有限制的,32位系统则有。至于为什么我们没有显式调用GC,GC却一样有在作用,原因是当程序使用的直接内存大小达到最大之后,GC就会自动执行,所以,如果我们在Ubuntu下加上参数-XX:MaxDirectMemorySize=1g来执行上述代码,限制最大可用内存,则程序执行到中途就会因为无法申请内存而终止,整个过程GC也没有发生作用:

总结Java程序内存溢出原因

 

堆溢出

      堆溢出可以说是Java程序中内存溢出最常见的一种情况,因为新生代对象大部分都是创建分配在eden区中,如果这些新生代都是形如String str = new String(”Hello World.”);这样的强引用对象,GC不会对它们进行回收,那么随着对象越积越多,最后所占的内存大小大于-Xmx参数设置的最大堆空间大小后,就会发生堆溢出,来看一个例子:

ArrayList<byte[]> myList = new ArrayList<byte[]>();

      for(int i=0; i<1024; i++) {

         System.out.println(i);

         myList.add(new byte[1024*1024]);

}

总结Java程序内存溢出原因

可以看到,程序执行抛出了堆溢出错误,后面标明了“Java heap space”,也就是说这是一次堆溢出。

 

方法区溢出

       方法区用来存放Java类元数据,也就是类的信息,即类名,实现的接口列表和直接父类名;一些常量和静态变量,字段和方法等,它和Java堆一样是线程共享的,如果一个程序中加载的类太多,那么方法区就有可能溢出。看一个例子:

try {

         for(int i=0; i<99999; i++) {

            //System.out.println(i);

            HeapOOM bean = new HeapOOM(i, new HashMap());

         }

      } catch(Error e) {

         e.printStackTrace();

      }

我们可以使用参数-XX:MaxPermSize=10m参数运行,使得程序出现OutOfMemoryError:PermGen space错误,如果出现方法区溢出,也可以通过把MaxPermSize的值调大来解决。