OOM分析与定位

OOM 即 out of memory(内存溢出),当程序运行时所需要的内存超出了JVM管理的最大内存就会导致内存溢出。

经常与内存溢出一起被提及的概念还有内存泄漏(memory leak),内存泄漏一般是指资源应释放而未释放,内存应回收而未回收,造成系统资源或内存浪费。当内存泄漏累计之后,资源和内存会被逐渐耗光,导致程序运行越来越慢,可能会导致OOM,甚至程序奔溃。

当内存溢出发生时,会抛出OutOfMemoryError或者StackOverflowError异常。但是当内存泄漏发生时,若还有资源和内存可用,则不会出现异常。

㈠ JVM堆内存溢出

堆内存溢出时抛出的异常是 java.lang.OutOfMemoryError:Java heap space。对象基本都是在堆中分配内存,所以导致堆内存溢出的原因极有可能与对象有关。

⑴ 可能导致堆内存溢出的原因

① 堆内存分配得不合理。堆内存配置的太小,或者新生代与老年代的比例配置得不合理,导致创建对象时没有内存空间可以分配。
② 存在大对象。例如大数组、大字符串、Map、集合等。
③ 大量的静态属性,用作本地缓存的Map、List等。
④ 频繁创建对象。例如在循环中不停的创建对象。
⑤ 一次从数据库中读取大量的数据。
⑥ 数据/流量峰值,程序没有做限流,数据量突然激增,导致内存不足。
⑦ 存在内存泄漏。导致内存泄漏的原因主要可以分为两类,一类是资源未释放,例如IO、数据库连接等;另一类是长生命周期的对象引用了短生命周期的对象,导致短生命周期的对象无法被回收,例如经常被提及的ThreadLocal内存泄漏问题。

堆内存溢出代码示例:

public class HeapOomTest {
   private static final Logger LOGGER = LoggerFactory.getLogger(HeapOomTest.class);

   private static List<byte[]> CACHE = new ArrayList<>(1000);

   public static void main(String[] args) {
      for (int loopTimes = 1; loopTimes < 20; loopTimes++) {
         long cacheSize = ObjectSizeCalculator.getObjectSize(CACHE);
         LOGGER.info("第 {} 次循环前, 对象已占用 {} M 内存", loopTimes, cacheSize / 1024 / 1024);

         // 每次占用5M内存
         CACHE.add(new byte[5 * 1024 * 1024]);
      }
   }
}

使用IDEA启动的时候,设置JVM的堆内存最大值为64M,如下图:
在这里插入图片描述
执行结果:
在这里插入图片描述
在第10次循环时,CACHE对象已占用了45M内存,再次创建byte数组申请5M内存时抛出OOM异常,可以看出,这个程序运行时,占用了近20M的堆内存。

⑵ 堆内存溢出的问题定位

定位问题,首先要找到java进程的ID(PID),使用 jps -vl 命令即可找到java进程的PID和启动时设置的jvm参数。
① 针对第一种场景,可以使用 jinfo PID 命令查询jvm的配置参数,检查内存配置是否合理。
在这里插入图片描述
如果确实是内存配置的太小了,则可以调整-Xms-Xmx参数,然后通过压力测试将这两个参数调整到最优值。

② 如果是其它场景,即对象占用了大量的内存,导致无法为新对象分配内存。此时需要使用 jmap 和 MAT 等工具进行分析。主要是要找出占用内存最多的对象。
直接使用 jmap -histo:live PID 命令查看哪个类型的对象占用了大量的内存。
在这里插入图片描述
或者使用jmap -dump:live,format=b,file=jvm_heapLive.hprof PID命令将堆中的对象信息导出,然后使用MAT等工具进行分析。
在这里插入图片描述
MAT工具分析dump文件:
在这里插入图片描述
在这里插入图片描述
找到了占用内存多的对象后,就可以查看代码,确定问题了。

在JVM启动的时候可以添加下面两个参数,开启发生OOM时,会自成生成dump文件:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/dump

MAT下载地址:https://www.eclipse.org/mat/downloads.php

编程时建议不要使用大对象,例如读取文件时,不要一次性读取完成再处理,可以读一部分处理一部分;处理数据库中的大量数据时,分批进行处理。
使用完的资源及时释放,尽可能的不要使用Map、List等静态属性做本地缓存。
做好限流、消峰,避免突增的流量和数据造成程序奔溃。

㈡ 元空间内存溢出

元空间和永久代都是JVM规范中方法区的实现,当内存不足时,也会抛出java.lang.OutOfMemoryError: Metaspace/PremGen space异常。永久代是JDK1.7及以前的内存区域,从JDK1.8开始用元空间替换了永久代,所以主要分析看一下元空间内存溢出的问题。元空间主要存储的是类型信息(class)和JIT编译后的代码,所以元空间的内存溢出主要与加载的class有关。

可能导致元空间内存溢出的原因及解决方法:
① 元空间的内存分配的太小。这种情况应该是见得最多的,处理方式与堆内存一样,调整适当的调整 -XX:MetaspaceSize-XX:MaxMetaspaceSize 两个参数。
② 加载了太多太多的类,例如引入了大量的第三方jar包,程序本身也有非常多的类等等。当出现这种情况,应尽量减少依赖的jar包,将单个服务拆分多个。
③ 程序运行时动态的生成class,例如CGLIB动态生成class。所有当有动态生成class时,要做好压力测试,将元空间内存设置在一个适合的范围。
④ 还想到一种情况没有验证过,就是自定义的类加载器比较多,每个类加载器都重复加载相同的类。

CGLIB动态生成class导致OOM的示例代码:

public class MetaspaceOomTest {
    private static final Logger LOGGER = LoggerFactory.getLogger(MetaspaceOomTest.class);

    public static void main(String[] args) {
        ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(MetaspaceOomTest.class);
            enhancer.setCallbackTypes(new Class[]{MethodInterceptor.class, InvocationHandler.class});
            enhancer.setCallbackFilter(new CallbackFilter() {
                @Override
                public int accept(Method method) {
                    return 0;
                }

                @Override
                public boolean equals(Object obj) {
                    return super.equals(obj);
                }
            });

            Class clazz = enhancer.createClass();
            LOGGER.info("CGLIB class : {}, TotalLoadedClassCount : {}", clazz.getName(), classLoadingMXBean.getTotalLoadedClassCount());
        }
    }
}

将元空间设置得比较小,方便快速出现OOM:
在这里插入图片描述
执行结果:
在这里插入图片描述

㈢ 栈内存溢出

每个方法在执行时都会生成一个栈帧,栈帧中保存了当前操作数、局部变量、返回地址等等一些信息,栈帧都会被纳入栈。当一个线程执行的方法太多就会出现栈内存溢出 java.lang.StackOverflowError,例如递归调用。可以通过参数-Xss适当的调整栈的大小。

栈内存溢出的示例代码:

public class StackOverflowTest {
    private static final Logger LOGGER = LoggerFactory.getLogger(StackOverflowTest.class);

    public static void main(String[] args) {
        StackOverflowTest stackOverflowTest = new StackOverflowTest();
        LongAdder longAdder = new LongAdder();
        stackOverflowTest.callMethod(longAdder);
    }

    public void callMethod(LongAdder longAdder) {
        longAdder.increment();
        LOGGER.info("第 {} 次调用", longAdder.longValue());
        this.callMethod(longAdder);
    }
}

将桟内存设置的足够小:
在这里插入图片描述
执行结果:
在这里插入图片描述

OOM的场景还有很多,例如:
直接内存溢出,java.lang.OutOfMemoryError: Direct buffer memory
GC回收超时引发的内存溢出,java.lang.OutOfMemoryError: GC overhead limit exceeded
压缩类空间内存溢出,java.lang.OutOfMemoryError: Compressed class space
。。。。。。

  • 0
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值