JVM垃圾回收算法
一、复制算法
上一篇文章说到,jvm通过可达性算法,标记堆中的实例对象是否直接或者间接的被GC Roots引用,而没有引用的则是垃圾对象,等待回收的,那么什么时候会回收这些对象呢?新生代存储满了,存不了下一个新生成的实例对象,那么就是触发一次youngGc,回收掉垃圾对象。
1、触发youngGc条件以及回收机制
堆内存新生代中分为三个区域,eden区、survivor1区、survivor2区,默认内存大小比例是 8:1:1。
新对象进入的区域是eden区,而survivor区其中一个存储的是上一次youngGc存活下来的对象,当触发一次youngGc时,会对eden区和有对象 的survivor进行标记,并且回收垃圾对象,然后存活下来的对象,全部转移到另外一个没有存储的survivor区,而下一次youngGc则又是标记回收,eden区和现在存储上一次存活对象的survivor区。
jvm为什么需要用这么复杂的方式来回收对象呢?
垃圾回收的性能和内存碎片的控制。
举一个例子,如果直接对堆的新生代区域进行回收,存活的对象之间相邻有很多内存碎片,新生的对象大小是不确定,不可能将大小不一内存碎片分配给新生对象,只能开辟新的内存空间,而这些内存碎片,会随着youngGC次数而增多,对内存导致了极大的浪费。
那么如果将新生代一分为二,新生对象到其中一片空间,每一次youngGC将存活的对象移动到另一片内存空间整齐排列,来回反复,这样就没有了内存碎片的困扰,但是这样内存实际上只有一般得到了利用,而我们知道其实,新生代中的对象大多数都是垃圾对象,只有少数对象会在一次youngGC存活下来,大概1%左右,所以就有了现在复制算法的优化。
现在的其中一个survivor还是空闲,但是可以接受,只是对大概10%的内存闲置。
2、对象是如何进入老年代的
-
过15次GC之后进入老年代
可以通过JVM参数“-XX:MaxTenuringThreshold”来设置,默认是15岁
-
动态年龄判断
当前放对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小的50%,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代了
举一个例子:年龄1的占用了33%,年龄2的占用了33%,累加和超过默认的TargetSurvivorRatio(50%),年龄2和年龄3的对象都要晋升。
这样的做的原因还是想,提前将长期存活的对象进入老年代
-
大对象直接进入老年代
有一个JVM参数,就是“-XX:PretenureSizeThreshold”,可以把他的值设置为字节数,比如“1048576”字节,就是1MB。
类似一个超大数组,一直来回在年轻代复制,很耗费时间,所以直接让他进入老年代。
-
Minor GC后的对象太多无法放入Survivor区
假设在发生GC的时候,发现Eden区里超过150MB的存活对象(假设Survivor区是100M),此时没办法放入Survivor区中,此时该怎么办呢?
这个时候就必须得把这些对象直接转移到老年代去。 -
老年代空间分配担保规则
如果新生代里有大量对象存活下来,确实是自己的Survivor区放不下了,必须转移到老年代去 那么如果老年代里空间也不够放这些对象呢?这该咋整呢?
判断一 JVM会先检查一下老年代可用的可用内存空间,是否大于新生代所有对象的总大小。 为啥要检查这个
因为在极端的情况可能存在一次young GC后,所有新生代对象全部存活,都进入老年代。
所以如果新生代所有对象的总大小小于老年代可用内存,则可以放心的young GC,否则的话就会看是否配置了”-XX:-HandlePromotionFailure“,有则走下一个判断:
看看老年代的内存是否大于之前每一次young GC后进入老年代的平均大小。大于的话,也执行young Gc。
冒险执行young Gc之后
-
第一种可能,young Gc过后,剩余的存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survivor区域即可。
-
第二种可能,Minor GC过后,剩余的存活对象的大小,是大于 Survivor区域的大小,但是是小于老年代可用内存大小的,此时就直接进入老年代即可。
-
第三种可能,很不幸,Minor GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内存的大小。此时老年代都放不下这些存活对象了,就会发生“Handle Promotion Failure”的情况,这个时候就会触发一次“Full GC”。Full GC就是对老年代进行垃圾回收,同时也一般会对新生代进行垃圾回收。因为这个时候必须得把老年代里的没人引用的对象给回收掉,然后才可能让Minor GC过后剩余的存活对象进入老年代里面。
如果要是Full GC过后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致所谓的“OOM”内存溢出了
-
3、老年代垃圾回收算法
老年代采取的是**标记清理算法** ,通过GC Roots标记垃圾对象和存活对象,标记完成后,不是清理掉需要回收的对象,而是将所有存活的对象向一端移动,然后将边界以外的内存全部清理掉,这样可以有效避免空间碎片的产生。