Pre
之前的文章已经分析了Metaspace和栈内存两块内存区域发生内存溢出的原理,同时给出了一些较为常见的引发他们内存溢出的场景,一般只要代码上注意一些,不太容易引发那两块区域的内存溢出。
重点要来了,真正最容易引发内存溢出的,说白了就是平时我们系统创建出来的对象实在是太多了,最终就导致了系统的内存溢出!
从对象在Eden区分配开始讲起
如果要把这大量的对象是如何导致堆内存溢出的给讲清楚,那就得从系统运行,在Eden区创建对象开始讲起了。
咱们都知道,平时系统运行的时候一直不停的创建对象,然后大量的对象会填满Eden区
一旦Eden区满之后,就会触发一次Young GC,然后存活对象进入S区。
如下图所示
高并发场景下导致ygc后存活对象太多
当然因为各种各样的情况,一旦出现了高并发场景,导致ygc后很多请求还没处理完毕,存活对象太多,可能就在Survivor区域放不下了,此时就只能进入到老年代里去了,老年代很快就会放满了,如下图所示。
一旦老年代放满了就会触发Full GC,如下图所示。
我们假设ygc过后有一批存活对象,Survivor放不了,此时就等着要进入老年代里,然后老年代也满了,那么就得等着老年代进行CMS GC,必须回收掉一批对象,才能让年轻代里存活下来的一批对象进去。
但是呢,不幸的事情发生了,老年代GC过后,依然存活下来了很多的对象!如下图所示。
这个时候如果年轻代还有一批对象等着放进老年代,人家GC过后空间还是不足怎么办?
还能怎么办!只能是内存溢出了!如下图所示!
所以这个时候,老年代都已经塞满了,你还要往里面放东西,而且触发了Full GC回收了老年代还是没有足够内存空间,你坚持要放?那只能给你一个内存溢出的异常了!JVM跑不动了,崩溃掉。
这个就是典型的堆内存实在放不下过多对象的内存溢出的一个典型范例。
什么时候会发生堆内存的溢出?
发生堆内存溢出的原因其实总结下来,就一句话:
有限的内存中放了过多的对象,而且大多数都是存活的,此时即使GC过后还是大部分都存活,所以要继续放入更多对象已经不可能了,此时只能引发内存溢出问题。
所以一般来说发生内存溢出有两种主要的场景:
- 系统承载高并发请求,因为请求量过大,导致大量对象都是存活的,所以要继续放入新的对象实在是不行了,此时就会引发OOM系统崩溃
- 系统有内存泄漏的问题,就是莫名其妙弄了很多的对象,结果对象都是存活的,没有及时取消对他们的引用,导致触发GC还是无法回收,此时只能引发内存溢出,因为内存实在放不下更多对象了
因此总结起来,一般引发OOM,要不然是系统负载过高,要不然就是有内存泄漏的问题
这个OOM问题,一旦你的代码写的不太好,或者设计有缺陷,还是比较容易引发的,所以这个问题也是我们后面要重点分析的。
Case Demo
一旦要是系统负载过高,比如并发量过大,或者是数据量过大,或者是出现了内存泄漏的情况,很容易就导致JVM内存不够用了,就会堆内存溢出,然后系统崩溃。 所以今天就体验一下堆内存溢出的场景。
Review 堆内存溢出的一个典型场景
首先还是来一张完整的JVM运行原理图,里面包含了对象的分配,GC的触发,对象的转移,各个环节如何触发内存溢出的,大家一定要牢记这张图。
接着我们就来回顾一下一个典型的堆内存溢出的场景:
假设现在系统负载很高,不停的运转和工作,不停的创建对象塞入内存里,刚开始是塞入哪里的?
当然是年轻代的Eden区了,如下图红圈所示。
但是因为系统负载实在太高了,很快就把Eden区塞满了,这个时候触发ygc
但是ygc的时候发现不对劲,因为似乎Eden区里还有很多的对象都是存活的,而且survivor区域根本放不下,这个时候只能把存活下来的大批对象放入老年代中去,如下图红圈处。
就这么来几次ygc之后,每次ygc后都有大批对象进入老年代,老年代很快就会塞满了,而且最重要的是这里的对象还大多都是存活的。
所以接下来一次ygc后又要转移一大批对象进入老年代,先触发full gc,但是full gc之后老年代里还是塞满了对象,如下图红圈所示。
这个时候ygc后存活下来的对象哪怕在full gc之后还是无法放入老年代中,此时就直接报出堆内存溢出了,如下图红圈所示。
所以堆内存溢出的场景就是这样子,非常的简单 。
模拟Code
代码很简单,就是在一个while循环里不停的创建对象,而且对象全部都是放在List里面被引用的,也就是不能被回收。
大家试想一下,如果你不停的创建对象,Eden区满了,他们全部存活会全部转移到老年代,反复几次之后老年代满了。
然后Eden区再次满了,ygc后存活对象再次进入老年代,此时老年代先full gc,但是回收不了任何对象,因此ygc后的存活对象就一定是无法进入老年代的。
所以我们用下面的JVM参数来运行一下代码:-Xms10m -Xmx10m,我们限制了堆内存大小总共就只有10m,这样可以尽快触发堆内存的溢出。
我们在控制台打印的信息中可以看到如下的信息:
当前创建了第360145个对象
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
所以从这里就可以看到,在10M的堆内存中,用最简单的Object对象搞到老年代被塞满大概需要36万个对象。然后堆内存实在放不下任何其他对象,此时就会OutOfMemory了,而且告诉了你是Java heap space,也就是堆空间发生了内存溢出的。
小结
可以看到堆内存是如何溢出的,以及溢出的时候回看到什么样的异常信息。