堆内存溢出

/**
 * 执行前需要配置的参数
 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * -Xms:最小堆内存;-Xmx:最大堆内存;
 * -XX:+HeapDumpOnOutOfMemoryError:内存溢出时拉取内存堆转储快照
 */

public class HeapOOM {
    /** 定义一个内部类 */
    static class OOMObject{
    }

    public static void main(String[] args) {
        //创建一个集合存放对象,防止对象被垃圾回收
        ArrayList<OOMObject> list = new ArrayList<>();

        //循环创建对象并放入集合中
        while (true){
            list.add(new OOMObject());
        }
    }
}

报错信息如下:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid2990.hprof ...
Heap dump file created [27926099 bytes in 0.076 secs]
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:267)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
	at java.util.ArrayList.add(ArrayList.java:464)
	at HeapOOM.main(HeapOOM.java:15)

可根据快照文件分析内存溢出的具体对象位置,根据业务中对象是否必需,我们把OOM分为两种情况:内存泄漏内存溢出

注:具体排查方式会在后续给出。

内存泄漏:

  1. 概念: 指的是堆内存中大部分对象已经无关紧要了,垃圾回收器完全可以回收它们,而它们却因为被判定为不可回收条件从而导致了对象的堆积。
  2. 解决方案: 通过分析快照找到具体是哪个对象导致了OOM,在业务中找到该对象的使用场景,在该对象被利用完之后应当置为NULL以便垃圾收集器回收。

内存溢出:

  1. 概念: 堆内存中的每一个对象都依然存在利用价值,不能被垃圾回收。
  2. 解决方案: 适当扩大堆内存的大小,或者在代码层面关注是否可通过某些创建型设计模式进行优化。

栈内存溢出

/**
 * 栈帧过多导致的SOF
 * VM Args: -Xss228k
 * -Xss:栈内存容量
 */

public class JavaVMStackSOF2Many {

    private int stackLength = 1;

    /**
     * 递归,无限增加栈帧
     */
    public void stackLeak(){
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
        JavaVMStackSOF2Many oom = new JavaVMStackSOF2Many();
        try {
            oom.stackLeak();
        } catch (Exception e) {
            System.out.println("stack length: " + oom.stackLength);
            throw e;
        }
    }
}

报错信息如下:

Exception in thread "main" java.lang.StackOverflowError
	at JavaVMStackSOF2Many.stackLeak(JavaVMStackSOF2Many.java:14)
	at JavaVMStackSOF2Many.stackLeak(JavaVMStackSOF2Many.java:15)
	at JavaVMStackSOF2Many.stackLeak(JavaVMStackSOF2Many.java:15)
	at JavaVMStackSOF2Many.stackLeak(JavaVMStackSOF2Many.java:15)
	at JavaVMStackSOF2Many.stackLeak(JavaVMStackSOF2Many.java:15)
	······
/**
 * 栈帧过大导致的SOF
 * VM Args: -Xss228k
 * -Xss:栈内存容量
 */


public class JavaVMStackSOF2Big {

    private static int stackLength = 1;

    /**
     * 扩大栈帧容量
     */
    public static void stackLeak(){
        long  unused1,unused2,unused3,unused4,unused5,unused6,unused7,unused8,unused9,unused10,
              unused11,unused12,unused13,unused14,unused15,unused16,unused17,unused18,unused19,unused20,
              unused21,unused22,unused23,unused24,unused25,unused26,unused27,unused28,unused29,unused30,
              unused31,unused32,unused33,unused34,unused35,unused36,unused37,unused38,unused39,unused40,
              unused41,unused42,unused43,unused44,unused45,unused46,unused47,unused48,unused49,unused50,
              unused51,unused52,unused53,unused54,unused55,unused56,unused57,unused58,unused59,unused60,
              unused61,unused62,unused63,unused64,unused65,unused66,unused67,unused68,unused69,unused70,
              unused71,unused72,unused73,unused74,unused75,unused76,unused77,unused78,unused79,unused80,
              unused81,unused82,unused83,unused84,unused85,unused86,unused87,unused88,unused89,unused90,
              unused91,unused92,unused93,unused94,unused95,unused96,unused97,unused98,unused99,unused100;

        stackLength++;
        stackLeak();
              unused1=unused2=unused3=unused4=unused5=unused6=unused7=unused8=unused9=unused10=
              unused11=unused12=unused13=unused14=unused15=unused16=unused17=unused18=unused19=unused20=
              unused21=unused22=unused23=unused24=unused25=unused26=unused27=unused28=unused29=unused30=
              unused31=unused32=unused33=unused34=unused35=unused36=unused37=unused38=unused39=unused40=
              unused41=unused42=unused43=unused44=unused45=unused46=unused47=unused48=unused49=unused50=
              unused51=unused52=unused53=unused54=unused55=unused56=unused57=unused58=unused59=unused60=
              unused61=unused62=unused63=unused64=unused65=unused66=unused67=unused68=unused69=unused70=
              unused71=unused72=unused73=unused74=unused75=unused76=unused77=unused78=unused79=unused80=
              unused81=unused82=unused83=unused84=unused85=unused86=unused87=unused88=unused89=unused90=
              unused91=unused92=unused93=unused94=unused95=unused96=unused97=unused98=unused99=unused100 = 0L;
    }

    public static void main(String[] args) {
        try {
            stackLeak();
        } catch (Exception e) {
            System.out.println("stack length: " + stackLength);
            throw e;
        }
    }
}

报错信息如下:

Exception in thread "main" java.lang.StackOverflowError
	at JavaVMStackSOF2Big.stackLeak(JavaVMStackSOF2Big.java:25)
	at JavaVMStackSOF2Big.stackLeak(JavaVMStackSOF2Big.java:26)
	at JavaVMStackSOF2Big.stackLeak(JavaVMStackSOF2Big.java:26)
	at JavaVMStackSOF2Big.stackLeak(JavaVMStackSOF2Big.java:26)
	at JavaVMStackSOF2Big.stackLeak(JavaVMStackSOF2Big.java:26)
	···

由此可知,不论是栈帧过多,还是栈帧过大,都会导致SOF异常。

注:

  1. 由于HotSpot虚拟机不支持栈容量动态扩展,所以没有OOM异常,而Classic虚拟机支持栈内存的动态扩展,当栈内存扩展时无法申请到足够的内存时,也会报OOM异常。
  2. 如果方法中执行的是创建线程并使用线程的操作,则也会抛OOM异常。其主要原因是:每创建一个线程,就会对应分配一个虚拟机栈(线程栈),操作系统分配给每个进程的内存是有限制的,我们可以粗略的认为进程内存 = 虚拟机栈+本地方法栈内存+堆内存+方法区内存+其他,在其他内存(包括堆内存、方法区内存等)都已分配完成的情况下,剩余部分都由虚拟机栈和本地方法栈来分配了,在总内存不够充裕的情况下,系统无法给新线程开辟栈空间,就会导致OOM,其本质还是总的空间内存不足导致的,而非栈内存不足。具体代码如下(善意提示:请勿轻易尝试,如果非要手贱的话,请保存当前工作任务):
/**
 * VM Args:-Xss2M
 */

public class JavaVMStackOOM {

    /**
     * 给线程定义一个死循环方法,保持线程活跃
     */
    private void dontStop() {
        while (true) {
        }
    }

    /**
     * 创建线程
     */
    public void stackLeakByThread() {
        while (true) {
            new Thread(() -> {
                dontStop();
            }).start();
        }
    }

    public static void main(String[] args) {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}

这里直接复制《深入理解Java虚拟机》中的错误信息:

Exception in Thread "main" java.lang.OutOfMemoryError: unable to create native thread

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

由于本人使用的M1芯片的Mac系统,无法使用旧版本的JDK,所以异常摘自《深入理解Java虚拟机》

/**
 * VM Args: -XX:PermSize=6M -XX:MaxPermSize=6M
 * 注:设置的是永久代内存,只适用于JDK1.6及之前版本
 */

public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        //创建集合存储字符串保证字符串对象不被垃圾回收
        HashSet<String> set = new HashSet<>();
        short i = 0;
        while (true){
            //将字符串放入方法区
            set.add(String.valueOf(i++).intern());
        }
    }
}

报错信息如下:

java.lang.OutOfMemoryError: PermGen space

而在JDK1.8中,永久代已完全退出历史舞台,取而代之的是元空间,代码无需更改,只需更改虚拟机参数即可:

/**
 * VM Args: -Xmx6M
 * 注:设置的是永久代内存,只适用于JDK1.6及之前版本
 */

public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        //创建集合存储字符串保证字符串对象不被垃圾回收
        HashSet<String> set = new HashSet<>();
        short i = 0;
        while (true){
            //将字符串放入方法区
            set.add(String.valueOf(i++).intern());
        }
    }
}

报错信息如下:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.HashMap.resize(HashMap.java:706)
	at java.util.HashMap.putVal(HashMap.java:665)
	at java.util.HashMap.put(HashMap.java:614)
	at java.util.HashSet.add(HashSet.java:220)
	at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:15)

可以看到,将最大堆内存设置为6M时,在HashMap扩容时就发生了OOM(HashSet底层是HashMap),几乎看不到方法区内存溢出,原因是在JDK1.8时,方法区的实现主要通过本地内存开辟的元空间来实现。

关于元空间的配置参数介绍:

  1. -XX:MaxMetaspaceSize:设置元空间最大值,默认为-1(不限制,但受限于本地内存)
  2. -XX:MetaspaceSize:元空间初始大小,达到该值触发垃圾回收。垃圾回收器会根据垃圾回收情况动态调整该值:如果垃圾回收时释放了大量空间,则会适当降低该值;如果释放的空间很微小,会适当提高该值(但不会超过设置的最大值)
  3. -XX:MinMetaspaceFreeRatio:在垃圾回收之后,控制最小的元空间剩余容量百分比,以减少因为元空间不足导致的垃圾回收频率。
  4. -XX:MinMetaspaceFreeRatio:控制最大的元空间剩余容量百分比。

注:类爆炸会引起方法区内存溢出。

本地直接内存溢出

直接内存可通过参数 -XX:MaxDirectMemorySize 来设置,如果不指定,会和最大堆内存 -Xmx 保持一致

/**
 * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
 */

public class DirectMemoryOOM {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws IllegalAccessException {
        //反射获取Unsafe实例对象
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe =(Unsafe) unsafeField.get(null);
        while (true){
            //申请分配内存 1MB
            System.out.println("分配内存1MB...");
            unsafe.allocateMemory(_1MB);
        }
    }
}

理论上应当会报OOM,但我在自己电脑上并没有报异常,具体问题有待分析…

注:DirectMemory导致的OOM并不会在Heap Dump中凸显出来,如果程序中出现了OOM而又没有较大或较多的异常对象时,应当考虑是否使用了NIO等利用了直接内存的相关技术。