JVM垃圾回收机制的学习与分析(二、GC算法)
GC算法
一、Stop The World(STW)
GC是在一个单独的线程,无论JVM用哪种算法,都会存在一个阶段需要停止所有的用户线程,称Stop The World(STW)
二、评价标准
- 吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC时间),值越大,性能越高
- 最大暂停时间:SWT的最大值
- 堆的使用效率:复制算法只用一半空间
三、标记清除算法Mark Sweep GC
实现:
- 从GC Root List开始,遍历引用链
- 找到可达对象,并标记清除没标记的对象
优点:
- 实现简单,只需给对象维护个标记位
缺点:
- 导致内存碎片化:从原本连续的内存空间,摘掉一些被回收的,得到一些碎片。
- 分配速度慢:由于内存碎片化,需要维护一个空闲链表记录可用空间,新对象来了每次都得往后遍历,找出一块合适大小的地儿安置
四、复制算法Copying GC
实现
- 堆内存一分为二,一半叫From,一半叫To
- 新对象来了往From安置
- GC时,把From的存活对象Copy到To
- 清掉From,From和To名字互换,原来的To做为新的From安置新new的对象
- (将存活的对象搬运到另一块空间,清理掉当前空间,互换名字)
优点:
- 解决了内存碎片化:往To搬的时候,按连续地址往过码
- 吞吐相比下面的标记整理算法要高:只需遍历一次存活对象。但不如标记-清除算法,因为后者不用给对象搬家
缺点:
- 堆内存使用率低:安置新对象只能用50%的堆空间,另一半得留着To
五、标记整理算法Mark Compact GC
也称标记压缩,用来解决标记清除算法的内存碎片化缺点。
实现:
- 从GC Root开始,遍历标记可达对象
- 将可达的存活对象移动到堆的一端,清掉非存活的
优点:
- 无内存碎片化问题:比标记清除多了一步整理
- 堆内存利用率比复制算法高
缺点:
- 理解阶段性能不高,得看整理阶段的实现算法
六、分代算法Generational GC
组合使用了上面的几种算法,被主流使用。分代即把内存分为年轻代和老年代
实现:
-
新new的对象,安置到堆的年轻代的伊甸园区
-
伊甸园区满了以后,触发GC,仅是年轻代的GC(Minor GC、Young GC)
-
把Eden的存活对象放入S1(To),Eden区被清空(复制算法)
-
互换名,S0做为To,S1做为From,再安置新对象,直到Eden和From满
-
再次触发Minor GC,Eden和From存活对象放入S0,其余清掉回收(每次GC能活下来的,记录年龄,+1)
-
对象GC年龄到达阈值(最大15,对象头里放着,默认值和垃圾回收器有关),晋升到老年代。(一直活着就别在From和To之间来回搬了)
-
老年代最后也满了,新new的对象进来,先Minor GC,还是不足,再Full GC,对整个堆进行垃圾回收,此时的STW时间就比Minor GC时的SWT长一些了
-
Full GC后,无法回收老年代对象,再往老年代放,就OOM
-
补充:如果现在新生代已经满了,Minor GC还是满,再来对象,尽管新生代有的对象没到达年龄阈值,也会被搬到老年代
优点
- 分代GC下,可以只进行Minor GC,不用每次Full GC,STW时间短
- 开发者可以通过调整年轻代和老年代的比例来适应不同的服务场景,提高性能(对象用完即丢的,生命短的多,可以调大年轻代,目的就是少STW,非STW的,也能Minor就别Full)
- 年轻代和老年代可以选择使用不同的算法,年轻代通常用复制算法、老年代则用标记清除或者标记整理
补充
- 很多对象都是new完很快就可以回收,比如一个个Vo
- 老年代存放一直用的对象,比如Spring容器里的一些Bean
- JVM默认设置下,新生代空间远小于老年代