垃圾回收器之CMS和G1

垃圾回收器之CMS和G1

1. 什么是垃圾

在介绍垃圾回收器之前,我们现需要明白什么是垃圾?

其实很简单,即在堆内存中,没有被引用的对象即是垃圾,当JVM发生GC时就会把这些垃圾给清除,释放堆内存空间。即内存中不再使用的对象

垃圾回收器之CMS和G1

那么JVM是如何找到内存中不再使用的对象呢?一般JVM提供了两种方法:

  1. 引用计数法:堆中每创建一个对象,就都会为该对象创建一个计数器,默认初始值为1。当有其他变量被赋值为该对象的引用时,数值加1。当一个对象实例的引用死亡或被赋值新值时,计数器减1。当计数值为0时,则表示该对象没有被引用,视为垃圾。
  2. 可达性分析算法:从GcRoot开始,寻找对应的引用节点,找到后继续寻找这个节点的引用结点,当所有引用节点寻找完毕后,剩余的节点就被认为是没有被引用的节点,即无用节点,无用节点被判定为可回收对象。

Java中可以作为GcRoot的包括下面几种:

  1. 虚拟机栈中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中应用的对象

首先来分析下引用计数法,这中方法较为容易理解,实现起来也并不复杂,看上去没什么问题,但是并不能解决循环引用的问题。

垃圾回收器之CMS和G1

从图中可以看出,当多个对象循环引用时,计数值是不等于0的,这是垃圾回收器会认为这几个对象不是垃圾,从而无法回收。但是真实情况是,这几个对象并没有被外界引用,是垃圾,应当被回收。

因此,一般是使用第二种方法来标记垃圾,即可达性分析法

垃圾回收器之CMS和G1

通过GCRoot寻找对应的引用节点,之后通过该节点继续找对应引用的节点,那么有用的对象就会被标记,剩下的没被标记的就是垃圾。

2. 垃圾回收算法

当找到垃圾后,JVM就要开始进行垃圾回收了,那么使用什么算法来回收垃圾也是个问题。

常见的垃圾回收算法

标记-清除法

从GcRoot开始搜描,对存活的对象进行标记。标记完后,再扫描整个空间中未被标记的对象,进行垃圾回收。标记-清除算法的缺点有两个:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

垃圾回收器之CMS和G1

标记-整理算法

该算法与标记-清除算法一样,但是在完成标记后,不直接清理可回收对象,而是将存活对象全部向一端移动,接着清理掉边界以外的内存。标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高。

垃圾回收器之CMS和G1

标记-复制算法

它将内存划分为大小相等的两块,每次只使用其中的一块。当这A快内存用完了,就将还存活的对象复制到B块上面,然后把A块的内存空间一次性清理掉。这种算法虽然实现简单,运行高效且不易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能使用的空间缩减为原来的一半。很显然,复制算法的效率跟存活对象的数量有很大关联,若存活对象很多,那么效率将大大降低。
垃圾回收器之CMS和G1

分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。其核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。将其分为年轻代、老年代和永久代。然后根据不同的区域采用合适的收集算法。

介绍垃圾回收器之前先补充下年轻代垃圾回收的算法

年轻代一般划分为三个区,即Eden区、Survivors1区、Survivors2区。当new一个对象时,一般是先放在Eden区。

当发生Young GC时,采用标记复制算法,即将Eden区和Survivors1区的对象标记,通过复制将有用的对象放在Survivors2区,之后再将Eden区和Survivors1区对象全部回收。回收完毕后,Survivors2区就变为Survivors1区,循环往复。

垃圾回收器之CMS和G1
垃圾回收器之CMS和G1

3. 垃圾回收器

CMS收集器

Concurrent Mark Sweep,以获取最短回收停顿时间为目标的收集器,基于并发“标记清理”实现。JDK1.7之前的默认垃圾回收算法,并发收集,停顿小。

优点:多线程回收,即并发回收,低停顿。

缺点:

  1. 对CPU非常敏感:在并发阶段虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢。
  2. 无法处理浮动垃圾:在最后一步并发清理过程中,用户线程执行也会产生垃圾,但是这部分垃圾是在标记之后,所以只有等到下一次gc的时候清理掉,这部分垃圾叫浮动垃圾。
  3. CMS使用“标记-清理”法会产生大量的空间碎片,当碎片过多,将会给大对象空间的分配带来很大的麻烦,往往会出现老年代还有很大的空间但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC,为了解决这个问题CMS提供了一个开关参数,用于在CMS顶不住,要进行FullGC时开启内存碎片的合并整理过程,但是内存整理的过程是无法并发的,空间碎片没有了但是停顿时间变长了。

CMS收集过程:

  1. 初始标记:独占PUC,仅标记GCRoot能直接关联的对象。
  2. 并发标记:可以和用户线程并行执行,标记所有可达对象。
  3. 重新标记:独占CPU(STW),对并发标记阶段用户线程运行产生的垃圾对象进行标记修正。
  4. 并发清理:可以和用户线程并行执行,清理垃圾。

垃圾回收器之CMS和G1

G1收集器

首先看下G1内存结构

垃圾回收器之CMS和G1

将堆内存分为大小相同的区,即Region(1M~32M)。每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。
年轻代、幸存区、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。在物理上不需要连续,则带来了额外的好处——有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾,这也就是G1名字的由来,即首先收集垃圾最多的分区。

其次需要理解几个概念:

Remembered set:Rset是指在每个Region中,有一部分空间是用来记录其它Region引用当前对象。

Collection set:记录本次GC需要清理的Region集合。

了解这些后再来看看G1垃圾收集过程:

在Young GC当中,使用的也是标记复制算法,和以前的垃圾回收器差不多。因此我们主要看Mix GC:

  1. 初次标记:也是标记GCRoot能直接关联的对象,即标记Region,记为RootRegion。(STW)
  2. RootRegion扫描:扫描所有Region,查看这些Rgion中的Rset是否有标记引用RootRegion。有,则标记出来。
  3. 并发标记:和CMS的并发标记相同,只不过遍历范围缩小,只需要便利RootRegion所引用的这些区。
  4. 重新标记:同CMS,只不过用了一种新的标记算法SATB。(STW)
  5. 复制清理:使用的是复制算法,因此也会发生STW。并且并不对所有的垃圾进行清理,而是选择垃圾较多的Region进行清理。因此会导致垃圾清理不干净,但是当下次GC时在清理就可以了。这么做的好处是,只需要清理垃圾较多的Region,提高GC效率。

G1垃圾收集的特点:

  1. 并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
  2. 分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。
  3. 空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。
  4. 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。