[Java异常]OOM

小伙伴们,有没有遇到过程序突然崩溃,然后抛出一个OutOfMemoryError的异常?这就是我们俗称的OOM,也就是内存溢出。简单来说,就是你的Java应用想要的内存超过了JVM愿意给的极限,就会抛出这个错误。

那么为什么会出现OOM呢?一般都是由这些问题引起:

  • 分配过少:JVM 初始化内存小,业务使用了大量内存;或者不同 JVM 区域分配内存不合理
  • 内存泄漏:某一个对象被频繁申请,不用了之后却没有被释放,发生内存泄漏,导致内存耗尽(比如ThreadLocal泄露)

接下来,我们来聊聊Java OOM的三大经典场景以及解决方案,保证让你有所收获!👍

Java OOM的三大核心场景

场景一:堆内存OOM(也叫堆内存溢出)#

这是最常见的OOM场景了,发生在JVM试图分配对象空间时,却发现剩余的堆内存不足以存储新对象。

例如我们执行下面的代码,就可以模拟出堆内存OOM的场景:

// 创建大量对象导致堆内存溢出
public class HeapOOM {
    static class OOMObject {
        // 假设这里有一些属性
    }

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

        while (true) {
            list.add(new OOMObject()); // 不断创建对象并添加到list中
        }
    }
}

那么当出现线上应用OOM场景时,该如何解决呢?

分析方法通常有两种:

  • 类型一:在线分析,属于轻量级的分析:

  • 类型二:离线分析,属于重量级的分析:

类型一:在线OOM分析:#

在线分析Java OOM(内存溢出)问题,通常涉及到监控运行中的Java应用,捕获内存溢出时的信息,分析堆转储(Heap Dump)文件,以及利用一些工具和命令来辅助定位问题。下面是一套详细的分析流程和命令,帮助你在线分析和解决Java OOM问题:

1、启用JVM参数以捕获Heap Dump

在Java应用启动命令中加入以下JVM参数,以确保在发生OOM时能自动生成堆转储文件:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/heapdump.hprof
这些参数的作用是:

-XX:+HeapDumpOnOutOfMemoryError:指示JVM在遇到OOM错误时生成堆转储文件。
-XX:HeapDumpPath:指定堆转储文件的存储路径,可以自定义路径和文件名。

2、实时监控内存使用情况
使用jvisualvm或jconsole等工具可以实时监控Java应用的内存使用情况。这些工具可以帮助你了解内存消耗的趋势,从而预测和避免OOM的发生。

JVisualVM:集成了多个JDK命令行工具,提供了可视化界面,可以监控内存使用、查看线程、分析堆等。
JConsole:Java监控和管理控制台,用于对JVM中的内存、线程和类等进行监控。

3、分析Heap Dump文件
当应用抛出OOM并且根据上述设置生成了堆转储文件后,使用Heap Dump分析工具来分析这个文件。常用的工具有:

Eclipse Memory Analyzer (MAT):一个强大的Java堆分析工具,可以帮助识别内存泄露和查看内存消耗情况。
VisualVM:除了监控功能外,也支持加载和分析Heap Dump文件。
在MAT中打开Heap Dump文件,主要关注以下几点:

查找内存中对象的分布,特别是占用内存最多的对象。
分析这些对象的引用链,确定是哪部分代码引起的内存泄漏或过度消耗。
检查ClassLoader,以确认是否有过多的类被加载导致的元空间(Metaspace)OOM。

4、使用命令行工具

JDK提供了一些命令行工具,如jmap,可以用来生成Heap Dump文件:

jmap -dump:live,format=b,file=heapdump.hprof
其中是Java进程的ID。-dump:live选项表示只转储活动对象,可以减小Heap Dump文件的大小。

5、分析日志和异常信息
最后,不要忽视应用的日志和抛出的异常信息。OOM之前的日志可能会提供一些导致内存溢出的操作或业务逻辑的线索。

类型二:离线OOM分析,这个属于重量级分析#

离线分析Java OOM(OutOfMemoryError)通常是在问题发生后,通过分析JVM生成的堆转储(Heap Dump)文件来进行。这个过程涉及到获取堆转储文件、使用分析工具进行深入分析和解读分析结果

1、获取Heap Dump文件
首先,确保你已经有了一个Heap Dump文件。这个文件可能是在JVM遇到OOM时自动生成的(如果启用了-XX:+HeapDumpOnOutOfMemoryError JVM参数),或者你可以在应用运行期间手动生成:

使用jmap命令生成Heap Dump文件:

jmap -dump:live,format=b,file=/path/to/heapdump.hprof
其中是Java进程的ID,/path/to/heapdump.hprof是你希望保存Heap Dump文件的位置。

2、使用Heap Dump分析工具

有了Heap Dump文件后,你需要使用专门的工具来进行分析。以下是一些常用的分析工具:

Eclipse Memory Analyzer (MAT):非常强大的内存分析工具,能帮助识别内存泄漏和查看内存消耗情况。
VisualVM:提供了一个可视化界面,可以用来分析Heap Dump文件。
JVisualVM:随JDK一起提供的工具,也支持加载Heap Dump文件进行分析。

3、分析Heap Dump文件

使用MAT(Eclipse Memory Analyzer)作为示例,分析流程如下:

  • 打开Heap Dump文件:启动MAT并打开Heap Dump文件(.hprof)。

  • 运行Leak Suspects Report:MAT可以自动生成一个内存泄漏报告(Leak Suspects Report),这个报告会指出可能的内存泄漏路径。

  • 分析Dominators Tree:这个视图显示了占用最多内存的对象及其引用。通过它,你可以找到最大的内存消耗者。

  • 查看Histogram:对象Histogram列出了所有对象的实例数和总大小,帮助你识别哪种类型的对象占用了最多的内存。

  • 检查GC Roots:为了确定对象为什么没有被垃圾回收,可以查看对象到GC Roots的引用链。

  • 分析引用链:通过分析对象的引用链,你可以确定是什么持有了这些对象的引用,导致它们无法被回收。

下面给大家提供一份Java应用上线前参考的的JVM配置(内存8G),以后系统上线前可以先配置下JVM,不要啥都不配置就上线了

-Xms6g -Xmx6g (按不同容器,4G及以下建议为50%,6G以上,建议设置为70%)
-Xmn2g    (以8G内存,年轻代可以设置为2G)
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-Xss256k
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:AutoBoxCacheMax=20000
-XX:+HeapDumpOnOutOfMemoryError (当JVM发生OOM时,自动生成DUMP文件)
-XX:HeapDumpPath=/usr/local/logs/gc/
-XX:ErrorFile=/usr/local/logs/gc/hs_err_%p.log (当JVM发生崩溃时,自动生成错误日志)
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/usr/local/heap-dump/

场景二:元空间(MetaSpace)OOM#

什么是元空间?

Java元空间(Metaspace)是Java虚拟机(JVM)中用于存放类的元数据的区域,从Java 8开始引入,替代了之前的永久代(PermGen)

元空间是方法区在HotSpot JVM 中的实现,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。

不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。理论上取决于32位/64位系统可虚拟的内存大小,可见也不是无限制的,需要配置参数。

元空间(Metaspace) 垃圾回收,会对僵死的类及类加载器的垃圾回收会进行回收,元空间(Metaspace) 垃圾回收的时机是,在元数据使用达到“MaxMetaspaceSize”参数的设定值时进行。

元空间OOM的现象

JVM 在启动后或者某个时间点开始,MetaSpace 的已使用大小在持续增长,同时每次 GC 也无法释放,调大 MetaSpace 空间也无法彻底解决。

元空间OOM的核心原因:生成了大量动态类

比如:
使用大量动态生成类的框架(如某些ORM框架、动态代理技术、热部署工具等)
程序代码中大量使用反射,反射在大量使用时,因为使用缓存的原因,会导致ClassLoader和它引用的Class等对象不能被回收
例如下面的生成大量动态代理类的代码示例,则会导致元空间的OOM

// 使用CGLIB动态生成大量类导致元空间溢出
public class MetaspaceOOM {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create(); // 动态生成类并加载
        }
    }

    static class OOMObject {
        // 这里可以是一些业务方法
    }
}

这段代码是一个示例,用来演示通过使用 CGLIB 动态生成大量类可能导致 Java 的元空间(Metaspace)溢出。以下是代码的解释:

  1. MetaspaceOOM 类包含了一个 main 方法,其中包含一个无限循环 while (true)

  2. 在循环内部,代码使用 CGLIB 库来生成新的类。CGLIB 是一个用于在运行时生成字节码并创建新类的库,通常用于动态代理和增强类的功能。

  3. 在每次循环迭代中,代码创建一个 Enhancer 对象,Enhancer 是 CGLIB 中用于增强类的工具类。

  4. 通过 enhancer.setSuperclass(OOMObject.class),指定了要生成类的父类为 OOMObject,这个类在代码中被定义为静态内部类 OOMObject

  5. enhancer.setUseCache(false) 表示不使用缓存,即每次都会生成新的类。

  6. 通过 enhancer.setCallback(...) 设置了一个 MethodInterceptor,用于在生成的类上设置回调方法,在这里回调方法只是简单地调用了原始方法。

  7. 最后,通过 enhancer.create() 方法动态生成类并加载到内存中。

由于代码中存在一个无限循环,每次循环迭代都会生成新的类,如果这个过程持续下去,将会导致 Metaspace 的使用量不断增加,最终耗尽 Metaspace,引发元空间溢出(Metaspace OOM)异常。

为避免这种情况,通常建议在动态生成类时,适当控制类的生成数量,以及确保及时释放不再需要的类,避免不必要的类加载和占用过多的 Metaspace 空间。

如果您有任何其他问题或需要更多详细信息,请随时告诉我。

元空间(Metaspace) OOM 解决办法:
减少程序中反射的大量使用

做好熔断限流措施,对应用做好过载保护,比如阿里的sentinel限流熔断中间件

场景三:堆外内存OOM#

Java对外内存(Direct Memory)OOM指的是Java直接使用的非堆内存(off-heap memory)耗尽导致的OutOfMemoryError。这部分内存主要用于Java NIO库,允许Java程序以更接近操作系统的方式管理内存,常用于高性能缓存、大型数据处理等场景

例如下面的代码,如何堆外内存太小,就会导致堆外内存的OOM:

// 分配大量直接内存导致OOM
import java.nio.ByteBuffer;

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

    public static void main(String[] args) {
        int count = 1;

        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(ONE_MB);
                count++;
            }
        } catch (Exception e) {
            System.out.println("Exception: instance created " + count);
            throw e;
        }
    }
}

直接内存

直接内存(Direct Memory)是一种在Java中可以使用的内存形式,它不受Java虚拟机(JVM)管理,而是由操作系统直接管理的一块内存区域。在Java中,可以使用 java.nio.ByteBuffer 类来分配和操作直接内存。直接内存通常用于需要更高性能的 I/O 操作,如网络编程或文件操作。

直接内存的优势包括:

  1. 零拷贝:直接内存可以在Java堆和本地内存之间进行零拷贝传输,提高了I/O操作的效率。

  2. 减少GC 压力:直接内存不受JVM的垃圾回收(GC)机制管理,因此可以减少GC 对性能的影响。

  3. 大内存分配:直接内存可以分配比堆内存更大的内存块,适合处理大数据量的场景。

  4. 直接内存的释放:尽管不受GC 管理,但需要注意手动释放直接内存,否则可能导致内存泄漏。

使用直接内存时需要注意以下事项:

  1. 手动释放:直接内存不受GC 管理,需要手动释放以避免内存泄漏。

  2. 性能考虑:虽然直接内存可以提高性能,但过度使用可能导致性能问题。

  3. 堆内存 vs 直接内存:在决定使用直接内存时,需要权衡其优势和缺点,并根据具体需求进行选择。

以下是一个简单的示例,演示如何在Java中使用直接内存:

import java.nio.ByteBuffer;

public class DirectMemoryExample {
    public static void main(String[] args) {
        int bufferSize = 1024;  // 1KB
        ByteBuffer directBuffer = ByteBuffer.allocateDirect(bufferSize);
        
        // 使用直接内存进行操作
        
        directBuffer.clear();   // 清空缓冲区
        
        directBuffer = null;    // 指定为null,方便垃圾回收
        
        // 需要手动释放直接内存
        // ((sun.nio.ch.DirectBuffer) buffer).cleaner().clean();
    }
}

请注意,释放直接内存需要调用特定的方法,具体方法取决于Java版本和实现。在实际使用中,请根据具体情况选择合适的释放方法。

如果您需要更多信息或有其他问题,请随时告诉我。

堆外内存的原因

  • 分配过量的直接内存:程序中大量使用DirectByteBuffer等直接内存分配方式,而没有相应的释放机制,导致内存迅速耗尽,常见于NIO、Netty等相关组件。
  • 内存泄露:如果分配的直接内存没有被及时释放(例如,ByteBuffer未被回收),就可能发生内存泄露。
  • JVM对外内存限制设置不当:通过-XX:MaxDirectMemorySize参数控制对外内存大小,如果设置过小,可能无法满足应用需求。

堆外内存OOM的解决方案

  • 合理设置对外内存大小:根据应用的实际需求调整-XX:MaxDirectMemorySize参数,给予足够的直接内存空间。
  • 优化内存使用:减少不必要的直接内存分配,重用DirectByteBuffer等资源。
  • 内存泄露排查:使用工具(如VisualVM、JProfiler等)定位和解决内存泄露问题。
  • 代码优化:确保使用完直接内存后显式调用sun.misc.Cleaner.clean()或通过其他机制释放内存。

作者:Esofar

出处:https://www.cnblogs.com/DCFV/p/18351425

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   Duancf  阅读(88)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
· DeepSeek “源神”启动!「GitHub 热点速览」
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示