JVM 基础 (5) -- 垃圾收集器

1. Serial 收集器

Serial 收集器是一个单线程的垃圾收集器,也就是说它只会使用一条线程进行 GC,并且在垃圾收集的同时会暂停所有的用户线程直到 GC 结束,采用 复制算法。
它的特点就是简单高效,并且在单 CPU 环境下,因为没有线程交互的开销,所以可以获得最高的单线程垃圾收集效率。也因此该垃圾收集器仍然是 JVM 运行在 Client 模式下,新生代默认的垃圾收集器。

2. Serial Old 收集器

Serial Old 收集器是 Serial 收集器的老年代版本,它同样是个单线程的收集器,使用 标记 - 整理 算法,这个收集器主要是 JVM 运行在 Client 模式下,老年代默认的垃圾收集器
在 Server 模式下,主要有两个用途:

  1. 在 JDK 1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用
  2. 作为年老代中使用 CMS 收集器的后备方案(后面 CMS 收集器会提原因)

新生代使用 Serial 收集器与老年代使用 Serial Old 收集器的垃圾收集过程图:
在这里插入图片描述

3. ParNew 收集器

ParNew 收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,该收集器是大多数 JVM 运行在 Server 模式下,新生代默认的垃圾收集器
ParNew 收集器默认开启和 CPU 数目相同的线程数,我们也可以通过设置 -XX:ParallelGCThreads 参数来控制用于 GC 的线程数量
在这里插入图片描述

4. Parallel Scavenge 收集器

Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也会引起 Stop-The-World,是一个多线程的 、吞吐量优先的垃圾收集器

吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
高吞吐量可以最高效率地利用 CPU,尽快地完成程序的运算任务,所以该收集器主要适用于在后台运算而不需要太多交互的任务

Parallel Scavenge 收集器提供了两个参数用于精准控制吞吐量:

  1. -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,是一个大于 0 的毫秒数
  2. -XX:GCTimeRation:设置吞吐量大小,是一个大于 0 小于 100 的整数,也就是程序运行时间占总时间的比率,默认值是 99,即垃圾收集时间最多为总时间的 1%(1/(1+99))

它还提供一个参数:-XX:+UseAdaptiveSizePolicy,这是个开关参数,打开之后就不需要手动指定新生代大小(-Xmn)、Eden 与 Survivor区的比例(-XX:SurvivorRation)、对象从新生代晋升到年老代的年龄(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统运行情况收集性能监控信息,然后动态调整这些参数以达到最大吞吐量,这种方式称为 GC 自适应调节策略,自适应调节策略也是 Parallel Scavenge 收集器与 ParNew 收集器的一个重要区别

新生代使用 Parallel Scavenge/ParNew 收集器与年老代使用 Serial Old 收集器的垃圾收集过程图:
在这里插入图片描述

5. Parallel Old 收集器

Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多线程的 标记-整理 算法,在 JDK1.6 才开始提供
在 JDK1.6 之前,新生代使用 Parallel Scavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge 和年老代 Parallel Old 收集器的搭配策略

新生代使用 Parallel Scavenge 收集器和年老代使用 Parallel Old 收集器的垃圾收集过程图:
在这里插入图片描述

6. CMS 收集器

以上的多线程收集器都只能称为 并行收集器,而 CMS 则是Sun 公司的 HotSpot 虚拟机中第一款真正意义上的 并发收集器,它允许我们的用户线程和 GC 线程同时工作,极大地减少了 GC 停顿时间,提高了用户体验。
它是一种老年代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他老年代垃圾收集器使用的 标记-整理 算法不同,它使用多线程的 标记-清除 算法。

CMS 的工作过程大致可以分为 7 个阶段:

  1. 初始标记:暂停用户线程,记录下老年代中所有与 GC Roots 直接相连的对象以及由新生代中存活对象直接引用的老年代中的对象,这个阶段速度很快。
    在这里插入图片描述

  2. 并发标记:同时开启 GC 线程和用户线程,然后用一个闭包结构去记录所有与 GC Roots 可达的对象,但是因为这个阶段用户线程同时也在运行,可能会导致一些引用发生改变,所以这个阶段结束之后,并不能标记所有存活的对象,在并发标记过程中发生了引用关系变化的区域,JVM会通过“Card”标记为“脏”区,这就是所谓的卡片标记,Card Marking
    在这里插入图片描述

  3. 并发预清理:同时开启 GC 线程和用户线程,统计出脏对象,标记出与他们可达的对象,并清空脏的 Card
    在这里插入图片描述

  4. 并发可取消的预清理:同时开启 GC 线程和用户线程,这阶段尽可能承担更多的并发预处理工作,从而减轻在最终标记阶段的 Stop-The-World
    主要循环做两件事:

    • 处理 From 和 To 区的对象,标记可达的老年代对象;
    • 和上一个阶段一样,扫描处理 Dirty Card 中的对象。

    具体执行多久,取决于许多因素,满足其中一个条件将会中止运行:

    1. 执行循环次数达到了阈值;
    2. 执行时间达到了阈值;
    3. 新生代 Eden 区的内存使用率达到了阈值
  5. 最终标记:暂停用户线程,然后对于 Card Table 或者 Mod Union Table 是 dirty 的 Card 、并发标记阶段由新生代晋升到老年代的对象,会把它们作为 GC Roots 进行扫描,标记所有与这些 roots 可达的对象,大多数的 Dirty Card 在预清理阶段已经被处理过了,这个阶段只有很少的 Dirty Card要处理。这个阶段可以理解成是对并发标记阶段的修正,并且这个阶段的耗时比初始标记阶段长,但远短于并发标记阶段。
    需要注意的是,这个阶段并不会处理那些在并发标记阶段被标记为可达,但因为用户线程继续运行而导致最终变为垃圾的对象,这些垃圾称为浮动垃圾,他们只能等到下一次 GC 的时候才能被回收掉。

  6. 并发清除:同时开启 GC 线程和用户线程,然后对标记的区域进行清扫,这个阶段也会产生浮动垃圾。
    在这里插入图片描述

  7. 并发重置:同时开启 GC 线程和用户线程,清理并恢复在 CMS GC 过程中的各种状态,重新初始化 CMS 相关数据结构,为下一个垃圾收集周期做好准备。

由于耗时最长的并发标记和并发清除阶段,GC 线程和用户线程是一起并发工作的,所以总体上来看 CMS 收集器的 GC 线程和用户线程是一起并发地执行的。
这个收集器的优点就是 并发收集 、低停顿。
缺点:

  1. 对 CPU 资源非常敏感,默认启动的收集线程数 = ( CPU 数量 + 3 ) / 4,所以在用户程序本来 CPU 负荷已经比较高的情况下,如果还要分出 CPU 资源用来运行垃圾收集器线程,会使得 CPU 负载加重。
  2. CMS 无法处理浮动垃圾,可能会导致 Concurrent Model Failure 失败进而导致另一次 Full GC。
    由于 CMS 收集器和用户线程同时进行,老年代 GC 的过程中会伴随着多次年轻代 GC,所以在老年代 GC 期间,可能有一些大对象,长期存活的对象,To Survivor 空间无法容纳的存活对象会直接进入老年代,因此 CMS 垃圾收集器不能像其他垃圾收集器那样等待老年代完全被填满之后再进行 GC,它需要预留一部分空间供并发收集时使用,可以通过参数 -XX:CMSInitiatingOccupancyFraction 来设置老年代空间达到多少的百分比时触发 CMS 进行垃圾收集,默认是 68%,jdk 1.8 开始默认是 92%。如果在 CMS 运行期间,预留的内存无法满足程序的需要,就会出现一次 Concurrent Model Failure 失败,进而触发 Full GC,此时虚拟机将启动预备方案,使用 Serial Old 收集器重新对老年代进行垃圾回收。
  3. CMS 收集器是基于 标记-清除 算法的,因此不可避免会产生大量的内存碎片,如果无法找到一块足够大的连续内存存放对象的话,将会触发 Full GC。CMS 提供一个开关参数 -XX:+UseCMSCompactAtFullCollection,用于指定在 Full GC 之后进行内存整理,因为内存整理会使得垃圾收集停顿时间变长,所以 CMS 提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,用于设置在执行多少次不压缩的 Full GC 之后,跟着再来一次内存整理。
    还有就是正因为在它释放了垃圾对象占用的空间后,不会移动存活对象到一边去,虽然这么做节省了垃圾回收的时间,但是由于之后空闲空间不是连续的,所以也就不能使用简单的 指针碰撞(bump-the-pointer) 进行对象空间分配了。它需要维护一个 空闲列表,将所有的空闲区域连接起来,当分配空间时,需要寻找到一个可以容纳该对象的区域。显然,它比使用简单的指针碰撞成本要高。同时它也会加大年轻代垃圾收集的负载,因为年轻代中的对象如果要晋升到老年代中,需要老年代进行空间分配。

CMS 收集器垃圾收集过程图:
在这里插入图片描述

1. Minor GC 时处理跨代引用的解决方案

在进行年轻代 GC 时,如果年轻代中的 Y 对象被老年代中 O 对象引用,那么称 O 对象存在跨代引用,而且 Y 对象在本次垃圾回收中会存活下来,所以老年代中的对象在年轻代 GC 时也是 GC roots 的一部分,但是如果每次年轻代 GC 都要去扫描老年代中所有对象的话,肯定会非常耗时。
如果只扫描那些有年轻代对象引用的对象,那效率就可以达到最高,不过使用这种方式,需要有一个地方保存这些对象的引用,是一个不小的内存开销,所以在 Hotspot 实现中,并没采用这样的方式,而是使用一个 GenRemSet 的数据结构,记录包含这些对象的内存区域是 clean 或者 dirty 状态。
在这里插入图片描述
CardTable 是 GenRemSet 的一种实现。
在这里插入图片描述
卡页其实就是我们说的 Card。
GenRemSet 随着堆内存一起初始化,通过具体的垃圾收集策略进行创建,比如 CMS 和 G1 是不一样的,其中 CMS 对应的是 CardTable。
在年轻代 GC 时,会对 Card Table 进行扫描。

2. 关于 Mod Union Table

老年代 GC 的过程中,其实伴随着多次年轻代 GC。
当新生代 GC 开始运行的时候,他也需要去扫描 CardTable,在扫描的时候,也是要对标记为 dirty 的 Card 进行分析,如果这个 Card 没有了对新生代的引用,那么新生代 GC 就会把它标记为 clean,但是这样子将会导致 CMS 收集器在最终标记阶段不会去扫描这个 Card。也就是出现了标记遗漏的情况。
为了解决上述的问题,就引入了 Mod Union Table,它是一个位向量,每个单元的大小只有 1 位,每个单元对应一个 Card,在新生代 GC 处理 dirty Card 之前,会先把该 Card 在 Mod Union Table 里面的对应项置为 dirty。这样,CMS 在执行最终标记阶段的时候,就会扫描 Mod Union Table 和 CardTable 里面被标记为 dirty 的项。

注意:

  1. 三色标记法和卡片标记法不同,三色标记法的作用是标记存活对象,卡片标记法是记录跨代引用或者是记录发生变更的引用
  2. CMS 也使用三色标记法标记对象

7. G1 收集器

比较重要,另起一篇文章讲!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值