之前通过三篇文章的分析,介绍了 直接内存、Metaspace和栈内存三块区域的内存溢出,同时给出了一些常见的引发内存溢出的场景以及对应解决方案,一般只要vm参数配置合理,代码上不出现大问题,一般不太容易引发对应的OOM。再次通过下图进行回顾:
而本篇文章介绍的堆内存OOM,就是我们的重点了,这块区域才是真正最容易引发内存溢出的。
引发的起因
我们在上图中其实可以发现,每次大量调用方法的时候,方法中的代码都会有创建对象的操作,那么会导致大量的对象进入到新生代,如果新生代放不下,触发Minor GC,则会转移存活对象进入 Survior区域,如果遇到高并发场景下,导致Minor GC过后依然有很多请求未处理完毕,存活对象太多,导致Survior区域放不下,直接进入老年代,如下图所示:
一旦当老年代也满了或达到阈值,就会触发Full GC,如果此时老年代GC后发现依然剩下很多存活对象,而新生代GC后需要转移的对象又很多,想放入老年代存放,发现老年代也放不下了,那么此时就会导致OOM,如下图所示
老年代触发GC后依然无法存放新生代转移过来的对象,没有足够的空间还要继续转移,那么就导致OOM。这就是一种典型的堆内存放不下而导致的内存溢出的一个案例。
堆内存溢出场景总结
一般发生堆内存溢出的场景主要有两种:
- 高并发场景,由于请求量非常大,导致大量对象都是存活状态,而大量存活对象放入有限的空间放不下,从而引发OOM,系统崩溃。
- 内存泄露场景,系统中存在内存泄露的问题,莫名其妙的产生了大量的对象,并且是存活状态,没有及时取消对象的引用,导致触发GC后还是无法回收,从而引发内存溢出。
因此,总结下就是导致内存泄露的原因:要不就是系统负载过高,要不就是内存泄露问题。
代码模拟堆内存OOM场景
我们通过以下代码来进行模拟:
/**
* vm参数: -Xms 10m -Xmx 10m
*/
public class OOMTest1 {
public static void main(String[] args) {
int count = 0;
List<Object> list = new ArrayList<>();
while(true){
list.add(new Object());
System.out.println("当前创建了第"+(++count)+"个对象");
}
}
}
代码很简单就是通过无限循环,往一个集合里添加对象,而集合是个强引用对象不会被回收,因此当Eden区存满后 ,存活对象均会进入到老年代,直到老年代也装不下后,触发OOM。
打印结果:
在10M的堆内存中,通过最简单的Object对象想要将内存撑满,也需要大概36万个对象。并且控制台里也有明确的提示:OutOfMemoryError : Java heap space 指向堆内存区域。
小结:
ok,通过以上的讲解,我们对堆内存发生OOM的根本原因有了一个理解,以及两种触发堆内存OOM的场景总结,以及通过简单的代码快速模拟了堆内存的OOM溢出。