OutOfMemoryError

Java开发一般会碰到经典的OOM,绝大多数情况是代码没写好导致内存没有被及时回收,我遇到的情况也不多,一般现在Web的应用都是难以出现,大多是CURD的模式,出现OOM十有八九是在循环体内创建的对象占用内存超大,又或者其他方式创建的对象过大,而且是GCROOTS能够达到的,那么就会出现OOM。

那么这些OOM有什么方法可以分析呢?

先创建一个OOM案例,最简单的,创建一个Map不断put对象

public class OOM {
    public static void main(String[] args) throws Exception {
        Map cache = new HashMap();
        for (int i = 0; i <  128; ++i) {
            cache.put(i, new byte[1024*1024]);
        }
    }
}

创建一个大概一个128M大小的Map,执行的时候分配128M的内存

java -Xmx128m OOM

之后出现OOM

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at OOM.main(OOM.java:7)

出现OOM,一般是重启啦,虽然出现OOM,但JVM还是在运行的,只不过没有响应,为了让其恢复原状,需要重启应用,但问题还是要解决。首先需要拿到Heap Dump,有两种方式

-XX:+HeapDumpOnOutOfMemoryError
jmap -dump:live,format=b,file=<filepath> <pid>

一个在程序运行前加个JVM参数,HeapDumpOnOutOfMemoryError,不同JVM的参数不同,我这个是一般人使用HotSpot的实现,就是OpenJDK里面的虚拟机。

在运行前加个参数

java -Xmx128m -XX:+HeapDumpOnOutOfMemoryError  OOM

之后Dump文件出现

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid74573.hprof ...
Heap dump file created [123587063 bytes in 0.238 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at OOM.main(OOM.java:7)

一个hprof文件就会出现,之后我们要分析之,以前不用这个参数,想要拿到现场Heap是很麻烦的,最好在生产环境中使用,总好过事后找(实际情况就不好说,你懂的)。

还有一种方式是在程序运行时,利用jmap命令获取,这是在你忘记加HeapDumpOnOutOfMemoryError命令使用的,先利用jps查找到相关程序比如我这里

 ~ jps                                                                                                   
77457 Jps
99587 BootLanguagServerBootApp
2066
99554 org.eclipse.equinox.launcher_1.5.700.v20200207-2156.jar
98855 org.eclipse.equinox.launcher_1.5.700.v20200207-2156.jar
77402 OOM

对上面的代码稍微改造

public class OOM {
    public static void main(String[] args) throws Exception {
        Map cache = new HashMap();
        for (int i = 0; i <  128; ++i) {
            Thread.sleep(500);
            cache.put(i, new byte[1024*1024]);
        }
    }
}

编译运行,然后在另一个窗口输入jmp

jmap -dump:live,format=b,file=`pwd`/77402.hprof 77402

也是能拿到Heap Dump文件

再者需要一些工具辅助,没有工具辅助,我觉得很难分析,再加上本来机会就不多,什么都不靠是很难。

一般使用有

VisualVM
JProfiler

VisualVM看内存占用和一些基本信息,JProfiler是分析一些Refrence和GC Roots,前者免费,后者商业还有点贵。

然后遵循的一些原则大概有以下三个

Metaspace/PermGen
瞄准Class对象
Heap space
瞄准占空间最大的对象
Path to GC Roots

我们先来看VisualVM吧,跑上面的程序,然后及时连接,VisualVM会识别正在运行的JVM,可以看到一些基本情况

他的作用就是这样,基本是分析基本情况,看一下Heap的使用,正常情况Heap的情况是由于GC上下波动,不是途中不断攀升的现象,最后造成OOM。

分析最后看JProfile,打开hprof文件,查看占用内存最大的对象,最重要是看Refrence和GC Root Path,一般OOM都是引用没有释放导致的。

Metaspace/PermGen,一般是对应ClassLoader,有些加载出现问题导致Class对象没有被正确释放,而Heap space则是创建了超大对象不断持有,最后我们要看一下他们的Path to GC Roots,JVM根据什么来判断对象是否还存在引用不能回收?没错它是定义了一些GC Roots,可以把它理解为根正苗红的第一对象,从它出发开始查看它引用了那些对象,没有引用到的对象则被回收,这是GC的基本原理,所以我们要从Path to GC Roots中找到答案,有一部分对象是持续被持有,个人经验来说一般是循环体或者轮询惹的祸,循环轮询没写好很容易出现OOM。

所以,当出现OOM,分析源码最好从循环和轮询出发,这块是最容易出现OOM,至于是哪里的循环和轮询,通过JProfile查看,分析Refrence和Path to GC Roots。

Show Comments