专栏原创出处:github-源笔记文件 (opens new window)github-源码 (opens new window),欢迎 Star,转载请附上原文出处链接和本声明。

Java JVM-虚拟机专栏系列笔记,系统性学习可访问个人复盘笔记-技术博客 Java JVM-虚拟机 (opens new window)

# 前言

JDK 11 前 HotSpot 虚拟机所包含的全部可用的垃圾收集器及组合关系如下图:

# 关于并发与并行的概念声明

  • 并行:同一时间有多条垃圾收集线程工作,通常默认此时用户线程处于等待状态

  • 并发:同一时间垃圾收集线程与用户线程都在运行

# 吞吐量和回收停顿时间与回收算法的一些想法

  • 吞吐量 : 用户代码时间 / (用户代码时间 + 垃圾收集时间)

  • 回收停顿时间 :用户程序被暂停的时间,通常我们叫作「Stop The World」

一般评估一个垃圾收集器的标准主要看这两个指标,吞吐量越高越好、回收停顿时间越短越好。但是二者又互相矛盾:

  • 如果说为了高吞吐量,我们可以少进行 GC,不到万不得已不 GC。 但是如果这样,在真正需要 GC 时候累积了太多对象需要处理,GC 一次耗时又很长,回收停顿时间变大了。

  • 反过来如果为了最短回收停顿时间,我们不停的 GC ,系统资源的消耗会导致吞吐量下降。

因此:一个 GC 算法只可能针对两个目标之一(最大吞吐量或最小回收停顿时间),或尝试找到一个二者的折衷。

理解了这些就容易理解为什么会有这么多收集器,每个收集器解决的问题及应用场景。

# Serial/Serial Old 收集器

  • Serial 是一个新生代收集器,基于标记-复制算法实现
  • Serial Old 是一个老年代收集器,基于标记-整理算法实现
  • 两者都是单线程收集,需要「Stop The World」

应用场景:

  • 客户端模式下的默认新生代收集器
  • 对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的
  • 对于单核处理器或处理器核心数较少的环境来说,Serial 收集器由于没有线程交互的开销

# ParNew 收集器

ParNew 收集器是是一款新生代收集器。与 Serial 收集器相比,支持垃圾收集器多线程并行收集。

# Parallel Scavenge/Parallel Old 收集器

  • Parallel Scavenge 收集器是一款新生代收集器,基于标记-复制算法实现
  • Parallel Old 收集器是一款老年代收集器,基于标记-整理算法实现
  • 两者都支持多线程并行收集,需要「Stop The World」
  • 控制吞吐量为目标

# 特性

1.可控制的吞吐量

两个参数用于精确控制吞吐量,分别是

  • 控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis ( >0 的毫秒数)
  • 设置吞吐量大小的 -XX:GCTimeRatio(0-100 的整数)

2.自适应的调节策略

-XX:+UseAdaptiveSizePolicy 当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数, 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

# CMS 收集器

  • CMS(Concurrent Mark Sweep)是一个老年代收集器,基于标记-清除算法实现
  • 以最短回收停顿时间为目标

# 实现步骤

  1. 初始标记:
    标记一下 GC Roots 能直接关联到的对象,速度很快,需要「Stop The World」

  2. 并发标记:
    从 GC Roots 的直接关联对象开始遍历整个对象图,并发执行

  3. 重新标记:
    为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要「Stop The World」

  4. 并发清除:
    清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,并发执行

# 缺点

  • 并发阶段占用一部分线程,CMS 默认启动的回收线程数是(处理器核心数量 +3)/4
  • 并发标记和清理阶段,程序可能会有垃圾对象不断产生最终导致 Full GC
  • 为了支持并发标记和清理阶段程序运行,超过参数值 -XX:CMSInitiatingOccupancyFraction 后临时使用 Serial Old 收集器进行一次 Full GC
  • 基于标记-清除算法实现会有大量空间碎片产生,CMS 收集器不得不进行 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,因此无法并发执行。

# 应用场景

关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。

# G1 收集器

  • G1 是一款主要面向服务端应用的垃圾收集器。
  • 从整体来看是基于「标记-整理」算法实现的收集器,但从局部(两个 Region 之间)上看又是基于「标记-复制」算法实现
  • "G1 即是新生代又是老年代收集器",无需组合其他收集器。

较旧的垃圾收集器(Serial,Parallel,CMS)将堆分为三个部分:固定内存大小的年轻代,老年代和永久代 (元空间)


G1 收集器 Region 划分

# 特性

  • Region 区域:把连续的 Java 堆划分为多个大小相等的独立 Region,每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间

  • Humongous 区域:专门用来存储大对象。只要大小超过了一个 Region 容量一半的对象即可判定为大对象。

  • 基于停顿时间模型:消耗在垃圾收集上的时间大概率不超过 N 毫秒的目标(使用参数-XX:MaxGCPauseMillis 指定,默认值是 200 毫秒)

  • Mixed GC 模式:可以面向堆内存任何部分来组成「回收集」进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大

  • 无内存空间碎片:G1 算法实现意味着运作期间不会产生内存空间碎片

每个 Region 的大小可以通过参数-XX:G1HeapRegionSize 设定,取值范围为 1MB~32MB,且应为 2 的 N 次幂。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中,G1 的大多数行为都把 Humongous Region 作为老年代的一部分来进行看待

# 大致实现步骤

  1. 初始标记:
    仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。 需要「Stop The World」,但耗时很短,而且是借用进行 Minor GC 的时候同步完成的。

  2. 并发标记:
    从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理 SATB 记录下的在并发时有引用变动的对象。

  3. 最终标记:
    对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。

  4. 筛选回收:
    负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。需要「Stop The World」,多条收集器线程并行完成。

TAMS 指针/SATB 记录的概念请阅读《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第 3 版)》周志明, 3.4.6、3.5.7 内容。

  • TAMS 指针简单理解为:G1 为每一个 Region 设计了两个名为 TAMS(Top at Mark Start)的指针,把 Region 中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上
  • SATB 记录简单理解为:解决并发扫描时对象的消失问题

# G1 缺点

  • G1 内存占用比 CMS 高,每个 Region 维护一个卡表
  • G1 额外执行负载比 CMS 高,维护卡表的额外操作复杂

# G1 应用场景

G1 的首要重点是为运行需要大堆且 GC 延迟有限的应用程序的用户提供解决方案。这意味着堆大小约为 6 GB 或更大,并且稳定且可预测的暂停时间低于 0.5 秒。

如果应用程序具有以下一个或多个特征,那么今天运行 CMS 或并行压缩的应用程序将从切换到 G1 中受益。

  • 超过 50%的 Java 堆被实时数据占用。

  • 对象分配率或提升率差异很大。

  • 应用程序希望暂停时间低于 0.5 秒。

# G1 最佳实践

1.不要设置年轻代的大小

如果通过 -Xmn 显式地指定了年轻代的大小, 则会干扰到 G1 收集器的默认行为。

G1 在垃圾收集时将不再关心暂停时间指标。所以从本质上说,设置年轻代的大小将禁用暂停时间目标。 G1 在必要时也不能够增加或者缩小年轻代的空间。 因为大小是固定的,所以对更改大小无能为力。

2.响应时间指标

设置 XX:MaxGCPauseMillis=N 时不应该使用平均响应时间 (ART, average response time) 作为指标,而应该考虑使用目标时间的 90% 或者以上作为响应时间指标。也就是说 90% 的用户请求响应时间不会超过预设的目标值。暂停时间只是一个目标,并不能保证总是满足。

3.什么是转移失败 (Evacuation Failure)?

对 Survivors 或晋升对象进行 GC 时如果 JVM 的 Heap 区不足就会发生提升失败。堆内存不能继续扩充,因为已经达到最大值了。

当使用 -XX:+PrintGCDetails 时将会在 GC 日志中显示 to-space overflow。

这是很昂贵的操作!

  • GC 仍继续所以空间必须被释放
  • 拷贝失败的对象必须被放到正确的位置
  • CSet 指向区域中的任何 RSets 更新都必须重新生成 (regenerated)
  • 所有这些步骤都是代价高昂的
  • 如何避免转移失败 (Evacuation Failure)

4.要避免避免转移失败, 考虑采纳下列选项

  • 增加堆内存大小
  • 增加空闲空间的预留内存百分比 -XX:G1ReservePercent=n,默认值是 10%
  • 更早启动标记周期,通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。
  • 增加标记线程的数量 -XX:ConcGCThreads=n

# CMS 废弃说明

JDK 9 发布后,G1 宣告取代 Parallel Scavenge 加 Parallel Old 组合,成为服务端模式下的默认垃圾收集器,而 CMS(JDK 5) 则沦落至被声明为不推荐使用的收集器。

如果对 JDK 9 及以上版本的 HotSpot 虚拟机使用参数-XX:+UseConcMarkSweepGC 来开启 CMS 收集器的话,用户会收到一个警告信息,提示 CMS 未来将会被废弃:
Java HotSpot(TM) 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release.

# 收集器运行示意图

# 选择合适的收集器

选择收集器的考虑点:

  • 运行模式

    • 客户端模式,低内存(Serial/Serial Old)
    • 服务端模式
      • 高吞吐量
      • 低延迟
  • 硬件规格、系统架构

  • JDK 发行版本

一般情况,服务端模式下结合 JDK 发行版本,通常我们使用最新的垃圾回收器。

JDK9 前,我们会在 CMS 和 G1 间选择,对于大概 4GB 到 6GB 以下的堆内存,CMS 一般能处理得比较好,而对于更大的堆内存,可重点考察一下 G1

# 部分参数总结

  • -XX:+UseSerialGC 在新生代和老年代使用串行收集器
  • -XX:+UseParNewGC 在新生代使用并行收集器
  • -XX:+UseParallelGC 新生代使用并行回收收集器,更加关注吞吐量
  • -XX:+UseParallelOldGC 老年代使用并行回收收集器
  • -XX:ParallelGCThreads 设置用于垃圾回收的线程数
  • -XX:+UseConcMarkSweepGC 新生代使用并行收集器,老年代使用 CMS+串行收集器
  • -XX:ParallelCMSThreads 设定 CMS 的线程数量

# G1 相关参数

  • -XX:+UseG1GC
    启用 G1 垃圾回收器

  • -XX:InitiatingHeapOccupancyPercent=<45>
    当整个 Java 堆的占用达到参数的值时,开始并发标记阶段

  • -XX:MaxGCPauseMillis=200
    G1 暂停时间目标 ( >0 的毫秒数)

  • -XX:NewRatio=n
    新生代与老生代 (new/old generation) 的大小比例 (Ratio). 默认值为 2

  • -XX:SurvivorRatio=n
    Eden/Survivor 空间大小的比例 (Ratio). 默认值为 8

  • -XX:MaxTenuringThreshold=n
    提升年老代的最大临界值 (tenuring threshold). 默认值为 15

  • -XX:ParallelGCThreads=n
    设置垃圾收集器在并行阶段使用的线程数,默认值随 JVM 运行的平台不同而不同

  • -XX:ConcGCThreads=n
    并发垃圾收集器使用的线程数量。 默认值随 JVM 运行的平台不同而不同

  • -XX:G1ReservePercent=n
    作为空闲空间的预留内存百分比,以降低目标空间溢出的风险。默认值是 10%

  • -XX:G1HeapRegionSize=n
    指定每个 Region 的大小。默认值将根据 heap size 算出最优解。最小值为 1Mb, 最大值为 32Mb

关于收集器的选择与相关参数配置,专栏另篇文章介绍。

# 参考

最后修改时间: 2/17/2020, 4:43:04 AM