跳到主要内容

14、调优实战 - 定位 Heap OOM

1. JVM 对象分配

前面两篇文章介绍了Metaspace区和Java虚拟机栈的内存溢出的分析及解决方案;一般来说,只要配置了合理的jvm参数和代码上注意一些,是不太容易发生这两块区域的内存溢出的。

真正最容易发生OOM的,是在 Java堆 中,也就是由于 平常系统中创建出来的对象实在太多了,最终导致了系统的OOM

前面也介绍过了jvm运行时在堆中创建对象和回收的流程,在这里就只再做一个简略的对象分配回顾:

1、 系统运行时会一直不停地创建对象,这些对象会先分配到Eden区;
2、 Eden区满了之后,就会触发YGC,然后存活对象进入Survivor区;
3、 如果存活对象太多,Survivor区放不下的时候,就会通过分配担保进入到老年代中;
4、 如果每次都有对象进入到老年代,也就会很快填满老年代;在老年代满了的时候,触发FGC;
5、 如果老年代FGC之后,存活的对象还是很多,并且新生代继续往老年代进入对象;
6、 在不能放下继续进入老年代的对象的时候,导致发生堆空间的OOM;

2. 什么情况会发生 Heap OOM

根据上面的对象分配流程,发生堆内存溢出的原因,总结下来就是:

  • 在新生代中的对象经过YGC之后大部分都是存活的,然后进入老年代经历FGC之后,仍然大部分都是存活的;所以老年代不能再继续放入更多对象,就导致了OOM。

针对这个原因,一般来说发生堆内存溢出就主要有两种场景:

1、 系统承载高并发请求:;

  • 因为请求量很大,就会创建很多的对象;

  • 因为请求量很大,系统处理就会变慢,导致GC的时候很多对象都是存活的;

  • 所以在FGC之后大量对象存活,并且继续放入,引发OOM; 2、 系统存在内存泄漏问题:;

  • 存在内存泄漏,对于大量对象没有及时清除引用,不能被GC掉;

  • 这些对象一直占据老年代空间,如果有新生代对象进入老年代,放不下了就会引发OOM;

这是两种主要场景,其他当然还有很多比如JVM内存分配不合理什么的,但是这些并不一定会导致堆内存溢出,可能更多的是导致频繁Full GC。

3. 模拟 Heap OOM

/**
 *
 * 堆内存溢出
 *
 * jvm options:
 *  -Xms100m -Xmx100m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xloggc:log/heap-oom.log
 *  -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=hprof/heap-oom
 */
public class HeapOomDemo {
   
     

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

		while (true) {
   
     
			list.add(new byte[1 * 1024 * 1024]);
			System.out.println("当前创建了 " + (++count) + "M 的对象");
		}
	}
}

这段代码也很简单,就是在一个while死循环里面不停地创建对象,并且使用一个list来存储对象,也就是说这些对象全部存在引用,不能被GC掉。

JVM参数为 -Xms100m -Xmx100m,没有指定新生代大小,也就是按照默认比例分配,新生代大概30M左右,老年代大概60多M左右。

简要分析一下代码执行流程:

1、 一直创建对象放在Eden区,在放入了快要满30M的时候(假设28M),发生YGC;这28M的对象不能被回收,全部进入老年代(老年代28M);
2、 YGC之后,继续放入Eden区,再次到28M的时候,发生YGC;继续放入老年代(老年代56M);
3、 继续执行,继续放入Eden区,再次到28M的时候,想要发生YGC,但是此时老年代已经有56M了,距离老年代空间限度很小了,所以不能直接发生YGC;
4、 就需要先执行FGC清除老年代的空间,但是这56M的对象,都被引用持有,在老年代也不能被GC掉,所以老年代依然占据56M空间;
5、 此时新生代的对象还得进入老年代,但是老年代装不下了;没有办法,直接抛出OOM了;

再来看下GC日志:
 
从GC日志的数据呈现来看,我们上面的分析大致正确;

再来看下代码执行结果:
 

在创建了95M对象的时候,发生了 OutOfMemoryError: Java heap space;

4. Heap OOM 的定位及解决

发生OOM之后,往往系统会很快崩溃掉,对于所有请求都不再响应,这个时候自然要登录到服务器上检查日志文件;
在检查日志的时候,就会看到上图的 OutOfMemoryError: Java heap space提示,自然就知道了系统发生了 堆内存溢出;

并且我们是打开了jvm的dump开关的,所以日志文件中也会显示 dump出了一份名为 java_pidxxxxx.hprof 的内存快照;

有了这个,自然是使用MAT进行分析了:

1、 打开MAT,直接查看Actions->DominatorTree:;
 

可以看到,这里有95个大小为1M的byte[]数组对象,他们一共占用了99%左右的内存空间; 2、 继续查看Reports->LeakSuspects:;
 
也能看出这里的95M对象; 3、 继续点击Seestacktrace进行查看:;
 
这里就看到了是 HeapOomDemo.java 类的第 21行处代码最终引发堆内存溢出的; 4、 再回到代码检查:;
list.add(new byte[1 * 1024 * 1024]);
也就是这行一直创建1M的byte[]数组对象的代码导致的…

通过MAT工具对于dump出来的内存快照进行分析,找到占用内存最多的对象,再定位到引发堆内存溢出的具体类和方法;

再结合代码进行分析,自然就能知道发生这次OOM的原因和解决办法了。