JVM随笔记录3-JVM的垃圾回收算法
上篇讲到了jvm的内存分配,这节记录一下jvm堆中新生代和老年代分别使用的垃圾算法
1.新生代
新生代使用的算法是复制算法,上节中说到新生代分为eden区和两个survivor区(S1,S2),那么新生代为什么要划分出这三个区域来呢,先从新生代的垃圾回收算法来说说这个
1.1.一种不太好的垃圾回收思路
这里先说说新生代一种不太好的垃圾回收思路,假设新生代只有一块区域,当新生代内存区域满了之后,对存活的对象进行标记,清理出没有用的垃圾对象,如下图,绿色表示存活对象,蓝色表示垃圾对象,然后将蓝色对象清理掉就变成右边那样,这样就会导致有很对内存碎片,会导致资源的浪费,图中红色标记出来的就是内存碎片了,内存碎片太多,可能所有的内存碎片加起来会是一块很大的内存,但是由于这些内存是分散的,就会导致没有一块完整的内存空间来分配新的对象,所以这种垃圾回收算法是不可取的,大量的内存碎片会导致大量的内存浪费。

1.2.一种合理的垃圾回收思路
我们如果将新生代划分为均等的两份,分别为S1和S2,新的对象先往S1中放,当S1区域满了之后,将存活的对象标记出来,将存活的对象转移到另一块内存S2中,然后再将S1中的垃圾对象清理掉,这样新生代中只有S2有存活的对象,而且可以紧凑排列,此时新的对象就可以分配在S2中还没使用的连续空白内存中,就避免了内存碎片得问题,而S1也就成了一块空闲可用的区域了,这个也就是所谓的“复制算法”,当其中一块内存满了之后,就把里面存活的对象一次性转移到另外一个内存中,这样就可以保证没有内存碎片了,接着回收掉原来那块区域的垃圾对象,再次空出来一块内存区域,两块内存就这样可以循环使用了。
但是这种思路有个缺点,如果我给新生代分配1G内存,那么永远只有512M的内存可用,剩余的512M用于处于空闲状态,这样内存使用效率太低了,接下里看下JVM里面对复制算法的优化处理
1.3.JVM新生代垃圾回收:复制算法
一般在JVM中,我们代码是不停的在创建对象到新生代中,但是一般很快那些对象就没有引用变成垃圾对象了,接着过了一段时间后,新生代满了,这时就会进行垃圾回收,回收掉垃圾对象,空出来内存空间给其他新的对象使用,从这里可以看出,垃圾回收过后。内存中大约99%的对象都是垃圾对象被回收了,可能就1%的对象存活了下来,这些存活对象可能时一些长期存活的对象,或这时还没使用完的对象。
所以实际上JVM对复制算法优化是:新生代分为:一个eden区和两个survive区,eden区占新生代内存的80%,两个survive区分别占10%,平常使用的是eden区和一块survive区,那么一般情况下新生代的内存使用情况是90%,新对象进来初始分配在eden区中,当eden区内存满了之后,就会触发垃圾回收,此时会把eden区中存活的对象转移到一块survive区,接着eden区则会清空,当有新的对象进来时,还是分配到eden区中,此时的内存分配情况是eden区和一块survive区(S1)是有对象的,其中S1里放的是上一次minor gc存活下来的对象,如果下次eden区满了,那么会再次出发minor gc,就会把eden区和S1区的存活对象转移到另外一块survive区(S2)中,如果下一次eden区又满了,就把eden区和S2区中存活的对象转移到S1区中。
之前有分析,每次垃圾回收后可能存活下来的对象只有1%,所以如果新生代分配内存1G,那么eden区占800M,两个survive区分别占100M,那么当eden区和一块suevive区满了之后,进行minor gc,可能存活下来的对象只有10M,那么此时将这10M存活对象转移到另一块survive区中,然后再一次性把eden区和上一块survive区的垃圾全部回收掉,接着新的对像继续向eden区和再使用的survive区中分配,这样就可以一直保持一块suvive区是空的,循环使用这三块区域。
这样的好处就在于,只会有10%的空间是空闲的,而90%的内存是都处于使用状态。
2.老年代
上面讲述了新生代垃圾回收算法,接着来说下对象怎么进入老年代以及老年代的回收算法
2.1.新生代躲过15次GC进去老年代
根据新生代的垃圾回收算法,如果一个对象时长期使用的,那么这个对象就会一直不会被新生代的垃圾回收算法回收掉,新生代中如果一个对象在一次minor gc后没有被回收掉,那么这个对象的分代年龄就加1,当则会个对象躲过15次GC后,那么这个对象的分代年龄就达到15,此时,这个对象就会被转移到老年代的内存中去,这个具体多少岁进入老年代,是可以由JVM参数”-XX:MaxTenuringThreshod“来设置的
2.2.动态年龄对象判断
这个规则可以让对象不经过15次GC就可进去老年代,大致的规则就是:在存放存活对像的survive区中,如果一批对象的内存大小大于这块survive区内存的50%,那么这个suvive区中其他对象大于那批对象最大年龄的对象就可以进入到老年代中,举个例子:比如survive 1区大小100M,其中有10个对象的总大小是60M,大于survive区的一半,这10个对象的年龄都是2岁,那么survive区中大于2岁的对象都可以转移到老年代中,这个就是 动态年龄判断 规则,实际上这个规则就是:年龄1+年龄2+。。。+年龄N的多个对象大小之和 大于 survive区大小的50%,那么此时就会把大于年龄N的对象转移到老年代中。
这里无论是是15次GC进入老年代,还是这个动态年龄判断规则,目的都是把年龄大的对象放到老年代中,既然你是长期存活的对象,就不用占用新生代的空间了。
2.3.minor gc后存活对象太多无法放入survive区
当新生代eden区满了,在进行minor gc后,发现存活下来的对象比survive区可用的内存区域大,那么这批对象就会直接进入到老年代中。比如survive区有100M,但是minor gc之后 eden区还有150M的存活对象,那么这150M的存活对像会直接转移到老年代中。
2.4.大对象直接进入到老年代
当一个对像的大小超过设置的指定值时,会将这个对象直接转移到老年代中,这个jvm参数值由”-XX:PretenureSizeThreshold“设置,可以把这个值设置成字节数,比如”1048576“字节,就表示1M,这样做的目的是防止这样的大对像在新生代中躲过一次次GC后频繁的来回复制。
2.5.老念代空间分配担保规则
老年代在新生代进行minor gc之前会先判断一下新生代所有对象的大小是否大于老年代内存的空闲大小,检查这个目的是 如果在极端的情况下,新生代的对象在minor gc过后都是存活的对象,那么新生代所有对象都要进入老年代中,所以如果新生代的所有对象大小小于老年代的空闲内存大小,那么新生代就可以放心的进行minor gc了,因为即使minor gc过后所有对象都存活,老年代也可以放的下。那么如果老年的空闲内存小于新生代所有对象大小呢,这个时候就看看一个jvm参数”-XX:-HandlePromotionFailure“是否设置了,如果有设置这个参数,那么就会尝试下一步判断,就是查看老年代空闲内存的大小是否大于之前多次minor gc后进入老年代的对象的平均大小,举个例子,之前每次minor gc会有10M左右的对象进入老年代,如果此时老年代的可用内存大于10M,那么表示这次minor gc后进入老年代的对象,老年代可能放得下,就会进行minor gc。如果老年代的内存大小小于10M,或者参数”-XX:-HandlePromotionFailure"没有设置,那么老年代会触发一次”FULL GC“,就是对老年代进行垃圾回收,回收出一点内存空间出来,然后再执行minor gc。
上述两种情况成功都会进行minor gc,那么此时进行monor gc可能会有以下几种情况
第一种是minor gc过后存活的对象比survive区小,那么会将对象放到survive区中
第二种情况minor gc后存活的对象大于survive区大小,但是小于老年代空闲大小,那么会将这些对象放进老年代中
第三种情况是minor gc后存活的对象大于survive区大小,同时也大于老年代空闲大小,此时老年代放不下这些对象了,就会进行一个”FULL GC“,如果FULL GC过后还是没有内存存放这些对象,那么就会产生所谓的”OOM“内存溢出异常了。
2.6.老年代的回收算法
根据上述描述,可以得出,老年代触发full gc一般就是两种情况:
一种是minor gc之前,老年代检查发现可能minor gc过后进入老年代的对象太多,也就是老年代可用空间小于历次进入老年代的存活对象的平均大小,此时提前触发一个full gc来释放出一点空间,然后再进行minor gc
另外一种就是minor gc后进入老年代的对象太多,老年代放不下,此时老年代就触发FULL GC
那么老年代的垃圾回收算法是什么呢?
简单来说,老年代采取的垃圾回收算法是 标记整理,就是将老年代中存活的对象标记出来,然后将这些对象再内存中移动到一起,紧凑的排列再一起,然后将垃圾对象回收掉,这样就避免了大量的内存碎片问题,这么看来,老年代的垃圾回收算法是比新生代的垃圾回收算法慢的,实际上也是的,一般老年代会比新生代慢10倍,如果频繁的出现full gc,就会严重影响系统的性能,频繁出现卡顿情况。
那么为什么老年代不跟新生代使用一样的算法呢,也分区eden区和survive区,这里主要是因为老年代放的大部分都是长期存活的对象,如果跟新生代那样,每次都会复制大量的数据,而且还会浪费大量的内存空间。
3.垃圾回收器
3.1.serial和serial Old垃圾回收器
这两个分别是用来回收新生代和老年代的垃圾对象,工作是单线程运行,垃圾回收的时候停掉其他工作线程,让我们系统处于卡死不动得状态,然后进行垃圾回收
3.2.ParNew和CMS垃圾回收器
ParNew一般用于新生代得垃圾回收,CMS用户老年代得垃圾回收,他们都是多线程并发得处理机制,性能更好
3.3.G1垃圾回收器
统计收集新生代和老年代得垃圾回收,采用了更加优秀的算法和设计机制
4.参数整理
-XX:MaxTenuringThreshod:分代年龄达到多少后进入老年代中,默认值是15
-XX:PretenureSizeThreshold:对象大小大于多少时直接进入到老年代。
-XX:-HandlePromotionFailure:开启空间担保规则,判断老年代空闲空间是否大于历次进入老年代的对象的平均大小,小于的话执行full gc

浙公网安备 33010602011771号