OOM的原因及解决方案——java笔记

305 阅读2分钟

记得笔试的时候出过这个问题,“写出OOM的原因及解决方案”。我能记住的就只有几个,还记不牢,特地在网上搜了一下,然后记录一下,图是网上盗的,内容是看完后根据自己的理解写的。

image.png

首先我找到源码看到原因分两类

  1. 第一个构造方法没有参数,报错信息是虚拟机给的;
  2. 第二个构造方法是有给出信息参数的。

image.png

构造方法是有给出信息参数的

Direct buffer memory

这个信息来自java.nio.Bits.reserveMemory()函数,查看调用过程:java.nio.Bits.DirectByteBuffer的构造函数 -> java.nio.Bits.ByteBuffer.allocateDirect()分配函数,然后结合reserveMemory源码帮助记忆。

解决方法:

  1. 分配时内存不够引起的错误,因为Direct ByteBuffer的默认大小为64MB,可以修改启动参数-XX:MaxDirectMemorySize
  2. 添加了-XX:+DisableExplicitGC参数导致无法正常gc导致的,删掉就好;
  3. 机器的内存太小了,没法调大,那就升级内存;
  4. 可以通过 Arthas 等在线诊断工具,排查java.nio.Bits.ByteBuffer.allocateDirect()分配函数的调用情况;
  5. jlra.tryHandlePendingReference()调用了sun.misc.Cleaner.clean()来清除Direct buffer,可以用反射来手动调用清除;
  6. 检查使用nio的代码,毕竟这个函数在这个包用的最多。

static void reserveMemory(long size, int cap) {

    ...

    // 执行gc,这个可以删除启动参数-XX:+DisableExplicitGC
    System.gc();

    // 会递增延迟时间的重试循环,
    boolean interrupted = false;
    try {
        long sleepTime = 1;
        int sleeps = 0;
        while (true) {
            if (tryReserveMemory(size, cap)) {
                return;
            }
            // 9次到了就退出循环
            if (sleeps >= 9) {
                break;
            }
            // 挂起失败
            if (!jlra.tryHandlePendingReference()) {
                try {
                    Thread.sleep(sleepTime);
                    // 延迟时间是指数性增长的
                    sleepTime <<= 1;
                    sleeps++;
                } catch (InterruptedException e) {
                    interrupted = true;
                }
            }
        }

        // 9次都没重试成功就会抛出oom
        throw new OutOfMemoryError("Direct buffer memory");

    } finally {
        if (interrupted) {
            // don't swallow interrupts
            Thread.currentThread().interrupt();
        }
    }
    ...
}

Required array size too large

数组太大,找到报错的来源,有BufferedInputStream,Files,AbstractCollection,ConcurrentHashMap这几个类;

image.png

static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

if (n >= MAX_ARRAY_SIZE)
    throw new OutOfMemoryError("Required array size too large");

从源码上可见,原因是超出数组分配的最大长度,如果数组真的需要这么大的,那就得考虑切分数组了。

构造方法是没有参数的

Java heap space

堆内存不够了。往报错位置里面找,基本都能找到分配内存相关的代码:

ByteBuffer.allocateDirect(0);

ByteBuffer.allocate(size);

// arrayList扩容时最小的容量小于0
if (minCapacity < 0) // overflow
    throw new OutOfMemoryError();

解决办法:

  1. 调整jvm启动参数-Xmn -Xms -Xmx,比如-Xms64m -Xmx512m;
  2. 排查有没有大对象,或者内存泄露,如果是机器内存不够支持业务导致的,那就加机器或者限流降级。

GC overhead limit exceeded

这是gc回收时间占用98%以上,却回收不到2%的空间导致的。内存泄露得检查代码,特别是那些一直new对象的代码。解决方法和上面的差不多。

String a = "a" + "b";

while(true) {
    ...
    new Object();
    ...
}

Metaspace

元数据区的内存不够。可能是在启动的时候报错,重新部署的时候报错,运行时报错,原因是加载的类太多或者太大。

解决办法:

  1. 调整-XX:MaxMetaspaceSize
  2. 长期没有重启时进行了重新部署,可能会重复加载class,这种情况只要重新启动就好
  3. 运行时可能动态创建了大量class,而且这些class生命周期短,但是jvm默认不会删除这些class,只能设置-XX:+CMSClassUnloadingEnabled 和 -XX:+UseConcMarkSweepGC

Unable to create new native thread

无法再创建线程。可能是创建的线程的内存不足,或者创建的线程数太多,超过了系统的限制。

解决办法:

  1. 增加机器的内存;
  2. 假如代码中的线程是一直创建不回收的话,那就需要修改,使用线程池;
  3. 使用线程池还太多的话,可以减少线程池的线程数量;
  4. 降低堆内存heap space,把内存让给线程;
  5. -Xss减少线程的堆栈内存大小;

Out of swap space

虚拟内存溢出,虚拟内存是由物理内存和交换空间构成。原因是内存耗光,或者是某个程序内存泄露导致的。

解决办法:

  1. 内存不够大话,可以增加内存,或者代码分解部署到多台机子;
  2. 升级到64bit;

Kill process or sacrifice child

这是linux系统一种内核作业方式导致的,out of memory killer,它会个进程打分,然后内存不足的情况下优先杀死评分低的来释放内存。 解决方法:

  1. 分解代码部署到多个服务器;
  2. 调整out of memory killer的参数。

笔记就做到这里了,要是有什么记错的地方,记得提个醒哈