内存溢出OOM和堆栈溢出SOF的示例

1、Java堆溢出 (OOM)

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

public class Test {

    public static void main(String[] args){

    List list=new ArrayList();   //持有“大对象”的引用,防止垃圾回收

        while(true){

            int[] tmp = new int[10000000];  //不断创建“大对象”

            list.add(tmp);

        }

    }

}


内存溢出OOM和堆栈溢出SOF的示例_第1张图片

要解决这个异常,一般先通过内存映像分析工具对堆转储快照分析,确定内存的对象是否是必要的,即判断是内存泄露还是内存溢出。如果是内存泄露,可以进一步通过工具查看泄露对象到GC Roots的引用链,比较准确地定位出泄露代码的位置。如果是内存溢出,可以调大虚拟机堆参数,或者从代码上检查是否存在某些对象生命周期过长的情况。

2、虚拟机栈和本地方法栈溢出 (SOF/OOM)

(1). SOF

如果线程请求的栈深度大于虚拟机栈允许的最大深度,将抛出StackOverflowError异常。我们知道,每当Java程序启动一个新的线程时,Java虚拟机会为它分配一个栈,并且Java虚拟机栈以栈帧为单位保持线程运行状态。每当线程调用一个方法时,JVM就压入一个新的栈帧到这个线程的栈中,只要这个方法还没返回,这个栈帧就存在。 那么可以想象,如果方法的嵌套调用层次太多,比如递归调用,随着Java虚拟机栈中的栈帧的不断增多,最终很可能会导致这个线程的栈中的所有栈帧的大小的总和大于-Xss设置的值,从而产生StackOverflowError溢出异常。看下面的栗子:

public class Test {

    public static void main(String[] args) {

          method();

    }

//递归调用导致 StackOverflowError

    public static void method(){

        method();

    }

}


内存溢出OOM和堆栈溢出SOF的示例_第2张图片

上面的SOF异常就是由递归引起的,具体而言就是因为method()方法中没有递归终止条件,从而使得该方法不断递归调用、不断创建栈帧导致的。

(2). OOM

  如果虚拟机在拓展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。在虚拟机栈和本地方法栈发生OOM异常场景如下:当Java 程序启动一个新线程时,若没有足够的空间为该线程分配Java栈(一个线程Java栈的大小由-Xss设置决定),JVM将抛出OutOfMemoryError异常。

3、方法区和运行时常量池溢出 (OOM)

  运行时常量池溢出的情况:String.intern()是一个native方法,在JDK1.6及之前的版本中,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象,否则将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在永久代中,如果不断地使用intern方法手动入池字符串,则会抛出OutOfMemoryError异常。但在JDK1.7及其以后的版本中,对intern()方法的实现作了进一步改进,其不会再复制实例到常量池中,而仅仅是在常量池中记录首次出现的实例的引用。看下面的例子(在JDK1.7中运行),

public class Test {  

    public static void main(String[] args) {  

        String str1 = new StringBuilder("计算机").append("软件").toString();

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

        String str2 = new StringBuilder("java").toString();

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

    }/* Output:

        true

        false

     *///:~  

  为什么第一个返回true,而第二个返回false呢?因为在JDK1.7中,intern()方法的实现不会再复制实例,只是在常量池中记录 首次 出现的实例的引用,因此str1.intern()和str1指向的是同一个字符串,所以返回true。同一个引用。对于“java”这个字符串,由于在执行StringBuilder.toString() 之前已经出现过,所以字符串常量池中在new StringBuilder(“java”).toString()之前已经有它的引用了,不符合首次出现的原则,因此返回fasle。有人可能心里可能就要嘀咕了,为啥第二个不符合首次出现的原则,而第一个就符合首次出现的原则呢? 实际上,

String str2 = new StringBuilder("java").toString();

1

等价于:

String s1 = "java";

StringBuilder sb = new StringBuilder(s1);

String str2 = sb.toString();


// StringBuilder的 toString()方法

public String toString() {

        // Create a copy, don't share the array

        return new String(value, 0, count);

}

  由上面代码可知,字符串”java”早就出现了,因此不符合首次出现的原则,返回false。同理,“计算机软件”这个字符串在new StringBuilder(“计算机”).append(“软件”).toString()之前从未出现过,因此符合首次出现的原则,返回true。

  方法区溢出的情况:一个类要被垃圾回收器回收掉,判断条件是比较苛刻的。在经常动态产生大量Class的应用中,需要特别注意类的回收状况,比如动态语言、大量JSP或者动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。

你可能感兴趣的:(内存溢出OOM和堆栈溢出SOF的示例)