「JVM 内存管理」OutOfMemoryError 异常

《Java 虚拟机规范》规定,除了程序计数器,其他几个运行时数据区都有可能 OOM;OOM 与 VM 本身实现细节密切相关,而非 Java 语言约定的公共行为;

1. Java Heap 溢出

OutOfMemoryError:Java heap space,Java Heap 用于存储对象实例,只要有足够的对象没有被 GC 清除,总会触及 Heap 上限;(-Xmx 参数决定);

VM Arguments 设置

# -Xms 表示堆的最小值
# -Xmx 表示堆的最大值
# -XX:+HeapDumpOnOutOfMemoryError 表示打开开关:VM 出现 OOM 时 Dump 当前内存堆转储快照
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

示例代码

循环 new OOMOjbect 对象,直到用完 -Xmx 设置的内存,从而触发堆不够用的异常;

public class HeapOOM {
    static class OOMObject {
    }

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

运行结果

触发 OutOfMemoryError: Java heap space,并 dump 下快照文件 java_pid34676.hprof

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid34676.hprof ...
Heap dump file created [27927927 bytes in 0.092 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 edu.aurelius.jvm.oom.HeapOOM.main(HeapOOM.java:17)

Process finished with exit code 1

Dump 分析

可以通过 JDK 自带的 Virtual VM 对程序内存进行查看和分析,从而发现一些明显占用内存较多的对象,判断是否就是导致 OOM 的对象;

heap-oom

分析 OOM 问题时可以从两方面考虑:内存泄漏(Memory Leak,导致 OOM 的对象本该被 GC 回收,因一些程序编写错误导致无法被回收掉)、内存溢出(Memory Overflow,导致 OOM 的对象确实无法被 GC 回收);

Eclipse Memory Analyzer 的 Leak Suspect 功能可以更方便的帮助开发者找到内存泄漏的可疑对象;如下图所示就是发现了我们故意循环 new 出的 HeapOOM$OOMObject 对象(点击 detail 可见 GC Roots 引用链);

heap-oom-leak-suspect

根据 GC Roots 引用链,找到破坏引用路径的引用从而消除内存泄漏问题;

若分析得出导致 OOM 的对象是有必要存在的,则需要从资源层面(如 JVM 堆参数 -Xmx、-Xms,物理可用内存)和代码层面(存储结构、对象生命周期、引用关系时间长短等)进行扩展或优化了;

2. Java VM Stack 与 Native Method Stack 溢出

StackOverflowError,HotSpot VM 并不区分虚拟机栈和本地方法栈,对 HotSpot 来说,-Xoss(设置本地方法栈大小的参数)实际是无效的,栈容量只能通过 -Xss 参数来设置;

《Java 虚拟机规范》描述了两种栈溢出异常:

  • StackOverflowError,线程请求的栈深度大于 VM 所运行的最大深度(Xss 允许的内存大小用尽);
  • OutOfMemoryError,当 VM 允许动态扩展栈内存,而扩展栈时无法申请到足够的内存(进程最大内存减去 Heap 最大容量、方法区最大容量、直接内存大小、JVM 本身消耗内存、程序计数器大小,剩下的就是给到所有栈的);
演示 1: 限制栈容量

VM Arguments 设置

# 设置 VM Stack 大小;
-Xss160k

不同版本的 JVM 和不同操作系统的栈容量最大大小限制可能不一样,若设置的大小小于限制,会得到如下提示信息:

Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

The stack size specified is too small, Specify at least 160k

Process finished with exit code 1

示例代码

运行一段无限递归的代码,用于触发栈容量不够用的异常;

public class JVMStackSOF {
    private int stackLength = 1;

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

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

运行结果

stack length:774
Exception in thread "main" java.lang.StackOverflowError
	at edu.aurelius.jvm.oom.JVMStackSOF.stackLeak(JVMStackSOF.java:11)
	at edu.aurelius.jvm.oom.JVMStackSOF.stackLeak(JVMStackSOF.java:12)
  ...
	at edu.aurelius.jvm.oom.JVMStackSOF.stackLeak(JVMStackSOF.java:12)
	at edu.aurelius.jvm.oom.JVMStackSOF.main(JVMStackSOF.java:18)

Process finished with exit code 1

体现了栈容量太小触发 StackOverflowError;

演示 2: 定义大量本地变量

示例代码

在代码中添加 100 个本地变量,然后进行无限递归,触发栈容量不够用的异常;

public class JVMStackSOF {
    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) {
        JVMStackSOF oom = new JVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

运行结果

stack length:41
Exception in thread "main" java.lang.StackOverflowError
	at edu.aurelius.jvm.oom.JVMStackSOF.stackLeak(JVMStackSOF.java:12)
	at edu.aurelius.jvm.oom.JVMStackSOF.stackLeak(JVMStackSOF.java:13)
  ...
	at edu.aurelius.jvm.oom.JVMStackSOF.stackLeak(JVMStackSOF.java:13)
	at edu.aurelius.jvm.oom.JVMStackSOF.main(JVMStackSOF.java:20)

Process finished with exit code 1

体现了栈帧过大触发 StackOverflowError;与 示例 1 对比可见,当栈容量不变,栈帧变大时,栈的深度(stack length)明显变小了;

如果是可以动态扩展栈大小的虚拟机(比如 Classic VM),可能会导致 OutOfMemoryError 而非 StackOverflowError 的结果;

演示 3: 线程过多

VM Arguments 设置

# 将 VM Stack 大小设置得大一些;
-Xss2M

示例代码

不断建立不会终止的线程(可能会导致操作系统死机,谨慎操作),把 JVM Stack 和 Native Method Stack 可支配的内存耗尽,从而触发无法创建本地线程的异常;

public class JVMStackSOF {
    private void dontStop() {
        while (true) {
        }
    }

    public void stackLeakByThread() {
        while (true) {
            new Thread(() -> dontStop()).start();
        }
    }

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

运行结果

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

如果建立过多内存会导致内存溢出,而又无法减少线程数量或增加可用内存,可以从限制栈容量、减少堆最大大小的方向考虑优化 unable to create native thread

3. Method Area 与 Runtime Constant Pool 溢出

使用永久代元空间来实现方法区,对方法区内存溢出表现是有影响的;

演示 1: 限制永久代容量

VM Arguments 设置

# 设置 Java 堆永久代大小为 6 M;
-XX:PermSize=6M -XX:MaxPermSize=6M

示例代码

通过持续往常量池添加 i 的字符串常量,用光 6MB 的永久代,从而触发永久代空间不足的异常;

public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        short i = 0;
        while (true) {
            set.add(String.valueOf(i++).intern());
        }
    }
}

运行结果

JDK 6 下的运行结果如下:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at edu.aurelius.jvm.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 15)

而在 JDK 7 或更高的版本中(无论是添加 -XX:MaxPermSize 限制,还是添加 JDK 8 才有的 -XX:MaxMetaspaceSize 限制)循环将会持续进行下去,直至 Heap 溢出(永久代的字符串常量池被移至 Java Heap);可能是常量分配导致 Heap 溢出,也可能是 Set 扩容导致 Heap 溢出;

示例 2: String.intern() 影响

String.intern() 调用的作用是将首次遇到的字符串放入字符串常量池;在 JDK 6 中,是将字符串复制一份到永久代的字符串常量池存储起来,并返回该字符串实例的引用;在 JDK 7 后,是将字符串实例的引用记录到堆中字符串常量池中;

示例代码

通过 StringBuilder::new 实例化两个字符串,并通过 String.intern() 调用获取字符串与原字段进行比较;

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);

运行结果

JDK 6: false(永久代常量对象引用 vs. 堆对象引用);false(永久代常量对象引用 vs. 堆对象引用);
JDK 7: true(堆对象引用 vs. 堆对象引用);false(堆对象引用 vs. 堆对象引用);

示例 3: 模拟方法区溢出

VM Arguments 设置

# 设置 Java 堆永久代大小为 10 M - JDK 7
-XX:PermSize=10M -XX:MaxPermSize=10M
# 设置元空间大小为 10 M - JDK 8
-XX:MetaspaceSize=10485760 -XX:MaxMetaspaceSize=10485760
# -XX:MinMetaspaceFreeRatio:GC 之后控制最小元空间剩余容量的百分比,减少 GC 频率;
# -XX:Max-MetaspaceFreeRatio:GC 之后控制最大云空间剩余容量的百分比;

-XX:MetaspaceSize 即元空间初始容量,当方法区使用达到该值会触发 GC 进行类型卸载,同时会调整该值;若释放大量空间,则降低该值,若释放少量空间,则在不超过 -XX:MaxMetaspaceSize(默认 -1,表示不限制)的情况下提高该值;

示例代码

借助 CGLib 直接操作字节码运行时生成大量动态代码,从而触发方法区内存溢出异常;

public class JavaMethodAreaOOM {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
            enhancer.create();
        }
    }

    static class OOMObject {
    }
}

运行结果

JDK 7 运行提示永久代溢出

Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632) at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
...

JDK 8 运行提示元空间溢出

Caused by: java.lang.OutOfMemoryError: Metaspace
...

4. Direct Memory 溢出

VM Arguments 设置

# 设置 Java 堆最大 20 M,直接内存最大 10 M;
-Xmx20M -XX:MaxDirectMemorySize=10M

若不指定 -XX:MaxDirectMemorySize,直接内存容量将与 Java Heap 容量一致;

示例代码

代码通过反射获取 Unsafe 实例直接进行内存分配(越过了 DirectByteBuffer 类,DirectByteBuffer 分配内存时,通过计算得知内存不足就会抛出溢出异常,不会真正向操作系统申请内存),从而触发直接内存不够的异常;

public class DirectMemoryOOM {
    private static final int _5MB = 1024 * 1024 * 5;

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

运行结果

[谨慎操作] -XX:MaxDirectMemorySize=10M 的限制只对 ByteBuffer.allocateDirect(size)new DirectByteBuffer(capacity) 的分配方式有效,对 unsafe.allocateMemory(size) 并无限制,如上代码会持续分配内存直至让操作系统死机;

若使用 ByteBuffer.allocateDirect(size) 方法申请直接内存,可得到如下结果:

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:695)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at edu.aurelius.jvm.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20)

5. GC 频繁

OutOfMemoryError:GC overhead limit exceeded 当超过 98% 的时间在 GC 时,也会抛出 OOM 异常;


上一篇:「JVM 内存管理」HotSpot VM 对象概要
下一篇:「JVM 内存管理」GC 可回收的对象

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!


参考资料:

  • [1]《深入理解 Java 虚拟机》
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aurelius-Shu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值