排查OOM问题

OOM可能发生在哪,如何排查,如何调优

参考回答:
除了程序计数器不会抛出OOM外,其他各个内存区域都可能会抛出OOM。
最常见的OOM情况有以下三种:
•    java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
•    java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
•    java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。
OOM分析--heapdump
要dump堆的内存镜像,可以采用如下两种方式:
•    设置JVM参数-XX:+HeapDumpOnOutOfMemoryError,设定当发生OOM时自动dump出堆信息。不过该方法需要JDK5以上版本。-XX:HeapDumpPath=/Users/weihuaxiao/Desktop/dump/  生成DUMP文件的路径
•    使用JDK自带的jmap命令。"jmap -dump:format=b,file=heap.bin "   其中pid可以通过jps获取。
dump堆内存信息后,需要对dump出的文件进行分析,从而找到OOM的原因。常用的工具有:
•    mat: eclipse memory analyzer, 基于eclipse RCP的内存分析工具。
•    jhat:JDK自带的java heap analyze tool,可以将堆中的对象以html的形式显示出来,包括对象的数量,大小等等,并支持对象查询语言OQL,分析相关的应用后,可以通过http://localhost:7000来访问分析结果。不推荐使用,因为在实际的排查过程中,一般是先在生产环境 dump出文件来,然后拉到自己的开发机器上分析,所以,不如采用高级的分析工具比如前面的mat来的高效。

常见OOM异常

java.lang.StackOverflowError
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

全局JVM参数设置

JVM参数设置
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5M


public class OOM {

    public static void main(String[] args) {
        stackOverFlow();
    }
    
    public static void stackOverFlow(){
        stackOverFlow();
    }
}

运行结果

抛出java.lang.StackOverflowError异常

栈溢出

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

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;
如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。

栈溢出原因

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

解决方案

查找关键报错信息,确定是StackOverflowError还是OutOfMemoryError
如果是StackOverflowError,检查代码是否递归调用方法等
如果是OutOfMemoryError,检查是否有死循环创建线程等,通过-Xss降低的每个线程栈大小的容量


public class OOM {

    static final int M = 1024*1024;

    static List byteList = new ArrayList<>();
    
    public static void main(String[] args) {
        javaHeapSpace();
    }

    public static void javaHeapSpace(){
        while (true){
            Byte[] bytes = new Byte[M];
            //在堆中无限创建对象
            byteList.add(bytes);
        }
    }
    
}

运行结果抛出

java.lang.OutOfMemoryError: Java heap space 异常

Java 堆溢出

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

Java 堆溢出原因

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

解决方案

1.查找关键报错信息,如
java.lang.OutOfMemoryError: Java heap space

2.使用内存映像分析工具(如Eclipsc Memory Analyzer或者Jprofiler)对Dump出来的堆储存快照进行分析,分析清楚是内存泄漏还是内存溢出。

3.如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,修复应用程序中的内存泄漏。

4.如果不存在泄漏,先检查代码是否有死循环,递归等,再考虑用 -Xmx 增加堆大小。


public class OOM {

    static final int M = 1024*1024;

    public static void main(String[] args) {
       directBufferMemory();
    }
    
    public static void directBufferMemory(){
        ByteBuffer byteBuffer = 
        // 在机器内存上直接分配
        ByteBuffer.allocateDirect(M*8);
    }
}

运行结果抛出

java.lang.OutOfMemoryError: Direct buffer memory

ByteBuffer分配8MB直接内存,而JVM参数-XX:MaxDirectMemorySize=5M指定最大是5M,因此发生直接内存溢出。

本机直接内存溢出

直接内存并不是虚拟机运行时数据区的一部分,也不是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 是否合理。


public class OOM {

    public static void main(String[] args) {
        unableToCreateNewNativeThread();
    }

    public static void unableToCreateNewNativeThread(){
        while (true){
            new Thread(()->{
                try {
                    Thread.sleep(Integer.MAX_VALUE);
                }catch (Exception e){

                }
            }).start();
        }
    }
   
}

运行结果抛出

java.lang.OutOfMemoryError: unable to create new native thread

排查代码,确定是否显示使用死循环创建线程

栈溢出

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

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;
如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。

栈溢出原因

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

解决方案

查找关键报错信息,确定是StackOverflowError还是OutOfMemoryError
如果是StackOverflowError,检查代码是否递归调用方法等
如果是OutOfMemoryError,检查是否有死循环创建线程等,通过-Xss降低的每个线程栈大小的容量


public class OOM {

    static int i = 0;
    static List list = new ArrayList<>();
    public static void main(String[] args) {
       gCOverHeadLimitExceeded();
    }

    public static void gCOverHeadLimitExceeded(){
        while (true){
            list.add(String.valueOf(i++).intern());
        }
    }
    
     /**
     * 实例代码使用了newFixedThreadPool线程池,它使用了无界队列,无限循环执行任务,会导致内存飙升。
     * 因为设置了堆比较小,所以出现此类型OOM。
     */
    public static void gCOverHeadLimitExceededByThreadPool(){
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        while (true){
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(Integer.MAX_VALUE);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

运行结果抛出

java.lang.OutOfMemoryError: GC overhead limit exceeded

GC overhead limit exceeded

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

解决方案

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


public class OOM {

    static class MataSpace {

    }

    public static void main(String[] args) {
        metaSpaceOOM();
    }

    public static void metaSpaceOOM() {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(MataSpace.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(o, objects);
                }
            });
            enhancer.create();

        }
    }
}

运行结果抛出

org.springframework.cglib.core.CodeGenerationException: java.lang.OutOfMemoryError-->Metaspace

方法区溢出

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

方法区溢出原因

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

解决方案

检查是否永久代空间设置得过小
检查代码是否频繁错误得使用String.intern方法
检查是否跟jsp有关。
检查是否使用CGLib


参考链接

https://zhuanlan.zhihu.com/p/95150243

你可能感兴趣的:(排查OOM问题)