Java 虚拟机垃圾收集器(1)— 经典垃圾收集器

前言

没有完美的垃圾收集器,只有最适合具体应用的垃圾收集器。

1. Serial 收集器

新生代收集器,最基础且历史最悠久的收集器,在 JDK 1.3.1 之前是 HotSpot 的唯一选择。

工作图

Java 虚拟机垃圾收集器(1)— 经典垃圾收集器_第1张图片
图1. Serial 收集器(前)和 Serial Old(后)收集器运行示意图

参数

-XX:SurvivorRatio
Eden 区与 Survivor 区的比例。
-XX:PretenureSizeThreshold
晋升老年代对象大小。

优点

  • 足够简单,额外内存消耗最少
  • 在单核处理器或处理器核心较少时,单线程收集效率最高

缺点

  • 单线程 收集器
  • 进行垃圾收集时,必须暂停其他所有工作线程

使用场景

  • 单核处理器或处理器核心较少
  • 运行在客户端模式
  • 内存资源受限

2. ParNew 收集器

新生代收集器,使用标记 — 复制算法,实质上为 Serial 收集器的多线程并行版本。其控制参数、收集算法、暂停线程、对象分配规则和回收策略等都与 Serial 收集器一致,它们的实现代码也有相当多共用部分。

JDK 7 以前,它是不少运行在服务端模式下的 HotSpot 虚拟机首选的新生代收集器。
JDK 9 以前,只有 Serial 和 ParNew 能与 CMS 收集器配合工作,JDK 9 开始 ParNew 只能和 CMS 收集器绑定使用。

工作图

Java 虚拟机垃圾收集器(1)— 经典垃圾收集器_第2张图片
图2. ParNew(前)和 Serial Old(后)收集器运行示意图

参数

同 Serial 收集器

优点

  • 支持多线程并行收集

缺点

  • 进行垃圾收集时,必须暂停其他所有工作线程

3. Parallel Scavenge 收集器

新生代收集器,使用标记 — 复制算法,和 ParNew 收集器一样支持多线程并行收集,但 Parallel Scavenge 收集器更关注吞吐量,即 处理器用于运行用户代码的时间 / 处理器总消耗时间,此处总消耗时间为 运行用户代码的时间 + 运行垃圾收集时间

停顿时间越短,越适合需要与用户交互或需要保证服务响应质量的程序。
吞吐量越高,处理器资源利用率越高,越快完成程序的运算任务,适合不需要太多交互的后台运算、分析任务。

参数

-XX:MaxGCPauseMilliis
最大垃圾收集停顿时间,大于 0 的毫秒数。值不宜过小,会导致新生代空间缩小,以换取更短停顿时间,进一步导致更频繁的 GC,吞吐量下降。
-XX:GCTimeRatio
吞吐量大小。值为吞吐量的倒数,即垃圾收集时间占总时间的比率。
-XX:+UseAdaptiveSizePolicy
动态设置新生代大小、Eden 与 Survivor 区的比例、晋升老年代对象大小等参数,提供最合适停顿时间或最大吞吐量。

优点

  • 支持多线程并行收集
  • 自适应调节参数
  • 吞吐量相较于 ParNew 收集器更好

缺点

  • 进行垃圾收集时,必须暂停其他所有工作线程

工作图

Java 虚拟机垃圾收集器(1)— 经典垃圾收集器_第3张图片
图3. Parallel Scavenge(前)和 Serial Old(后)收集器运行示意图

4. Serial Old 收集器

老生代收集器,使用标记 — 整理算法,Serial Old 是 Serial 收集器的老年代版本,故也为单线程收集器。

工作图

Java 虚拟机垃圾收集器(1)— 经典垃圾收集器_第4张图片
图4. Serial 收集器(前)和 Serial Old(后)收集器运行示意图

优点

  • 足够简单,额外内存消耗最少
  • 在单核处理器或处理器核心较少时,单线程收集效率最高

缺点

  • 单线程 收集器
  • 进行垃圾收集时,必须暂停其他所有工作线程

使用场景

  • 单核处理器或处理器核心较少
  • 运行在客户端模式
  • JDK 5 及以前版本下,与 Paraellel Scavenge 收集器搭配使用
  • 作为 CMS 收集器失败时的后备预案

5. Parallel Old 收集器

老生代收集器,使用标记 — 整理算法,Parallel Old 是 Parallel Scavenge 收集器的老年代版本,故也为多线程收集器。

出现于 JDK 6,结束了此前 Parallel Scavenge 收集器只能搭配 Serial Old 收集器使用的状态。

工作图

Java 虚拟机垃圾收集器(1)— 经典垃圾收集器_第5张图片
图5. Parallel Scavenge 收集器(前)和 Parallel Old(后)收集器运行示意图

优点

  • 支持多线程并行收集
  • 自适应调节参数
  • 吞吐量相较于 ParNew 收集器更好

缺点

  • 进行垃圾收集时,必须暂停其他所有工作线程

使用场景

  • 注重吞吐量
  • 处理器资源不充足

6. CMS 收集器

老生代收集器,使用标记 — 清除算法,支持多线程并行收集,以最短回收停顿时间为目标的收集器。CMS 收集器在 JDK 5 中发布。

工作图

Java 虚拟机垃圾收集器(1)— 经典垃圾收集器_第6张图片
图6. CMS 收集器运行示意图

工作流程

1. 初始标记

仅标记 GC Roots 能直接关联到的对象,速度较快,但需要暂停用户线程。

2. 并发标记

从 GC Roots 直接关联对象开始遍历整个对象图的过程,耗时较长但不影响用户线程运行。采用 增量更新 的

3. 重新标记

修正并发标记期间,用户线程运行导致标记产生变动的那一部分对象的标记记录,速度略慢于初始标记,但远快于并发标记。

4. 并发清除

清理删掉标记阶段判断已经死亡的对象,不需要移动存活对象,故可与用户线程并发。

参数

-XX:CMSInitiatingOccupancyFraction
CMS 触发垃圾回收的内存占用比阈值

优点

  • 支持多线程并行收集
  • 停顿时间较短

缺点

  • 对处理器资源非常敏感
    CMS 默认启动的回收线程数为 (处理器核心数量 + 3) / 4。当处理器核心数在四个及以上时,只占用不超过 25% 的运行资源,且核心数越多,占用越低;但核心数不足四个时,回收线程对用户程序影响会变大。
  • 会产生浮动垃圾,有可能出现 并发失败,进而执行 Full GC,导致所有线程暂停
    并发清理阶段,用户线程并发执行,会伴随新的垃圾对象产生,但这部分垃圾出现在标记之后,无法在此次垃圾收集中清理,故留到下一次垃圾收集中清理,此部分垃圾被称为浮动垃圾。
    在并发垃圾收集时,用户线程需要持续进行,故需要预留足内存空间给用户线程使用,JDK 5 为 68%,偏保守;JDK 6 为 92%,又略激进,直接导致面临预留空间不足而导致并发失败,并需要启动 Serial Old 收集器进行老年代垃圾收集,暂停用户线程,造成更久的停顿时间。
  • 空间碎片多
    空间碎片多是标记 — 清除算法的通病,这将导致大对象容易找不到足够的空间进行分配,从而触发一次 Full GC,造成停顿。

使用场景

  • 追求停顿时间短的 B/S 系统

7. Garbage First(G1)收集器

G1 收集器可面向堆内存任何部分组成回收集进行回收,不再局限于新生代还是老年代。是垃圾收集器技术史上里程碑式的成果,JDK 7 Update 4 中正式商用。

G1 收集器基于 Region 堆内存布局,仍然是遵循分代收集理论设计,但 G1 不再以固定大小及数量划分分代区域,而是分成多个独立区域(Region)。每个 Region 根据需要扮演新生代的 Eden 空间、Survivor 空间或老年代空间,且 不需要连续,G1 收集器会对不同的角色进行不同策略的处理。

Region 中有一类特殊的 Humongous 区域,专门用来存储大对象。G1 收集器对大对象的定义为:超过一个 Region 容量一半的对象。当对象超过整个 Region 容量的超级大对象,将会被存放在多个连续的 Humongous 区域中,Humongous 区域被 G1 收集器当作老年代看待。

Region 是最小回收单元,每次回收的内存空间都是 Region 大小的整数倍。因此,G1 能建立可预测的停顿时间模型,有计划避免全区域垃圾收集(相当于 Full GC)。G1 收集器会根据回收所获得空间大小及所需时间的经验值,后台维护一个优先级列表,根据用户设定允许的收集停顿时间优先处理收益最大的 Region,故 G1 不追求一次性清理完整个堆,只需要保证收集速度跟得上对象分配速度。

设计思路

  • 局部收集
  • Region 内存布局

内存区域图

Java 虚拟机垃圾收集器(1)— 经典垃圾收集器_第7张图片
图7. Region 内存分区示意图

其中 E 为 Eden 区域,S 为 Survivor 区域,H 为 Humongous 区域。

工作图

Java 虚拟机垃圾收集器(1)— 经典垃圾收集器_第8张图片
图8. G1 收集器运行示意图.png

工作原理

1. 跨 Region 引用对象的解决

G1 收集器中,每个 Region 都会维护自己的记忆集,像其他收集器的记忆集一样,记忆集中记录下对其他 Region 的指针,还会记录别的 Region 对自己的指针,并标记这些指针在哪些卡页范围内,即为双向卡表。

G1 收集器的记忆集存储结构本质上是一种哈希表,Key 是别的 Region 的起始地址,Value 为一个集合,存储卡表的索引号。

因记忆集比其他传统收集器多很多,导致 G1 收集器有着更高的内存负担,根据经验,G1 至少需要 Java 堆容量 10% 到 20% 额外内存维持收集器工作。

2. 收集线程和用户线程的协调
  • 对象引用关系改变
    用户线程改变对象引用关系时,必须保证不能打破原本的对象图结构,导致标记错误,G1 采用 原始快照 算法实现。
  • 新对象创建
    G1 为每个 Region 设计了两个名为 TAMS 的指针,将 Region 中的一部分空间划分出来用于并发回收过程中新对象分配,并发回收时新分配对象地址都必须在这两个指针位置上。G1 默认在这个地址以上的对象是被隐式标记过,即默认存活,不纳入回收范围。
  • Full GC
    当内存回收速度赶不上内存分配速度时,G1 收集器和 CMS 收集器类似,会冻结用户线程执行,进行 Full GC 而产生停顿。
3. 建立可靠的停顿预测模型

用户通过 -XX:MaxGCPauseMillis 参数指定的停顿时间只意味着垃圾收集发生前的期望值。

G1 收集器停顿预测模型是以衰减均值为理论基础实现的,在垃圾收集过程中,G1 收集器会记录每个 Region 的回收耗时、每个 Region 记忆集中脏卡数量等各个可测量步骤花费的成本,并分析出平均值、标准偏差、置信度等统计信息。统计状态越新越能决定回收的价值,通过这些信息即可预测当前时间开始回收,通过哪些 Region 组成回收集可在不超过期望停顿时间的约束下获得最高收益。

工作流程

1. 初始标记

仅标记 GC Roots 能直接关联到的对象并修改 TAMS 指针的值,让下一个阶段用户线程并发运行时,能正确地在可用 Region 中分配新对象。速度较快,且是在 Minor GC 发生时同步完成,故实际上没有额外的停顿。

2. 并发标记

从 GC Roots 开始遍历整个对象图的过程,耗时较长但不影响用户线程运行。对象图扫描完后,还要重新处理 原始快照 记录下的在并发时有引用变动的对象。

3. 最终标记

对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量原始快照记录。

4. 筛选回收

负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程的,由多条收集器线程并行完成。

优点

  • 支持多线程并行收集
  • 停顿时间低,可动态设置预期值,且在延迟可控情况下,保证尽可能高的吞吐量
  • Region 内存布局使垃圾收集更加动态化,按回收收益动态回收
  • 不会产生内存空间碎片
    G1 整体看是基于标记 — 整理算法,但从 Region 局部看,是基于标记 — 复制算法,两种算法都无空间碎片。

缺点

  • 内存、CPU 负载都比 CMS 等其他收集器高

使用场景

  • 大内存应用
    根据经验,通常为堆容量 6GB 到 8GB 之间的应用,小内存应用中 CMS 收集器表现大概率好过 G1,但 G1 仍在不断更新优化中,未来可期。

你可能感兴趣的:(Java 虚拟机垃圾收集器(1)— 经典垃圾收集器)