JVM 实战 OutOfMemoryError 异常

简介: JVM 实战 OutOfMemoryError 异常

Java 堆溢出


Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。


模拟代码


下面是简单的模拟堆内存溢出的代码:


/**
 * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 * @author zhengsh
 * @date 2021-8-13
 */
public class HeapOOM {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            list.add(new byte[2048]);
        }
    }
}


返回结果信息如下所示:


Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
 at cn.zhengsh.jvm.oom.HeapOOM.main(HeapOOM.java:16)


问题分析


我们需要定位是内存泄漏(Memory Leak)还是,内存溢出(Memory Overflow)


  1. 内存泄漏
  2. 内存溢出


内存泄漏


我们可以通过 jdk 自带的 jvisualvm 工具来加载堆快照文件进行分析。如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息 以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。


640.png


内存溢出


如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运 行期的内存消耗。


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


HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置 本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由-Xss参数来设定。关于虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两种异常:


  1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。


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


《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展,而HotSpot虚拟机 的选择是不支持扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现 OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法 容纳新的栈帧而导致StackOverflowError异常。


虚拟机栈内存溢出


StackOverflowError


示例代码:


/**
 * VM Args:-Xss128k
 *
 * @author zhengsh
 * @date 2021-08-13
 */
public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}


返回异常信息


Exception in thread "main" java.lang.StackOverflowError
stack length:992
 at cn.zhengsh.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
 at cn.zhengsh.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:14)
  //.... 省略更多


OutOfMemoryError


package cn.zhengsh.jvm.oom;
/**
 * @author zhengsh
 * @date 2021-08-13
 */
public class JavaVMStackSOF2 {
    private static int stackLength = 0;
    public static void test() {
        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++;
        test();
        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) {
        try {
            test();
        } catch (Error e) {
            System.out.println("stack length:" + stackLength);
            throw e;
        }
    }
}


输出结果:


stack length:6986
Exception in thread "main" java.lang.StackOverflowError
 at cn.zhengsh.jvm.oom.JavaVMStackSOF2.test(JavaVMStackSOF2.java:22)
 at cn.zhengsh.jvm.oom.JavaVMStackSOF2.test(JavaVMStackSOF2.java:22)


总结


无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候, HotSpot虚拟机抛出的都是StackOverflowError异常。可是如果在允许动态扩展栈容量大小的虚拟机上,相同代码则会导致不一样的情况。


创建线程导致内存溢出


注意:下面的这个实验可能导致操作系统卡死,建议大家在虚拟机中执行


/**
 * VM Args:-Xss512k
 *
 * @author zhengsh
 * @date 2021-08-13
 */
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) throws Throwable {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}


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


由于运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起进行。HotSpot从JDK 7 开始逐步“去永久代”的计划,并在JDK 8中完全使用元空间来代替永久代。


方法区内存溢出


方法区的主要职责是用于存放类型的相关信息,如类 名、访问修饰符、常量池、字段描述、方法描述等。对于这部分区域的测试,基本的思路是运行时产 生大量的类去填满方法区,直到溢出为止。虽然直接使用Java SE API也可以动态产生类(如反射时的 GeneratedConstructorAccessor和动态代理等),但在本次实验中借助了CGLib直接操作字节码运行时生成了大量的动态类。


/**
 * VM Args:-XX:MetaspaceSize=21m -XX:MaxMetaspaceSize=21m
 *
 * @author zhengsh
 * @date 2021-08-13
 */
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[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }
    static class OOMObject {
    }
}


输出代码


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


常量池案例


String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的 字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加 到常量池中,并且返回此String对象的引用。在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,我们可以通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量。


/**
 * @author zhengsh
 * @date 2021-08-13
 */
public class RuntimeConstantPoolOOM2 {
    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);
    }
}


这段代码在JDK 6中运行,会得到两个false,而在JDK 7中运行,会得到一个true和一个false。产生差异的原因是,在JDK 6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用,而由StringBuilder创建的字符串对象实例在 Java堆上,所以必然不可能是同一个引用,结果将返回 false。而JDK 7(以及部分其他虚拟机,例如JRockit)的intern()方法实现就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此intern()返回的引用和由StringBuilder创建的那个字符串实例就是同一个。而对str2比较返 回false,这是因为“java”这个字符串在执行String-Builder.toString()之前就已经出现过了,字符串常量 池中已经有它的引用,不符合intern()方法要求“首次遇到”的原则,“计算机软件”这个字符串则是首次出现的,因此结果返回true。


本机直接内存溢出


直接内存(Direct Memory)的容量大小可通过 -XX:MaxDirectMemorySize参数来指定,默认与Java堆最大值(由-Xmx指定)一致,代码越过了DirectByteBuffer类直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法指定只有引导类加载器才会返回实例,体现了设计者希望只有虚拟机标准类库里面的类才能使用Unsafe的功能,在JDK 10时才将Unsafe 的部分功能通过VarHandle开放给外部使用),因为虽然 DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配就会在代码里手动抛出溢出异常,真正申请分配内存的方法是 Unsafe::allocateMemory()。


/**
 * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
 *
 * @author zhengsh
 * @date 2021-08-13
 */
public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;
    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(_1MB);
        }
    }
}


输出内容:


Exception in thread "main" java.lang.OutOfMemoryError
Exception in thread "main" java.lang.OutOfMemoryError
 at java.base/jdk.internal.misc.Unsafe.allocateMemory(Unsafe.java:616)
 at jdk.unsupported/sun.misc.Unsafe.allocateMemory(Unsafe.java:462)
 at cn.zhengsh.jvm.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:21)
相关文章
|
1月前
|
NoSQL Java Redis
秒杀抢购场景下实战JVM级别锁与分布式锁
在电商系统中,秒杀抢购活动是一种常见的营销手段。它通过设定极低的价格和有限的商品数量,吸引大量用户在特定时间点抢购,从而迅速增加销量、提升品牌曝光度和用户活跃度。然而,这种活动也对系统的性能和稳定性提出了极高的要求。特别是在秒杀开始的瞬间,系统需要处理海量的并发请求,同时确保数据的准确性和一致性。 为了解决这些问题,系统开发者们引入了锁机制。锁机制是一种用于控制对共享资源的并发访问的技术,它能够确保在同一时间只有一个进程或线程能够操作某个资源,从而避免数据不一致或冲突。在秒杀抢购场景下,锁机制显得尤为重要,它能够保证商品库存的扣减操作是原子性的,避免出现超卖或数据不一致的情况。
64 10
|
7月前
|
监控 Java 调度
探秘Java虚拟机(JVM)性能调优:技术要点与实战策略
【6月更文挑战第30天】**探索JVM性能调优:**关注堆内存配置(Xms, Xmx, XX:NewRatio, XX:SurvivorRatio),选择适合的垃圾收集器(如Parallel, CMS, G1),利用jstat, jmap等工具诊断,解决Full GC问题,实战中结合MAT分析内存泄露。调优是平衡内存占用、延迟和吞吐量的艺术,借助VisualVM等工具提升系统在高负载下的稳定性与效率。
116 1
|
2月前
|
存储 IDE Java
实战优化公司线上系统JVM:从基础到高级
【11月更文挑战第28天】Java虚拟机(JVM)是Java语言的核心组件,它使得Java程序能够实现“一次编写,到处运行”的跨平台特性。在现代应用程序中,JVM的性能和稳定性直接影响到系统的整体表现。本文将深入探讨JVM的基础知识、基本特点、定义、发展历史、主要概念、调试工具、内存管理、垃圾回收、性能调优等方面,并提供一个实际的问题demo,使用IntelliJ IDEA工具进行调试演示。
54 0
|
3月前
|
监控 架构师 Java
JVM进阶调优系列(6)一文详解JVM参数与大厂实战调优模板推荐
本文详述了JVM参数的分类及使用方法,包括标准参数、非标准参数和不稳定参数的定义及其应用场景。特别介绍了JVM调优中的关键参数,如堆内存、垃圾回收器和GC日志等配置,并提供了大厂生产环境中常用的调优模板,帮助开发者优化Java应用程序的性能。
|
3月前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
75 2
|
6月前
|
运维 监控 Java
(十)JVM成神路之线上故障排查、性能监控工具分析及各线上问题排错实战
经过前述九章的JVM知识学习后,咱们对于JVM的整体知识体系已经有了全面的认知。但前面的章节中,更多的是停留在理论上进行阐述,而本章节中则更多的会分析JVM的实战操作。
156 1
|
6月前
|
缓存 监控 Java
Java虚拟机(JVM)性能调优实战指南
在追求软件开发卓越的征途中,Java虚拟机(JVM)性能调优是一个不可或缺的环节。本文将通过具体的数据和案例,深入探讨JVM性能调优的理论基础与实践技巧,旨在为广大Java开发者提供一套系统化的性能优化方案。文章首先剖析了JVM内存管理机制的工作原理,然后通过对比分析不同垃圾收集器的适用场景及性能表现,为读者揭示了选择合适垃圾回收策略的数据支持。接下来,结合线程管理和JIT编译优化等高级话题,文章详细阐述了如何利用现代JVM提供的丰富工具进行问题诊断和性能监控。最后,通过实际案例分析,展示了性能调优过程中可能遇到的挑战及应对策略,确保读者能够将理论运用于实践,有效提升Java应用的性能。 【
232 10
|
6月前
|
Arthas 存储 监控
JVM内存问题之JNI内存泄漏没有关联的异常类型吗
JVM内存问题之JNI内存泄漏没有关联的异常类型吗
|
5月前
|
存储 监控 算法
深入解析JVM内部结构及GC机制的实战应用
深入解析JVM内部结构及GC机制的实战应用
|
6月前
|
缓存 算法 Java
JVM内存溢出(OutOfMemory)异常排查与解决方法
JVM内存溢出(OutOfMemory)异常排查与解决方法