欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

荐 不能不知道的OOM异常分析以及解决方案

程序员文章站 2022-03-26 21:25:25
在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区都会发生OOM异常的可能,本文通过几个例子来了解一下虚拟机常见的OOM异常。本文的代码参考《深入理解Java虚拟机(第二版)》Java堆溢出Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免避免垃圾回收清除对象,那么这些对象达到最大堆的容量限制之后就会产生内存溢出异常。设置VM参数,方便导出dump文件分析-Xms20m //JVM初始分配的内存20m-Xmx20m...

在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区都会发生OOM异常的可能,本文通过几个例子来了解一下虚拟机常见的OOM异常。

本文的代码参考《深入理解Java虚拟机(第二版)》

Java堆溢出

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

  • 设置VM参数,方便导出dump文件分析
-Xms20m  //JVM初始分配的内存20m
-Xmx20m  //JVM最大可用内存为20m
-XX:+HeapDumpOnOutOfMemoryError  //当JVM发生OOM时,自动生成DUMP文件
-XX:HeapDumpPath=/Users/wangchengming/Desktop/  //生成DUMP文件的路径,方便后面进行分析

如果不加-XX:HeapDumpPath-XX:+HeapDumpOnOutOfMemoryError参数的话,也可以通过jps命令找到pid,如下:
荐
                                                        不能不知道的OOM异常分析以及解决方案
然后使用jmap -dump:format=b,file=serviceDump.dat 2513命令手打导出dump文件,如下:
荐
                                                        不能不知道的OOM异常分析以及解决方案

  • 堆内存溢出代码
public class OOMTest {

    static class OOMObject {
    }

    public static void main(String[] args) {
        List<OOMObject> oomObjects = new ArrayList<>();
        while (true) {
            oomObjects.add(new OOMObject());
        }
    }
}
  • 运行结果如下:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to /Users/wangchengming/Desktop/java_pid2493.hprof ...
Heap dump file created [27779830 bytes in 0.088 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:265)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
	at java.util.ArrayList.add(ArrayList.java:462)
	at OOMTest.main(OOMTest.java:16)

Process finished with exit code 1
  • 使用JDK自带的VisualVM可视化工具导入dump文件

荐
                                                        不能不知道的OOM异常分析以及解决方案
可以看出一共创建了810326个对象,占用了99.4%的内存,发现内存被撑爆了。

  • 解决思路
    • 通过上面的分析可以得出是堆内存溢出。这个时候可以定位到代码:OOMTest$OOMOject,发现是死循环导致的,修改代码之后就不会出现堆内存溢出了
    • 或者可以检查虚拟机堆参数(-Xmx和-Xms)是否可以调大
    • 思考是不是存在GC Roots不可达的对象,如果存在GC Roots不可达的对象则是内存泄漏
虚拟机栈和本地方法栈溢出

通常情况下栈容量是由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机中描述了两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,则会抛出*Error
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则会抛出OOM

接下来演示一波*Error

  • 设置VM参数-Xss160k,最小是160k,默认1M
  • 编写代码
private int stackLength = 1;

/**
 * 递归
 */
public void stackLeak() {
    stackLength++;
    stackLeak();
}

public static void main(String[] args) {
    OOMTest oomTest = new OOMTest();
    try {
        oomTest.stackLeak();
    } catch (Throwable e) {
        System.out.println("stack length : " + oomTest.stackLength);
        throw e;
    }
}
  • 从运行结果来看,单线程下虚拟机抛出的是*Error
stack length : 773
Exception in thread "main" java.lang.*Error
	at OOMTest.stackLeak(OOMTest.java:23)
	at OOMTest.stackLeak(OOMTest.java:24)
	at OOMTest.stackLeak(OOMTest.java:24)
	at OOMTest.stackLeak(OOMTest.java:24)

接下来演示一波OOM

  • 设置VM参数-Xss2M
  • 编写代码
private void dontStop() {
    while (true) {
    }
}

private void stackLeakByThread() {
    while (true) {
        Thread thread = new Thread(this::dontStop);
        thread.start();
    }
}

public static void main(String[] args) {
    OOMTest oomTest = new OOMTest();
    oomTest.stackLeakByThread();
}
  • 运行结果
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
  • 解决思路
    • 通常在单线程下,栈帧太大,或者虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出*Error 异常。如果是这种情况,就需要仔细检查代码有没有深度递归的情况。
    • 不断地建立线程的方式会导致内存溢出。如果是这种情况就要检查是不是有死循环,或者通过-Xss降低的每个线程栈大小的容量
方法区和运行时常量池溢出

方法区,(又叫永久代,JDK8后,元空间替换了永久代),用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。运行时产生大量的类,会填满方法区,造成溢出。

  • 设置VM参数,-XX:MetaspaceSize=10M-XX:MaxMetaspaceSize=10M
  • 编写代码
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 {
}
  • 运行结果
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:538)
	at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
	at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:585)
	at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:131)
	at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319)
	at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:572)
	at org.springframework.cglib.proxy.Enhancer.create(Enhancer.java:387)
	at OOMTest.main(OOMTest.java:31)
  • 解决思路
    • 首先我们看到错误信息Caused by: java.lang.OutOfMemoryError: Metaspace,然后检查代码是否应用了大量的代理,发现确实是应用了代理。
    • 检查是否永久代空间设置得过小
    • 检查代码是否频繁错误得使用String.intern方法
    • 检查是否跟jsp有关。
    • 检查是否使用CGLib生成了大量的代理类
    • 重启大法,重启JVM
本机直接内存溢出

直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。但是,这部分内存也被频繁地使用,而且也可能导致OOM。

在JDK1.4 中新加入了NIO(New Input/Output)类,它可以使用 native 函数库直接分配堆外内存,然后通过一个存储在Java堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

DirectMemory容量可以通过-XX:MaxDirectMemorySize指定,如果不指定则默认和Java堆最大值(-Xmx指定)一致。

  • 设置VM参数-Xmx256m-XX:MaxDirectMemorySize=100M
  • 编写代码
public static void main(String[] args) throws IllegalAccessException, InterruptedException {
    //分配128MB直接内存
    ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);

    TimeUnit.SECONDS.sleep(10);
    System.out.println("ok");
}
  • 运行结果
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:694)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at OOMTest.main(OOMTest.java:85)
  • 解决思路
    • 检查代码是否恰当
    • 检查JVM参数-Xmx-XX:MaxDirectMemorySize是否合理。
总结

通过以上的分析,一共得出以下几种常见OOM异常,希望可以帮到各位小伙伴

java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: unable to create new native thread
java.lang.OutOfMemoryError: Metaspace
java.lang.OutOfMemoryError: Direct buffer memory

本文地址:https://blog.csdn.net/wangchengming1/article/details/107343013