疑问:java死循环中无限生成对象会不会OOM?

答案:有可能会有可能不会。

1.先说会的场景,虚拟机配置:-Xmx10M -Xms10M

代码如下:

public class TestHeapOom {

    static class OOMObject{
    }

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

运行结果如下:

F:\java\bin\java.exe -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:64209,suspend=y,server=n -Xmx10M -Xms10M ......
Connected to the target VM, address: '127.0.0.1:64209', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:64209', transport: 'socket'
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	    at java.util.Arrays.copyOf(Arrays.java:3210)
	    at java.util.Arrays.copyOf(Arrays.java:3181)
	    at java.util.ArrayList.grow(ArrayList.java:261)
	    at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	    at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	    at java.util.ArrayList.add(ArrayList.java:458)
	    at com.example.demo.OOM.TestHeapOom.main(TestHeapOom.java:15)

    Process finished with exit code 1
为什么会OOM?

一直new对象,并将new出的对象指向了list,占用内存在某一时刻大于10M了,就OOM了,那么多问自己一句为什么这么多对象没有被回收?因为可达性分析算法判定该对象仍是可达状态不可回收,jvm中在不断的new对象,这些对象没有被回收有一个前提,那就是仍是可达状态,为什么可达,因为list相当于这些new出的对象的GC Roots(总共4种对象可作为GC Roots,最典型的就是栈中引用指向的对象),可达性分析算法规定,对象与GC Roots之间存在引用链,则不回收,所以会OOM.

2.再说不会的场景,虚拟机配置:-Xmx10M -Xms10M

代码如下:

public class TestHeapOom {

    static class OOMObject{
    }

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

使用 jvisualvm.exe 工具分析堆内存占用情况如下:

Android thread 中的死循环检测 不执行了_List


可以发现内存占用到达一定高度后就会触发GC回收内存,并不会有OOM的情况,两处代码的唯一区别也就是一下这行代码的位置,一个在循环外,一个在循环内。

List<OOMObject> list = new ArrayList<OOMObject>();
为什么不会OOM?

按照可达不可达的算法,这种情况new出的对象也会有引用链存在,为什么会被回收了呢?

猜想一:虽说每个new对象都有对应的list对象作为GC Roots但是每个对象都是相同的,会不会有其他机制判断相同同样会是GC的目标?

验证猜想一代码:

public class TestHeapOom {

    static class OOMObject{
       String name;
       public OOMObject(String name){
           this.name = name;
       }
    }
    
    public static void main(String[] args){
        int n =0;
        while (true){
            List<OOMObject> list = new ArrayList<OOMObject>();
            list.add(new OOMObject("a"+n));
        }
    }
}

堆内存占用情况如下图:

Android thread 中的死循环检测 不执行了_局部变量_02

猜想一结论:很明显内存占用到达一定高度就会触发GC,说明这些对象还是留不住,显然不是因为对象都是相同的就给回收了。
猜想二:对象的回收应该还有其他的判定,作为可达性分析算法的补充,或者我理解的可达性分析算法有误,暂时没有解决方案,记录在此,下次再研究,如有路过的朋友知道答案,还望不吝赐教,感激不尽。

3 不会OOM的真正原因

3.1先对几个概念进行说明

①除static修饰的方法外,其余方法的局部变量表(不清楚这是啥的,需要先看看JVM的运行时数据区)最小值是1,这个1是用于存储该方法所属类的实例变量的,这是一个隐式加载每个非static方法都是如此,在方法内使用this调用的就是这个隐式对象。

②局部变量表中的变量槽的个数在编译器就已经确定,且局部变量在方法内部也是有作用范围的,当局部变量超出自身作用范围后,如果需要,这个已经超过使用范围的局部变量所占用的局部变量槽会被其他变量占用,这也就是局部变量槽可以复用的场景。

③局部变量表是最常见的一种GC Roots。

3.2 原因解释

首先看下2里面代码的class文件,可以清晰的看到main方法只拥有3个局部变量槽,一个是 传入参数args占用,一个是局部变量n占用,一个是局部变量list占用,既然只有三个变量槽,那无限生成的list会存储到哪里?看下3.1中的②,因为局部变量槽是可以复用的,后期生成的list其实都是在占用之前的变量槽,即使无限生成对象,其实局部变量表中也就只有一个对象指向到堆中,其余堆中new出来的对象是没有引用指向他们的,没有引用指向他们,他们自然就是垃圾了,那么只要有GC触发他们就会被回收。这也就是为什么不会OOM的原因了。

Android thread 中的死循环检测 不执行了_List_03