一、引言
如果tomcat中有很多正在并发执行的线程,在并发执行我们的代码,每个线程在执行代码的过程中会不停地在堆内存中创建对象。
那么就会出现一个问题:JVM地内存大小是有限制的,内存是很昂贵的资源;
因为栈内存和metaspace区域也需要空间,对于2核4G的机器,堆内存一般在2GB左右;4核8G的机器,堆内存一般在4GB左右;
如果堆内存满了,就会去进行垃圾回收,去干掉一些不需要的对象,把内存空间腾出来。
二、内存分代的模型
把内存分为年轻代和老年代;
年轻代又可以分为Eden区、Survivor区,默认情况下eden和2个survivor区的比例是8:1:1;
三、什么时候会触发垃圾回收
1、如果Eden区满了,此时触发年轻代的垃圾回收young gc,去回收年轻代的垃圾对象;
垃圾对象:没有人引用的对象就是垃圾对象;
如:
public class MyController{
private static MyBean myBean = new MyBean();
}
类中的静态变量去引用一个实例对象,这个实例对象显然就不是垃圾对象;
2、如果让代码一边运行,一边判断哪些对象是可以回收的,这个是不现实的,垃圾回收中有一个stop the world的概念,去停止你的jvm里的工作线程的运行,然后扫描所有的对象,判断哪些可以回收,哪些不可以回收;
3、对于年轻代而言,大部分情况下,实例对象的生存周期是很短的,可能在0.01ms之内,线程执行了3个方法,创建了几个对象,0.01ms之后方法就执行结束了,之前创建的对象就变成了垃圾,是可以被回收的;
对于年轻代而言,由于大部分对象是可被回收的垃圾对象,所以垃圾回收采取的策略是复制算法,它会把存活的对象复制挪到survivor区里面去,把Eden区里面的对象全部清空,完成一次young gc;
4、三种场景下,年轻代里的实例对象会跑到老年代里面去:
第一种场景:有的对象在年轻代里熬过了很多次垃圾回收,比如15次垃圾回收,此时就会认为这个对象是要长期存活的对象,比如一直被Spring容器引用的bean,会把这个长期存活的对象放到老年代里面去;
第二种场景:young gc中,survivor区存放不下了的对象,也会被放到老年代里面去;
第三种场景:在创建对象时,特别大的对象也会被放到老年代里面去,因为如果在年轻代中,反复移动太大的对象,是很消耗内存的。
四、老年代的垃圾回收算法
1、当老年代中的对象越来越多,老年代的内存空间也会变满的;
2、如果在老年代中也采用和年轻代young gc一样的垃圾回收算法,是不合适的;
3、因为老年代中的实例对象是被长期引用的,如Spring容器中长期引用的bean,长期存活的对象比较多,所以使用复制算法的时候需要复制的东西就比较大,效率就会比较低下;
4、因为老年代中的垃圾对象相对没有那么多,所以可以使用标记-清理算法,找出来那些垃圾对象,把那些垃圾对象直接清理掉,但是会出现内存碎片的问题;
5、为了解决可能产生的内存碎片的问题,可以改用标记-整理算法,把老年代里的存活对象标记出来,移动到一起,剩余的就是垃圾对象,直接清除即可,就可以得到剩下的连续可用的内存空间;
五、常见的垃圾回收器
1、parnew+cms
用parnew垃圾回收器对年轻代进行回收,用cms垃圾回收器对老年代进行回收;
2、g1直接分代回收
g1可以回收年轻代,也可以回收老年代。