面向垃圾的Java垃圾回收器

G1 "Garbage First" 垃圾回收器是 Sun 在2009年 JDK update 14 中首次提出,并在JDK 7 中向服务器端应用大力推广的一个垃圾回收器。G1回收器进化于CMS (Concurrent Mark Sweep) 回收器,不仅拥有CMS的并行分代回收的特点,更是具有空间整合和可预测停顿的优势。

垃圾回收算法比较
特征          收集器  阐述
并行回收 CMS,G1
  1. 垃圾回收时,多CPU同时处理多个内存块,减少停顿时间
  2. 垃圾回收(标记阶段)可以和程序运行并发执行,消除长时间停顿
分代回收 CMS,G1 根据对象的存活时间来把对象放在不同的内存区域。存活时间短的对象所在区域被优先回收,提高回收效率。
空间整合 G1 采用“复制”回收算法,将碎片内存对象拷贝到连续整合的内存块中,大大减少了因内存碎片而分配内存失败的现象。
可预测停顿 G1 普通垃圾回收中的不可预测停顿时间已经严重影响了服务程序的响应时间指标。G1算法可以动态调整每次回收区域的大小来努力达到一个可预测的短时间停顿。

G1回收器的实现思路

堆内存的分区

为了能够实现并行化处理以及达到一个可控的回收粒度(避免整个堆被一起回收),内存被划分成了2048个分区。这个划分是在JVM启动的时候便完成的,而具体的划分还会根据用户的参数来调整。

这样做的目的非常明显,我们希望所有分区可以被并行处理;同时我们也可以选择性地进行处理某些分区而避免整个内存被扫描。可是不可避免的问题便是跨分区的引用,我们将在下一个小节中阐述如何解决这个问题。

这些分区某些被放入年轻代(EDEN)分区集合,有些被放入老年代分区集合,还有些被用作“存活区”,来实现基于“复制”的回收。

跨区引用的维护

如果有别的分区存在对象引用到当前分区的对象的时候,我们便没法将当前分区隔离开来进行单独的回收了。G1中为了解决这个问题引入了RS "remember set"。RS是一个集合,记录了所有其他分区对这个分区的应用。

这个集合是这样被维护的:当每一个对象引用改写操作发生时,JVM都会执行一小段代码来确认新的引用是否是跨分区的。所有跨分区的引用将会被计入在RS中。当G1对每个分区进行垃圾回收时,把分区的RS加入根节点集合一块进行标记即可。

年轻代的回收

根据用户指定的可容忍停顿时间,G1 可以确定一个较小的可控年轻代分区集合。当年轻代GC触发时,所有指向年轻代的根节点都会用来标记年轻代中的存活对象,当然我们也要更新存活对象的跨区引用。然后所有散落在年轻代分区集合中存活对象将会被复制到“存活区”的分区中去,他们本来所在的分区也就被回收了。这是一次垃圾回收,更是一次内存碎片的整理。

“存活区”本身也是属于年轻代的一部分。当下一次内存整理发生的时候,存活区中零散的存活对象也会被一同复制入另外一个存活区。当一个对象反复被在多个“存活区”之间复制之后,将会被复制如“老年代”分区。于是年轻代的长时间存活对象逐渐成为老年代的对象。

老年代的回收

随着老年代内存对象的增长,整个内存的占用率也会上升(因为老年代的内存是不会被年轻代GC回收的)。JVM根据一个预定的阈值(65%)来触发老年代的垃圾回收。

老年代是很大的,我们不可能对所有老年代的分区进行回收,于是我们要选出一些最有价值的老年代分区进行回收(这也是Garbage First 这个称谓的来源)。当然,在选择分区进行回收之前,我们还是需要对老年期存活对象进行标记,然后根据标记的结果来选择垃圾最多的分区。

对老年期存活对象的标记在绝大多数时候是可以和用户程序"side-by-side"并行执行的,这样就可以避免长时间的标记停顿时间。

为了避免用户程序打乱已经标记好的对象,我们在标记前做一个“snapshot”,并在标记过程中记录所有用户程序的改变,然后在标记快结束时,暂停用户程序,通过读取这个改变log来重新标记那些改变的分块和RS。

标记完成后,我们也就选出了最有价值的分区,然后将这些分区合并入下一次“年轻代”回收 -- 通过复制到存活区的方法来回收和整理内存。

更多阅读

更多关于G1垃圾回收器的介绍,请进入 http://www.drdobbs.com/jvm/g1-javas-garbage-first-garbage-collector/219401061?pgno=1 查看。

博客推送