读者朋友,下午好。

这里从JVM的堆、栈、方法区(常量池)、本机直接内存四个维度分别制造OOM-Out Of Memory。
目的:
1、怎样的操作会导致在指定区域发生OOM?或者StackOverFlow?
2、出现这种情况时候如何分析?如何解决。(不是重点,后面学习了各种检测工具之后回头思考)

以下代码示例均来自:

《深入理解Java虚拟机 JVM高级特性与最佳实践》 周志明著 第三版
机械工业出版社

1、堆溢出

要使堆溢出,只需要创建大量对象即可,并且保证对象在使用。

package com.cmh.concurrent.jvm;

import java.util.ArrayList;
import java.util.List;

/**
 * Author: 起舞的日子
 * Date:2021/5/15 3:33 下午
 * <p>
 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/hmc/myjvm
 */
public class HeapOOM {

    static class OOMObject {
    }

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

报错信息:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to /Users/hmc/myjvm/java_pid5636.hprof ...
Heap dump file created [30327258 bytes in 0.215 secs]
Exception in thread "main" Exception in thread "Monitor Ctrl-Break" java.lang.OutOfMemoryError: Java heap space
	at com.cmh.concurrent.jvm.HeapOOM.main(HeapOOM.java:20)
java.lang.OutOfMemoryError: Java heap space

错误很明显,heap的地方溢出了。那么需要进一步判断哪里导致了溢出,我们在JVM参数中dump出了堆转储快照,对这个简单分析一下看下结果。若是Eclipse可以用Eclipse Memory Analyzer工具,但是Mac下找了许久用JProfiler工具看下。

堆转储快照,文件格式是:java_pid1364.hprof ,其中1364即是pid。

JProfiler不是免费的,适用一下感受一下,又因为用Jmat命令看不了,即便看的了也不方便。
通过:Session => Open snapshot打开 .hprof文件即可。

oom后java生成dump java如何制造oom_sed

可以发现,54万个实例对象,8M左右就把20M的堆内存挤爆了。

oom后java生成dump java如何制造oom_oom后java生成dump_02


通过Graph也可以清晰看到哪里导致的堆溢出。

oom后java生成dump java如何制造oom_常量池_03

报错信息:
stack length:499
Exception in thread “main” java.lang.StackOverflowError
at com.cmh.concurrent.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15)
at com.cmh.concurrent.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:16)
at com.cmh.concurrent.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:16)

2、虚拟机栈溢出

栈里面主要是方法,递归调用方法就可以把栈耗尽;或者使局部变量表长度超过限制。

2.1 法1:减少栈容量

栈容量通过:-Xss144k控制

package com.cmh.concurrent.jvm;

/**
 * Author: 起舞的日子
 * Date:2021/5/16 3:32 下午
 * <p>
 * VM Args: -Xss144k
 * 验证:改变虚拟机栈容量,使其变小导致stackOverFlow
 */
public class JavaVMStackSOF {

    private int stackLength = 1;

    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

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

报错信息:

stack length:499
Exception in thread "main" java.lang.StackOverflowError
	at com.cmh.concurrent.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15)
	at com.cmh.concurrent.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:16)
	at com.cmh.concurrent.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:16)

2.2 法2:增加本地变量表长度

package com.cmh.concurrent.jvm;

/**
 * Author: 起舞的日子
 * Date:2021/5/16 3:32 下午
 * <p>
 * 增加局部变量表,使栈帧过大,抛出stackOverFlow
 */
public class JavaVMStackSOFV2 {

    private int stackLength = 1;

    public 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 = 0;
    }

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

结果:

stack length:3379
Exception in thread "main" java.lang.StackOverflowError
	at com.cmh.concurrent.jvm.JavaVMStackSOFV2.stackLeak(JavaVMStackSOFV2.java:36)
	at com.cmh.concurrent.jvm.JavaVMStackSOFV2.stackLeak(JavaVMStackSOFV2.java:36)
	at com.cmh.concurrent.jvm.JavaVMStackSOFV2.stackLeak(JavaVMStackSOFV2.java:36)

2.3 法3: 创建大量线程

package com.cmh.concurrent.jvm;

/**
 * Author: 起舞的日子
 * Date:2021/5/16 3:55 下午
 * <p>
 * VM Args: -Xms2M
 */
public class JavaVMStackOOM {

    private void dontStop() {
        while (true) {
        }
    }

    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

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

结果:

[11.455s][warning][os,thread] Failed to start thread - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 4k, detached.
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
	at java.base/java.lang.Thread.start0(Native Method)
	at java.base/java.lang.Thread.start(Thread.java:801)
	at com.cmh.concurrent.jvm.JavaVMStackOOM.stackLeakByThread(JavaVMStackOOM.java:24)
	at com.cmh.concurrent.jvm.JavaVMStackOOM.main(JavaVMStackOOM.java:30)
[71.593s][warning][os,thread] Failed to start thread - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 4k, detached.
OpenJDK 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated

3、方法区和常量池溢出

方法区主要放着类相关的信息和常量池。所以不断创建类或者不断增加常量池内容,都可以使其溢出。

注意jdk8之后,常量池放在了方法区域,已经没有PermSize相关的设置了

3.1 增加常量池内容

package com.cmh.concurrent.jvm;

import java.util.HashSet;
import java.util.Set;

/**
 * Author: 起舞的日子
 * Date:2021/5/16 4:17 下午
 * <p>
 * VM Args: -XX:PermSize=6M -XX:MaxPermSize=6M 这么限制已无效,需要
 * 所以需要限制 -Xmx6m 才会出现OOM
 */
public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        //使用Set保持着常量池的引用,避免Full GC回收常量池的行为
        Set<String> set = new HashSet<>();
        //在short范围内足以让6M的PermSize产生OOM了
        short i = 0;
        while (true) {
            set.add(String.valueOf(i++).intern());
        }
    }

    /**
     * OpenJDK 64-Bit Server VM warning: Ignoring option PermSize; support was removed in 8.0
     * OpenJDK 64-Bit Server VM warning: Ignoring option MaxPermSize; support was removed in 8.0
     *
     *  因为JDK8以后已经用元空间替代永久代了,所以限制永久代来间接限制常量池已经不起作用了
     *  所以需要限制 -Xmx6m 才会出现OOM
     *
     *
     */
}

3.2 增加类

package com.cmh.concurrent.jvm;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * Author: 起舞的日子
 * Date:2021/5/16 5:11 下午
 * <p>
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M jdk7以后已经不生效了,会永远运行下去
 * 改用:
 * -XX:MaxMetaspaceSize=5M
 */
public class JavaMethodAreaOOM {

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invoke(obj, objects);
                }
            });
            enhancer.create();
        }
    }

    static class OOMObject {
    }
}

3.3 String.intern()方法

因为常量池涉及到了此方法,顺便写了demo验证了下

package com.cmh.concurrent.jvm;

import org.junit.Test;

/**
 * Author: 起舞的日子
 * Date:2021/5/16 4:31 下午
 */
public class StringIternTest {
    public static void main(String[] args) {
        String str1 = new StringBuilder("计算机").append("软件").toString();
        System.out.println(str1.intern() == str1);


        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);


        /**
         * 结果:
         * true
         * false
         *
         * 因为JDK7以后,字符串常量池已经移动到Java方法区中了。所以第一个是true。
         * 因为java这个词之前已经出现,不符合itern()方法要求"首次遇到"的原则,
         *
         * String.itern()
         * String提供的intern方String.intern() 是一个Native方法,它的作用是:如果运行时常量池中已经包含一个等于此String对象内容的字符串,
         * 则返回常量池中该字符串的引用;
         * 如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。
         *
         *
         * 书中说"java"首次进入常量池是在加载sun.misc.Version时候进入的。
         */
    }

    @Test
    public void testStrIntern() {
        String str1 = "abcd";
        String str2 = new String("abcd");
        System.out.println(str1 == str2);//false  str1在方法区的常量池中;str2在堆的空间中


        String s1 = new String("计算机");
        String s2 = s1.intern();
        String s3 = "计算机";
        System.out.println(s2);//计算机  s.intern()发现常量池中没有,所以新建了一个
        System.out.println(s1 == s2); //false s1是堆中的,s2是方法去常量池中的
        System.out.println(s3 == s2); //true  直接用""表示的就默认在常量池中去找

        /**
         * 结论:直接使用双引号声明的String对象会直接存储在常量池中
         *
         */


    }

    /**
     * 字符串的拼接
     */
    @Test
    public void testStrInternV2() {
        String str1 = "str";
        String str2 = "ing";
        String str3 = "str" + "ing";
        String str4 = str1 + str2;
        String str5 = "string";
        System.out.println(str3 == str4); //false str3、str5是在常量池中创建,str4在堆中,类似new
        System.out.println(str3 == str5);//true
        System.out.println(str4 == str5); //false
    }
}

4、本地直接内存溢出

package com.cmh.concurrent.jvm;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * Author: 起舞的日子
 * Date:2021/5/16 5:20 下午
 * <p>
 * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
 */
public class DirectMemoryOOM {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws IllegalAccessException {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }

    /**
     * 未出现OOM.....
     *
     */
}

这个没出现OOM。

好了,今天就到这里吧。