Java程序员必备:常见OOM异常分析

前言

放假这几天,温习了深刻理解Java虚拟机的第二章, 整理了JVM发生OOM异常的几种状况,并分析缘由以及解决方案,但愿对你们有帮助。html

Java 堆溢出

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

Java 堆溢出缘由

  • 没法在 Java 堆中分配对象
  • 应用程序保存了没法被GC回收的对象。
  • 应用程序过分使用 finalizer。

Java 堆溢出排查解决思路

  1. 查找关键报错信息,如
java.lang.OutOfMemoryError: Java heap space
复制代码
  1. 使用内存映像分析工具(如Eclipsc Memory Analyzer或者Jprofiler)对Dump出来的堆储存快照进行分析,分析清楚是内存泄漏仍是内存溢出。
  2. 若是是内存泄漏,可进一步经过工具查看泄漏对象到GC Roots的引用链,修复应用程序中的内存泄漏。
  3. 若是不存在泄漏,先检查代码是否有死循环,递归等,再考虑用 -Xmx 增长堆大小。

demo代码

package oom;

import java.util.ArrayList;
import java.util.List;

/**
 * JVM配置参数
 * -Xms20m    JVM初始分配的内存20m
 * -Xmx20m   JVM最大可用内存为20m
 * -XX:+HeapDumpOnOutOfMemoryError 当JVM发生OOM时,自动生成DUMP文件
 * -XX:HeapDumpPath=/Users/weihuaxiao/Desktop/dump/  生成DUMP文件的路径
 */
public class HeapOOM {
    static class OOMObject {
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        //在堆中无限建立对象
        while (true) {
            list.add(new OOMObject());
        }
    }
}

复制代码

运行结果

按照前面的排查解决方案,咱们来一波分析。spring

1.查找报错关键信息bash

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
复制代码

2. 使用内存映像分析工具Jprofiler分析产生的堆储存快照jsp

由图可得,OOMObject这个类建立了810326个实例,是属于内存溢出,这时候先定位到对应代码,发现死循环致使的,修复便可。函数

栈溢出

关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:工具

  • 若是线程请求的栈深度大于虚拟机所容许的深度,将抛出StackOverflowError 异常;
  • 若是虚拟机栈能够动态扩展,当扩展时没法申请到足够的内存时会抛出 OutOfMemoryError 异常。

栈溢出缘由

  • 在单个线程下,栈帧太大,或者虚拟机栈容量过小,当内存没法分配的时候,虚拟机抛出StackOverflowError 异常。
  • 不断地创建线程的方式会致使内存溢出。

栈溢出排查解决思路

  1. 查找关键报错信息,肯定是StackOverflowError仍是OutOfMemoryError
  2. 若是是StackOverflowError,检查代码是否递归调用方法等
  3. 若是是OutOfMemoryError,检查是否有死循环建立线程等,经过-Xss下降的每一个线程栈大小的容量

demo代码

package oom;

/**
 * -Xss2M
 */
public class JavaVMStackOOM {
    private void dontStop(){
        while(true){

        }
    }
    public void stackLeakByThread(){
        while(true){
            Thread thread = new Thread(new Runnable(){
                public void run() {
                    dontStop();
                }
            });
            thread.start();}
    }
    public static void main(String[] args) {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}
复制代码

运行结果

1.查找报错关键信息post

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
复制代码

2.肯定是建立线程致使的栈溢出OOM性能

Thread thread = new Thread(new Runnable(){
                public void run() {
                    dontStop();
                }
            });
复制代码

3.排查代码,肯定是否显示使用死循环建立线程,或者隐式调用第三方接口建立线程(以前公司,调用腾讯云第三方接口,上传图片,遇到这个问题)学习

方法区溢出

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

方法区溢出缘由

  • 使用CGLib生成了大量的代理类,致使方法区被撑爆
  • 在Java7以前,频繁的错误使用String.intern方法
  • 大量jsp和动态产生jsp
  • 应用长时间运行,没有重启

方法区溢出排查解决思路

  • 检查是否永久代空间设置得太小
  • 检查代码是否频繁错误得使用String.intern方法
  • 检查是否跟jsp有关。
  • 检查是否使用CGLib生成了大量的代理类
  • 重启大法,重启JVM

demo代码

package oom;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 *  jdk8以上的话,
 *  虚拟机参数:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M 
 */
public class JavaMethodAreaOOM {
    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 {
    }
}
复制代码

运行结果

1.查找报错关键信息

Caused by: java.lang.OutOfMemoryError: Metaspace
复制代码

2.检查JVM元空间设置参数是否太小

-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M 
复制代码

3. 检查对应代码,是否使用CGLib生成了大量的代理类

while (true) {
  ...
   enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object obj, Method method,
                                        Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
  }
复制代码

本机直接内存溢出

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

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

直接内存溢出缘由

  • 本机直接内存的分配虽然不会受到Java 堆大小的限制,可是受到本机总内存大小限制。
  • 直接内存由 -XX:MaxDirectMemorySize 指定,若是不指定,则默认与Java堆最大值(-Xmx指定)同样。
  • NIO程序中,使用ByteBuffer.allocteDirect(capability)分配的是直接内存,可能致使直接内存溢出。

直接内存溢出

  • 检查代码是否恰当
  • 检查JVM参数-Xmx,-XX:MaxDirectMemorySize 是否合理。

demo代码

package oom;

import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;

/**
 * -Xmx256m -XX:MaxDirectMemorySize=100M
 */
public class DirectByteBufferTest {
    public static void main(String[] args) throws InterruptedException{
        //分配128MB直接内存
        ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);

        TimeUnit.SECONDS.sleep(10);
        System.out.println("ok");
    }
}
复制代码

运行结果

ByteBuffer分配128MB直接内存,而JVM参数-XX:MaxDirectMemorySize=100M指定最大是100M,所以发生直接内存溢出。

ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);
复制代码

GC overhead limit exceeded

  • 这个是JDK6新加的错误类型,通常都是堆过小致使的。
  • Sun 官方对此的定义:超过98%的时间用来作GC而且回收了不到2%的堆内存时会抛出此异常。

解决方案

  • 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
  • 检查JVM参数-Xmx -Xms是否合理
  • dump内存,检查是否存在内存泄露,若是没有,加大内存。

demo代码

package oom;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * JVm参数 -Xmx8m -Xms8m
 */
public class GCoverheadTest {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            executor.execute(() -> {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    //do nothing
                }
            });
        }
    }
}

复制代码

运行结果

实例代码使用了newFixedThreadPool线程池,它使用了无界队列,无限循环执行任务,会致使内存飙升。由于设置了堆比较小,因此出现此类型OOM。

总结

本文介绍了如下几种常见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
java.lang.OutOfMemoryError: GC overhead limit exceeded
复制代码

但愿你们遇到OOM异常时,对症下药,顺利解决问题。同时,若是有哪里写得不对,欢迎指出,感激涕零。

参考与感谢

我的公众号

  • 若是你是个爱学习的好孩子,能够关注我公众号,一块儿学习讨论。
  • 若是你以为本文有哪些不正确的地方,能够评论,也能够关注我公众号,私聊我,你们一块儿学习进步哈。
相关文章
相关标签/搜索