2

美团面试:说说OOM三大场景和解决方案? (绝对史上最全) - 江-小北

 6 months ago
source link: https://www.cnblogs.com/jiang-xiao-bei/p/18031815
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

首先,咱们先聊聊,什么是OOM?

小伙伴们,有没有遇到过程序突然崩溃,然后抛出一个OutOfMemoryError的异常?这就是我们俗称的OOM,也就是内存溢出。简单来说,就是你的Java应用想要的内存超过了JVM愿意给的极限,就会抛出这个错误。
那么为什么会出现OOM呢?一般都是由这些问题引起:

  1. 分配过少:JVM 初始化内存小,业务使用了大量内存;或者不同 JVM 区域分配内存不合理

  2. 内存泄漏:某一个对象被频繁申请,不用了之后却没有被释放,发生内存泄漏,导致内存耗尽(比如ThreadLocal泄露)

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

Java OOM的三大核心场景

oom3.png

场景一:堆内存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场景时,该如何解决呢?

twobuzhou.png

分析方法通常有两种:

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

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

类型一:在线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、实时监控内存使用情况

使用jvisualvmjconsole等工具可以实时监控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 <pid>

其中<pid>是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 <pid>
    

    其中<pid>是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)作为示例,分析流程如下:

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

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

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

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

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

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

下面给大家提供一份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)

图中红色箭头所指就是元空间

jvm.png

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

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

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

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

元空间OOM的现象

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

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

  1. 使用大量动态生成类的框架(如某些ORM框架、动态代理技术、热部署工具等)

  2. 程序代码中大量使用反射,反射在大量使用时,因为使用缓存的原因,会导致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 {
        // 这里可以是一些业务方法
    }
}

元空间(Metaspace) OOM 解决办法:

  1. 减少程序中反射的大量使用

  2. 做好熔断限流措施,对应用做好过载保护,比如阿里的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;
        }
    }
}

堆外内存的原因

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

堆外内存OOM的解决方案

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

最后说一句(求关注,求赞,别白嫖我)

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。

这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软](程序员江小北的笔记-江小北的资料馆 (aijiangsir.com))

本文,已收录于,我的技术网站 [aijiangsir.com](程序员江小北的笔记-江小北的资料馆 (aijiangsir.com)),有大厂完整面经,工作技术,架构师成长之路,等经验分享

求一键三连:点赞、分享、收藏

点赞对我真的非常重要!在线求赞,加个关注我会非常感激!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK