垃圾收集算法

image.png

分代收集理论

即对于年轻代和老年代,采用不同的垃圾收集算法。
年轻代用复制算法,老年代则使用标记整理算法或者标记清除算法。

复制算法的优点是快速,缺点是需要额外的空间。因为年轻代中的对象大部分都会死去,所以在复制的时候只需付出少量空间,可以快速进行垃圾清理。

标记整理和标记清除则比复制算法要慢一些,但是考虑到老年代中的对象数据量大,因此不适合采用复制算法,需要消耗额外的大量的空间。

复制算法

该算法一般只在年轻代使用,因为它一次只能使用一半的空间,年轻代的对象大部分是朝生夕舍的,所以即便浪费一些空间也没太大问题。

但是老年代不行,老年代的对象可能长时间存活,浪费空间将导致容易触发full GC.

流程

  1. 将内存分为大小相等的两块,每次使用其中一块;
  2. 进行垃圾收集时,将还存活的对象复制到另一块,并将已使用的这一块完全清空;

image.png

优点

  1. 速度快

缺点

  1. 浪费空间,一次只能使用一半空间。

    标记清除算法

    先标记,再清除,但是并不进行整理。

image.png

弊端:

  1. 效率问题(标记数量太多)
  2. 空间问题(产生不连续的碎片)

    标记整理算法

    先标记,让存活对象向一端移动,然后清除掉端边界以外的内存。
    image.png
    因此不会产生碎片。

垃圾收集器

垃圾收集器是对垃圾收集算法的实现。

1.1 Serial收集器

该收集器为串行收集器,历史最悠久,但是因为是单线程,所以效率低。最大的优点是简单,现在几乎不用了。

1.2 Parallel收集器

可以多线程进行收集。JDK1.8的默认收集器。当内存较小时,效率比较高,但是当内存较大时,CMS更好。

1.3 Parnew收集器

和Parallel收集器差不多,优点是可以和CMS搭配使用。

1.4 CMS收集器(重点)

CMS是Concurrent Mark Sweep的缩写,并发标记清除。下图中,灰色箭头代表的是CMS线程,白色箭头是应用程序箭头,可以看出来,虽然CMS垃圾收集的过程虽然复杂,但是只有在初始标记过程和重新标记过程会STW(Stop The World)
image.png
步骤1, 初始标记:
初始标记,该步骤STW,标记GC roots能直接引用的对象,注意是能直接引用,间接引用的不标记,因此速度很快。

步骤2, 并发标记:
并发标记,该过程不会STW,因此可以提升用户体验。由于没有STW,所以会有已标记过的对象的状态发生改变的问题。

步骤3, 重新标记:
重新标记,由于步骤2会导致某些对象的状态出问题,因此进行重新标记,STW。该过程用三色标记法。解决步骤2的问题。

步骤4, 并发清理:
并发清理,将垃圾对象清除掉。

步骤5, 并发重置:
并发重置,重置本次GC过程中的标记数据。即清除标记之类的。

CMS的缺点:

  1. 用户线程和垃圾收集线程会互相争夺资源;
  2. 无法处理浮动垃圾,浮动垃圾是在步骤4和5产生的垃圾,只能留给下一次GC进行处理;
  3. 使用的是标记清除算法,而不是标记整理算法,会产生大量空间碎片,但是,可以通过设置参数来实现标记整理。
  4. 并发失败,Concurrent mode failure,在垃圾收集的过程中,因为不是整个阶段都STW,所以在用户程序运行的过程中,有可能会产生大对象放进老年代。但是老年代可能空间不够放不下了,此时CMS会STW来专心做垃圾回收,使用Serial Old. 这会导致效率非常低,因此要尽量避免出现这种情况。

CMS的核心参数:
image.png
在CMS的使用过程中,一定要避免并发失败的问题,因此要特别注意第五个参数-XX: CMSInitiatingOccupancyFraction参数,我们可以将这个参数设置小一点,从而留出多余的空间给并发时产生大对象。

调参实例讲解

image.png
假设每

  1. 动态年龄阈值设置(-XX: MaxTenuringThreshold=5)

该值在默认情况下为15,也就是说当对象的动态年龄达到15时,才会进入老年代。也就是经历15次minor GC才会进入老年代。我们根据经验值将其设置成5,因为根据上图,没25秒进行一个minor GC, 经历5次已经过去了两三分钟,通常来说大多数对象会在几秒内变成垃圾,而如果一个对象经历了两三分钟仍然没有变成垃圾,说明它是一个可以存活较长的对象。因此设置成5早点让它移动到老年代,而不是一直占着survivor空间。

三色标记法

在并发标记的过程中,因为没有STW,会导致出现多标或者漏标的问题出现。

黑色:表示该对象被扫描过了,而且它的所有引用都被扫描过了。
灰色:该对象被扫描过,但是这个对象至少存在一个引用没有被扫描过。
白色:该对象没有被垃圾收集器扫描过。

多标问题不大,下次回收就可以了。
漏标问题很大,会导致不应该被回收的对象被回收了。
什么情况下会出现漏标?
例如,刚开始时,B -> D, B断开到D的引用,此时发生标记,D被标记为白色;
然后,A -> D,假设A一开始是黑色的,那么A不会再被处理,那么D的颜色不会被改变;
最终,D因为是白色被回收了。但实际D不应该被回收,因为D被A引用着。

漏标解决办法

以下两个方法都是通过写屏障来实现的。

增量更新

当增加一个赋值操作时,会用一个表保存这些引用,将来对这些引用进行重新标记。在重新标记的过程中,黑色对象会变成灰色对象。

原始快照(SATB)

当灰色对象要删除指向白色对象的引用时,用一个表来保存该引用记录。当进行重新标记时,将这些白色对象标记为黑色,让其存活下来,虽然它有可能是垃圾需要被回收。

写屏障

有点像钩子函数,即在赋值操作之前,或者之后进行一些操作。例如在赋值之前,或者之后,将引用记录放进某个表里。