G1垃圾收集器入门教程(3)——G1垃圾收集器

垃圾优先(G1)收集器是一种服务器式的垃圾收集器,适用于具有大容量内存的多处理器计算机。它可以高概率地满足垃圾收集(GC)停顿时间的目标,同时达到高吞吐量。在 Oracle 的 JDK 7u4 以及更新版本中,G1 垃圾收集器已经得到完全的支持了。G1 收集器是专门为有以下要求的应用程序而设计的:

  • 可以和应用程序的线程并发操作,类似于 CMS 收集器。
  • 整理空闲的内存空间,不会由于冗长的 GC 而导致长时间停顿。
  • 需要更加可预测的 GC 停顿持续时间。
  • 不想牺牲太多的吞吐量性能。
  • 不需要增加大量的 Java 堆内存。

G1 被计划作为并发标记-清除收集器(CMS)的长期替换方案。对 G1 和 CMS 进行比较,可以发现两者之间存在不同之处,G1 是一个更好的解决方案。其中有一个不同之处就是 G1 是一种整理型的收集器。G1 能够充分地整理内存碎片,这样在分配内存时,便可以完全避免使用细粒度的空闲内存列表,而是改为依赖于内存区域(Region)。这种方式可以大大简化收集器的部件,最大程度地消除潜在的内存碎片问题。此外,与 CMS 收集器相比,G1 收集器能够提供更加可预测的垃圾收集停顿时间,使得用户可以为停顿时间设定性能指标。

G1 运行概况

较老的垃圾收集器(串行、并行、CMS)都会将堆内存划分为三个区域:年轻代、老年代,以及固定内存空间的永久代。

Hotspot的堆内存结构

所有的内存对象最终都会存储在这三个区域的其中一个之中。

G1 收集器采用不同的方法。

G1的堆内存分配

整个堆内存被分为一系列相同大小的堆内存区域(Region),每个区域都是一段连续的虚拟内存范围。类似于较老的收集器的堆内存分区,某些内存区域的集合也会被分配成相同的角色(Eden区、Survivor区和老年代),但是这些角色的内存空间并不是固定大小的。这种方式能够大大提高内存使用的灵活性。

当执行垃圾收集时,G1 的运行方式类似于 CMS 收集器。G1 会执行一个并发的全局标记阶段,以便于确定遍布堆内存的所有对象的存活情况。在标记阶段完成之后,G1 会知道哪些区域大部分是空闲的。它首先会在这些区域进行垃圾收集,通常会释放大量的空闲空间。这就是为什么这种垃圾收集方法被称为“垃圾优先”的原因。顾名思义,G1 将它的收集和整理活动集中在很有可能充满可回收对象的堆内存区域之上,这些可回收对象也被称为垃圾。G1 使用一种停顿预测模型来满足用户自定义的停顿时间目标,然后根据指定的停顿时间目标,选择要收集的堆内存区域(Region)的数量。

在时机成熟时,G1 会对需要回收的堆内存区域进行标记,然后使用排空法进行垃圾收集。G1 会将一个或更多个堆内存区域的存活对象拷贝至堆内存上的另一个区域之中,并在此过程中整理和释放内存空间。这种排空法是在多处理器上并行执行的,以便于减少停顿时间和增加吞吐量。因此,每次执行垃圾收集时,G1 都会为了减少内存碎片而持续工作,尽可能在用户自定义的停顿时间之内完成工作。这个特性超出了先前的两种垃圾收集方法的能力。CMS(并发标记-清除)垃圾收集器不能整理内存碎片。ParallelOld 垃圾收集器只能对整个堆内存进行碎片整理,这样便会导致相当长的停顿时间。

注意,G1 并不是一种实时的垃圾收集器,这一点非常重要。它能够高概率地满足设定的停顿时间目标,但是并不能绝对地满足这个目标。根据以前收集的数据,G1 会估计在用户指定的目标时间之内能够收集多少堆内存区域。因此,这个收集器就会有一个相当精确的收集堆内存区域的成本模型,并且会使用这个模型来确定在目标停顿时间之内,应该收集哪些堆内存区域,以及应该收集多少个堆内存区域。

注意:G1 既有并发(与应用程序的线程一起运行,例如:细化、标记和清理)阶段,又有并行(多线程,例如:stop the world)阶段。完整的垃圾收集(Full GC)仍然是单线程的,但是如果对你的应用程序进行适当地调优,那么应该能够避免 Full GC。

G1 内存占用量

如果你想要从 ParallelOld 或 CMS 收集器迁移至 G1,你将很有可能看到一个更大的 JVM 进程的大小。这在很大程度上与“记账”数据结构有关,例如:记忆集合(Remembered Set)和收集集合(Collection Set)。

  • 记忆集合(RSet):用于在一个给定的堆内存区域中跟踪对象引用。堆内存中的每个区域都有一个 RSet。RSet 使得一个堆内存区域能够并行和独立地进行垃圾收集。总的来说,RSet 对于内存占用量的影响不超过 5%。

  • 收集集合(CSet):将要在某次 GC 中进行垃圾收集的堆内存区域的集合。在执行 GC 期间,CSet 中的所有存活数据都会被排空(复制/移动)。堆内存区域的集合可以是 Eden 区、Survivor 区和/或老年代的。CSet 占用的 JVM 内存空间不超过 1%。

G1 的推荐用例

如果用户运行的应用程序需要大量的堆内存,同时要求 GC 延迟时间尽可能的短暂,那么 G1 能够向这类用户提供较为满意的解决方案。这就意味着,如果堆内存的容量有 6GB 或者更大,那么可预测的停顿时间能够稳定保持在 0.5 秒以下。

现在运行的大多数应用程序,一般会使用 CMS 或 ParallelOld 垃圾收集器,如果这些应用程序具有以下一个或多个特性,那么切换至 G1 收集器就能够改善性能:

  • Full GC 持续时间太长或者太频繁。
  • 为对象分配内存的速率,或者晋升对象的速率变化地非常明显。
  • 不希望发生长时间的垃圾收集,以及漫长的碎片整理停顿时间(长于 0.5 至 1 秒)。

注意:如果你正在使用 CMS 或ParallelOld 垃圾收集器,并且你的应用程序没有经历过垃圾收集导致的长时间停顿,那么仍然使用当前的垃圾收集器也是很好的。使用最新版本的 JDK 并不会强求一定要改为使用 G1 垃圾收集器。