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默认设置下,新生代空间远小于老年代