OutOfMemoryError异常

文章参考《深入理解Java虚拟机》第3版

1、Java堆溢出

1.1、OutOfMemoryError示例

下面的代码运行环境是jre1.8

/**
 * Java堆用于存储对象实例,以下代码通过不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机智清楚这些对象,
 * 那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常
 * VM args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * -Xms:指定jvm堆的初始大小
 * -Xmx:指定jvm堆的最大值
 * -XX:+HeapDumpOnOutOfMemoryError:	当首次遭遇OOM(OutOfMemoryError)时导出此时堆中相关信息
 */
public class HeapOOM {

    static class OOMObject{

    }

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

//运行结果:出现Java堆内存溢出时,异常堆栈信息"java.lang.OutOfMemoryError"会跟随进一步提示"Java heap space"
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid15684.hprof ...
Heap dump file created [28701867 bytes in 0.096 secs]

1.2、解决Java堆内存区域异常的方法

1)先确认内存中导致OOM的对象是否是必要的,也就是分清楚是内存泄露(Memory Leak)还是内存溢出(Memory Overflow)

2)内存泄露可通过工具查看泄露对象到GC Roots的引用链,进而找出产生内存泄露的代码的具体位置

3)内存溢出则需要检查堆(-Xmx与-Xms)设置,与机器的内存比较,看看是否还有向上调整的空间

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

2.1、Java虚拟机规范描述的两种异常

1)线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError

2)如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常

2.2、StackOverflowError示例

​ 因HotSpot虚拟机不支持动态扩展,所以只会因为栈容量无法容量新的栈帧而导致StackOverflowError异常,不会出现因为扩展而导致OOM

2.2.1、通过减少栈内存容量实现栈溢出异常

下面的代码运行环境是jre1.8

/**
 * 使用-Xss参数减少栈内存容量
 * VM args:-Xss180k
 * 当出现以下提示时:修改-Xss大于或者等于108k即可
 * The stack size specified is too small, Specify at least 108k
 */
public class JavaVMStackXss {

    private int stackLength = 1;

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

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

//运行结果:抛出StackOverflowError
stack length:1002
Exception in thread "main" java.lang.StackOverflowError
	at oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:14)
	at oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15)
2.2.2、通过增加方法帧中本地变量表的长度实现栈溢出异常

下面的代码运行环境是jre1.8

public class JavaVMStackMethod {

    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, unused101, unused102, unused103, unused104, unused105, unused106, unused107, unused108, unused109, unused110;
        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 = unused101 = unused102 = unused103 = unused104 = unused105 = unused106 = unused107 = unused108 = unused109 = unused110 = 0;
    }

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

//运行结果:抛出StackOverflowError
stack length:3696
Exception in thread "main" java.lang.StackOverflowError
	at oom.JavaVMStackMethod.stackLeak(JavaVMStackMethod.java:10)
	at oom.JavaVMStackMethod.stackLeak(JavaVMStackMethod.java:10)

2.3、解决虚拟机栈和本地方法区栈异常的方法

​ 1)出现StackOverflowError异常时,会有明确的堆栈可供分析,根据堆栈信息定位问题所在

​ 2)如果使用HotSpot虚拟机默认参数,栈深度在大多数情况下到达1000-2000是完全没有问题的

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

3.1、运行时常量池溢出

​ 通过String::intern()本地方法来验证,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用

3.1.1、JDK1.6及以下版本运行时字符串常量池在方法区溢出

下面的代码运行环境是jre1.6

/**
 * JDK1.6及以下版本时,常量池在方法区中,通过-XX:PermSize和-XX:MaxPermSize设置非堆内存大小
 * VM args:-XX:PermSize=6M -XX:MaxPermSize=6M
 */
public class RuntimeConstantPoolOOMJRE6 {

    public static void main(String[] args) {
        //使用Set保持着常量池引用,避免Full GC回收常量池行为
        Set<String> set = new HashSet<String>();
        //在short范围内足以让6MB的PermSize产生OOM了
        short i = 0;
        while (true){
            set.add(String.valueOf(i++).intern());
        }
    }
}
//运行结果:在运行时常量池溢出时,在OutOfMemoryError异常后面跟随的提示信息是“PermGen space”(永久代)
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
        at java.lang.String.intern(Native Method)
        at oom.RuntimeConstantPoolOOMJRE6.main(RuntimeConstantPoolOOMJRE6.java:17)
3.1.2、JDK1.7及以上版本运行时字符串常量池在堆内存溢出

下面的代码运行环境是jre1.8

/**
 * JDK1.7及以上版本时,常量池在堆内存中,通过-Xmx参数来设置堆内存大小
 * VM args:-Xmx20M
 */
public class RuntimeConstantPoolOOMJRE8 {

    public static void main(String[] args) {
        //使用Set保持着常量池引用,避免Full GC回收常量池行为
        Set<String> set = new HashSet<String>();
        //在int范围内让20MB的堆内存产生OOM
        int i = 0;
        while (true){
            set.add(String.valueOf(i++).intern());
        }
    }
}
//运行结果:在运行时常量池溢出时,在OutOfMemoryError异常后面跟随的提示信息是“Java heap space”(Java堆内存)
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.HashMap.resize(HashMap.java:703)
	at java.util.HashMap.putVal(HashMap.java:662)
	at java.util.HashMap.put(HashMap.java:611)
	at java.util.HashSet.add(HashSet.java:219)
	at oom.RuntimeConstantPoolOOMJRE8.main(RuntimeConstantPoolOOMJRE8.java:18)

3.2、方法区溢出

​ 使用了CGLib直接操作字节码运行时生成大量的动态类来填满方法区的内存,依赖spring-core包

/**
 * -XX:MaxMetaspaceSize 设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小
 * -XX:MetaspaceSize 设置元空间的初始大小,以字节为单位
 * -XX:MinMetaspaceFreeRatio 作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率
 * 通过设置元空间的最大值来实现OutOfMemoryError
 * VM args:-XX:MaxMetaspaceSize=10M
 */
public class JavaMethodAreaOOMJRE8 {

    static class OOMObject{

    }

    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 o, Method method, Object[] objects, MethodProxy methodProxy)
                        throws Throwable {
                    return methodProxy.invokeSuper(o, objects);
                }
            });
            enhancer.create();
        }
    }
}
//运行结果:在运行时常量池溢出时,在OutOfMemoryError异常后面跟随的提示信息是“Metaspace”(元空间)
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:348)
	at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:467)
	at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:336)
	at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:492)
	at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114)
	at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
	at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
	at org.springframework.cglib.proxy.Enhancer.create(Enhancer.java:305)
	at oom.JavaMethodAreaOOMJRE8.main(JavaMethodAreaOOMJRE8.java:30)

4、本机直接内存溢出

​ 使用unsafe分配本机内存

public class DirectMemoryOOMJRE8 {

    public 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);
        }
    }
}
//运行结果
Exception in thread "main" java.lang.OutOfMemoryError
	at sun.misc.Unsafe.allocateMemory(Native Method)
	at oom.DirectMemoryOOMJRE8.main(DirectMemoryOOMJRE8.java:19)
posted on 2021-12-24 15:44  cxbks  阅读(86)  评论(0)    收藏  举报