Garbage First
Garbage First 简称 G1,它的目标是要做到尽量减少 GC 所导致的应用暂停的时间,让应用达到准实时的效果,同时保持 JVM 堆空间的利用率,其最大的特色在于允许指定在某个时间段内 GC 所导致的应用暂停的时间最大为多少,例如在 100 秒内最多允许 GC 导致的应
用暂停时间为 1 秒,这个特性对于准实时响应的系统而言非常的吸引人,这样就再也不用担心系统突然会暂停个两三秒了。G1 要做到这样的效果,也是有前提的,一方面是硬件环境的要求,必须是多核的 CPU以及较大的内存(从规范来看,512M 以上就满足条件了),另外一方面是需要接受吞吐量的稍微降低,对于实时性要求高的系统而言,这点应该是可以接受的。
为了能够达到这样的效果,G1 在原有的各种 GC 策略上进行了吸收和改进,在 G1 中可以看到增量收集器和 CMS 的影子,但它不仅仅是吸收原有 GC 策略的优点,并在此基础上做出了很多的改进,简单来说,G1 吸收了增量 GC 以及 CMS 的精髓,将整个 jvm Heap 划分为多个固定大小的 region,扫描时采用 Snapshot-at-the-beginning 的并发 marking 算法(具体在后面内容详细解释)对整个 heap 中的 region 进行 mark,回收时根据 region 中活跃对象的bytes 进行排序,首先回收活跃对象 bytes 小以及回收耗时短(预估出来的时间)的 region,回收的方法为将此 region 中的活跃对象复制到另外的 region 中,根据指定的 GC 所能占用的时间来估算能回收多少 region,这点和以前版本的 Full GC 时得处理整个 heap 非常不同,这样就做到了能够尽量短时间的暂停应用,又能回收内存,由于这种策略在回收时首先回收的是垃圾对象所占空间最多的 region,因此称为 Garbage First。
看完上面对于 G1 策略的简短描述,并不能清楚的掌握 G1,在继续详细看 G1 的步骤之前,必须先明白 G1 对于 JVM Heap 的改造,这些对于习惯了划分为 new generation、oldgeneration 的大家来说都有不少的新意。
G1 将 Heap 划分为多个固定大小的 region,这也是 G1 能够实现控制 GC 导致的应用暂停时间的前提,region 之间的对象引用通过 remembered set 来维护,每个 region 都有一个remembered set, remembered set 中包含了引用当前 region 中对象的 region 的对象的 pointer,由于同时应用也会造成这些 region 中对象的引用关系不断的发生改变, G1 采用了 Card Table来用于应用通知 region 修改 remembered sets,Card Table 由多个 512 字节的 Card 构成,这些 Card 在 Card Table 中以 1 个字节来标识,每个应用的线程都有一个关联的 remembered setlog,用于缓存和顺序化线程运行时造成的对于 card 的修改,另外,还有一个全局的 filled RSbuffers,当应用线程执行时修改了 card 后,如果造成的改变仅为同一 region 中的对象之间的关联,则不记录 remembered set log,如造成的改变为跨 region 中的对象的关联,则记录到线程的 remembered set log,如线程的 remembered set log 满了,则放入全局的 filled RSbuffers 中,线程自身则重新创建一个新的 remembered set log,remembered set 本身也是一个由一堆 cards 构成的哈希表。
尽管 G1 将 Heap 划分为了多个 region,但其默认采用的仍然是分代的方式,只是仅简单的划分为了年轻代(young)和非年轻代,这也是由于 G1 仍然坚信大多数新创建的对象都是不需要长的生命周期的,对于应用新创建的对象,G1 将其放入标识为 young 的 region
中,对于这些 region,并不记录 remembered set logs,扫描时只需扫描活跃的对象,G1 在分代的方式上还可更细的划分为:fully young 或 partially young,fully young 方式暂停的时候仅处理 young regions,partially 同样处理所有的 young regions,但它还会根据允许的 GC 的
暂停时间来决定是否要加入其他的非 young regions,G1 是运行到 fully-young 方式还是partially young 方式,外部是不能决定的,在启动时,G1 采用的为 fully-young 方式,当 G1完成一次 Concurrent Marking 后,则切换为 partially young 方式,随后 G1 跟踪每次回收的效
率,如果回收 fully-young 中的 regions 已经可以满足内存需要的话,那么就切换回 fully young方式,但当 heap size 的大小接近满的情况下,G1 会切换到 partially young 方式,以保证能提供足够的内存空间给应用使用。
除了分代方式的划分外, G1 还支持另外一种 pure G1 的方式,也就是不进行代的划分,pure 方式和分代方式的具体不同在下面的具体执行步骤中进行描述。
1.Initial Marking --初始标记
G1 对于每个 region 都保存了两个标识用的 bitmap,一个为 previous marking bitmap,一个为 next marking bitmap,bitmap 中包含了一个 bit 的地址信息来指向对象的起始点。
开始 Initial Marking 之前,首先并发的清空 next marking bitmap,然后停止所有应用线程,并扫描标识出每个 region 中 root 可直接访问到的对象,将 region 中 top 的值放入 next top atmark start(TAMS)中,之后恢复所有应用线程。
触发这个步骤执行的条件为:
G1 定义了一个 JVM Heap 大小的百分比的阀值,称为 h,另外还有一个 H,H 的值为(1-h)*Heap Size,目前这个 h 的值是固定的,后续 G1 也许会将其改为动态的,根据 jvm 的运行情况来动态的调整,在分代方式下,G1 还定义了一个 u 以及 soft limit,soft limit 的值为 H-u*Heap Size,当 Heap 中使用的内存超过了 soft limit 值时,就会在一次 clean up 执行完毕后在应用允许的 GC 暂停时间范围内尽快的执行此步骤;
在 pure 方式下,G1 将 marking 与 clean up 组成一个环,以便 clean up 能充分的使用 marking 的信息,当 clean up 开始回收时,首先回收能够带来最多内存空间的regions,当经过多次的 clean up,回收到没多少空间的 regions 时,G1 重新初始化一个新的 marking 与 clean up 构成的环。
2.Concurrent Marking
按照之前 Initial Marking 扫描到的对象进行遍历,以识别这些对象的下层对象的活跃状态,对于在此期间应用线程并发修改的对象的以来关系则记录到 remembered set logs 中,新创建的对象则放入比 top 值更高的地址区间中,这些新创建的对象默认状态即为活跃的,同时修改 top 值。
3.Final Marking Pause
当应用线程的 remembered set logs 未满时,是不会放入 filled RS buffers 中的,在这样的情况下,这些 remebered set logs 中记录的 card 的修改就会被更新了,因此需要这一步,这一步要做的就是把应用线程中存在的 remembered set logs 的内容进行处理,并相应的修改remembered sets,这一步需要暂停应用,并行的运行。
4.Live Data Counting and Cleanup
值得注意的是,在 G1 中,并不是说 Final Marking Pause 执行完了,就肯定执行 Cleanup这步的,由于这步需要暂停应用,G1 为了能够达到准实时的要求,需要根据用户指定的最大的 GC 造成的暂停时间来合理的规划什么时候执行 Cleanup,另外还有几种情况也是会触发这个步骤的执行的:
G1 采用的是复制方法来进行收集,必须保证每次的”to space”的空间都是够的,因此 G1 采取的策略是当已经使用的内存空间达到了 H 时,就执行 Cleanup 这个步骤;
对于 full-young 和 partially-young 的分代模式的 G1 而言,则还有情况会触发 Cleanup的执行,full-young 模式下,G1 根据应用可接受的暂停时间、回收 young regions需要消耗的时间来估算出一个 yound regions 的数量值,当 JVM 中分配对象的 young regions 的数量达到此值时,Cleanup 就会执行;partially-young 模式下,则会尽量频繁的在应用可接受的暂停时间范围内执行 Cleanup,并最大限度的去执行non-young regions 的 Cleanup。http://i.cnblogs.com/EditPosts.aspx?postid=4527158
这一步中 GC 线程并行的扫描所有 region,计算每个 rehttp://i.cnblogs.com/EditPosts.aspx?postid=4527158gion 中低于 next TAMS 值中 markeddata 的大小,然后根据应用所期望的 GC 的短http://i.cnblogs.com/EditPosts.aspx?postid=4527158延时以及 G1 对于 region 回收所需的耗时的预估,排序 region,将其中活跃的对象复制到其他 region 中。
[---> 注 <----]
Remember Set --- region 之间的对象引用关系
Card Table --- region 中对象的引用关系不断的发生改变
filled RSbuffers --- 记录 remembered set log --跨 region 中的对象的关联发生变化。
全文 《构建高性能的大型分布式Java应用》
翻开Garbage First文档
It is important to note that G1 is not a real-time collector.It meets the set pause time target with high probability but not absolute certainty. Based on data from previous collections, G1 does an estimate of how many regions can be collected within the user specified target time. Thus, the collector has a reasonably accurate model of the cost of collecting the regions, and it uses this model to determine which and how many regions to collect while staying within the pause time target.