无限制的调用方法是如何让线程的栈内存溢出的?
一个线程调用多个方法的入栈和出栈
下图是一个相对较为完整的JVM运行原理图,如下所示:
看下面的代码:
按照我们之前所说的,JVM启动之后,HelloWorld类被加载到了内存里来,然后就会通过main线程执行main()方法
此时在main线程的虚拟机栈里,就会压入main()方法对应的栈桢,里面就会放入main()方法中的局部变量。
大家看看上面的图,在图里是不是有main线程的虚拟机栈和main()方法的栈桢的概念?
而且我们还知道一个概念,就是我们是可以手动设置每个线程的虚拟机栈的内存大小的,一般来说现在默认都是给设置1MB
所以看下图,main线程的虚拟机栈内存大小一般也是固定的。
现在回过头思考一下上面的代码,代码中是不是在main()方法中又继续调用了一个sayHello()方法?
而且sayHello()方法中也会自己的局部变量,所以此时会继续将sayHello()方法的栈桢压入到main线程的虚拟机栈中去,如下图。
接着sayHello()方法如果运行完毕之后,就不需要为这个方法在内存中保存他的一些局部变量之类的东西了,此时就会将sayHello()方法对应的栈桢从main线程的虚拟机栈里出栈,如下图。
再接着,一旦main()方法自己本身也运行完毕,自然会将main()方法对应的栈桢也从main线程的虚拟机栈里出栈,这里我们就不在图里表示出来了。
一个重要的概念:每次方法调用的栈桢都是要占用内存的
在这里,要给大家明确一个重要的概念,那就是每个线程的虚拟机栈的大小是固定的,比如可能就是1MB,然后每次这个线程调用一个方法,都会将本次方法调用的栈桢压入虚拟机栈里,这个栈桢里是有方法的局部变量的。
虽然说一些变量和其他的一些数据占用不了太大的内存,但是大家要记得,每次方法调用的栈桢实际上也是会占用内存的!
这是非常关键的一点,哪怕一个方法调用的栈桢就占用几百个字节的内存,那也是内存占用!
到底什么情况下会导致JVM中的栈内存溢出?
既然明确了上述前提之后,那么大家思考一下,到底什么情况下JVM中的栈内存会溢出呢?
其实非常简单,既然一个线程的虚拟机栈内存大小是有限的,比如1MB,那么假设你不停的让这个线程去调用各种方法,然后不停的把方法调用的栈桢压入栈中,是不是就会不断的占用这个线程1MB的栈内存?如下图所示:
那么如果不停的让线程调用方法,不停的往栈里放入栈桢,此时终有一个时刻,大量的栈桢会消耗完毕这个1MB的线程栈内存,最终就会导致出现栈内存溢出的情况。
一般什么情况下会发生栈内存溢出?
那么一般什么情况下会发生栈内存溢出呢?通常而言,哪怕你的线程的虚拟机栈内存就128KB,或者256KB,通常都是足够进行一定深度的方法调用的。
但是如果说你要是走一个递归方法调用,那就不一定了,看下面的代码。
一旦出现上述代码,一个线程就会不停的调用同一个方法,即使是同一个方法,每一次方法调用也会产生一个栈桢压入栈里,比如说对sayHello()进行100次调用,那么就会有100个栈桢压入中。
所以如果疯狂的运行上述代码,就会不停的将sayHello()方法的栈桢压入栈里,最终一定会消耗掉线程的栈内存,引发内存溢出。
所以一般来说,其实引发栈内存溢出,往往都是代码里写了一些bug才会导致的,正常情况下发生的比较少。
对象太多了!堆内存实在是放不下,只能内存溢出!
从对象在Eden区分配开始讲起
如果要把这大量的对象是如何导致堆内存溢出的给讲清楚,那就得从系统运行,在Eden区创建对象开始讲起了。
咱们都知道,平时系统运行的时候一直不停的创建对象,然后大量的对象会填满Eden区
一旦Eden区满之后,就会触发一次Young GC,然后存活对象进入S区。
如下图所示:
高并发场景下导致ygc后存活对象太多
当然因为各种各样的情况,一旦出现了高并发场景,导致ygc后很多请求还没处理完毕,存活对象太多,可能就在Survivor区域放不下了,此时就只能进入到老年代里去了,老年代很快就会放满了,如下图所示。
一旦老年代放满了就会触发Full GC,如下图所示。
我们假设ygc过后有一批存活对象,Survivor放不了,此时就等着要进入老年代里,然后老年代也满了,那么就得等着老年代进行CMS GC,必须回收掉一批对象,才能让年轻代里存活下来的一批对象进去。
但是呢,不幸的事情发生了,老年代GC过后,依然存活下来了很多的对象!如下图所示。
这个时候如果年轻代还有一批对象等着放进老年代,人家GC过后空间还是不足怎么办?
还能怎么办!只能是内存溢出了!如下图所示!
所以这个时候,老年代都已经塞满了,你还要往里面放东西,而且触发了Full GC回收了老年代还是没有足够内存空间,你坚持要放?那只能给你一个内存溢出的异常了!JVM跑不动了,崩溃掉。
这个就是典型的堆内存实在放不下过多对象的内存溢出的一个典型范例。
什么时候会发生堆内存的溢出?
发生堆内存溢出的原因其实总结下来,就一句话:
有限的内存中放了过多的对象,而且大多数都是存活的,此时即使GC过后还是大部分都存活,所以要继续放入更多对象已经不可能了,此时只能引发内存溢出问题。
所以一般来说发生内存溢出有两种主要的场景:
(1) 系统承载高并发请求,因为请求量过大,导致大量对象都是存活的,所以要继续放入新的对象实在是不行了,此时就会引发OOM系统崩溃
(2) 系统有内存泄漏的问题,就是莫名其妙弄了很多的对象,结果对象都是存活的,没有及时取消对他们的引用,导致触发GC还是无法回收,此时只能引发内存溢出,因为内存实在放不下更多对象了
因此总结起来,一般引发OOM,要不然是系统负载过高,要不然就是有内存泄漏的问题
这个OOM问题,一旦你的代码写的不太好,或者设计有缺陷,还是比较容易引发的,所以这个问题也是我们后面要重点分析的。