在Java虚拟机运行时数据区中,除了程序计数器之外,虚拟机栈、本地方法栈、方法区和Java堆都有发生OutOfMemoryError(简称OOM)异常的可能。

  一、Java堆溢出

  Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。

  VM参数:

  • -Xms20m:设置堆的最小值为20MB
  • -Xmx20m:设置堆的最大值为20MB,两者设置一样是为了避免堆自动扩展
  • --XX:+HeapDumpOnOutOfMemoryError:让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析(默认存储路径为程序工作目录下)
public class HeapOOM {

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



Java heap space



java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid14208.hprof ...
Heap dump file created [28125411 bytes in 0.077 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
    at java.util.Arrays.copyOf(Unknown Source)
    at java.util.ArrayList.grow(Unknown Source)
    at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
    at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
    at java.util.ArrayList.add(Unknown Source)
    at outOfMemoryError.HeapOOM.main(HeapOOM.java:14)



  解决方法:一般的手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分析清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

  •  内存溢出(Memory Overflow):是指程序在申请内存时,没有足够的内存空间供其使用。
  •  内存泄漏(Memory Leak):是指在程序申请内存后,无法释放以申请的内存空间。

  如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法回收它们的。掌握了泄漏对象的类型信息以及GC Roots引用的信息,就可以比较准确地定位出泄漏的位置。

  如果不存在内存泄漏,也就是内存中的对象确实都还必须存活着,那就应该检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期间的内存消耗。

  例如:可以看到占据了main线程创建的对象占据了16MB的内存。

  

java dump 文件 分析 java dump oom_java

 

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

  由于在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此,对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。对于虚拟机栈和本地方法栈,在Java虚拟机中有两种异常。

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常

  实际情况下,在单个线程情况下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。

  如果不断地建立线程,并且通过-Xss参数为每个线程的栈分配的内存越大,越容易产生OutOfMemoryError异常。

  因此,如果建立过多线程导致内存溢出,在不能较少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

  例如:1.通过-Xss128k设置单个线程的虚拟机栈容量为128k,单个线程情况下出现了StackOverflowError异常

        2.为通过-Xss2M设置单个线程的虚拟机栈容量为2M,然后不断地创建线程的情况下出现了OutOfMemoryError异常。

 

  三、包括常量池溢出

  String.intern()方法是一个Native方法,它的作用是:如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回常量池中的这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

  在JDK1.6及之前的版本中,由于常量池分配在永久代,可以通过-XX:PermSize=10M和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池容量。



public class RuntimeConstantPool100M {

    public static void main(String[] args) {
        // 使用List保持着常量池引用,避免Full GC回收常量池行为
        List<String> list = new ArrayList<>();
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}



  因此,在JDK1.6及之前的版本中,会报PermGen Space异常,这是因为,通过String的intern()方法连续不断地将不同的字符串都加入到运行时常量池中,然后会填满运行时常量池。这里的常量池属于方法区的一部分,而方法区又属于永久代,因此会报永久代异常。

  但是,上面的示例在JDK1.6之后的版本就不会得到相同的结果,而是会一直循环下去,这是因为字符串常量池实现方式的不同。例如:



public class RuntimeConstantPool100M {
    
    public static void main(String[] args) {
        String str1 = new StringBuilder("Computer").append("Software").toString();
        System.out.println(str1.intern() == str1);  // 返回true
        
        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);  // 返回false
    }
}



  在JDK1.6中会返回两个false,这是因为JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用,而又StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,因此将返回false。

  而JDK1.7中的intern()不会再复制实例,只是在常量池中记录首次出现的实例的引用并返回这个引用,因此对于str1.intern()方法的执行过程就是,把str1这个引用记录到常量池中,并且返回这个引用。而对于str2来说,由于常量池中已经有“Java”这个字符串的引用(默认就有),因此不是首次出现的,所以不是同一个引用。

  

  四、方法区溢出

  方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。如果运行时产生了大量类填满了方法区,那么方法区就会溢出。方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。在经常动态生成大量Class的应用中,需要特别注意类的回收情况。

 

  五、本机内存直接溢出

  DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样。由DirectMemory导致的内存溢出,在Heap Dump文件中不会看见明显的异常,如果发现Dump文件很小,而程序中又直接或者间接使用了NIO,可能就是由于DirectMemory溢出导致的。