Chapter 3.5 - 经典垃圾收集器
Created by : Mr Dk.
2020 / 01 / 26 16:26 🧨🧧
Ningbo, Zhejiang, China
3.5 经典垃圾收集器
到目前为止还没有最好的垃圾收集器出现,也不存在万能的收集器。
3.5.1 Serial 收集器
Serial 收集器是最基础、历史最悠久的收集器。该收集器是一个单线程工作的收集器,在进行垃圾收集时,必须暂停其它所有工作线程,直至收集结束,即 Stop The World。
相当于妈妈给你打扫房间,你只能要么在椅子上,要么在房间外面等着。
简单、高效。新生代 Serial 使用标记/复制算法,对于单核 CPU 或 CPU 核心数较少的环境来说,可以获得最高的单线程收集效率。
3.5.2 ParNew 收集器
是 Serial 收集器的多线程并行版本。在 GC 时,同时使用多个线程,在单核心处理器下,不可能会比 Serial 有更好的效果。是一个新生代收集器,已经退出历史舞台。
3.5.3 Parallel Scavenge 收集器
- 新生代收集器
- 基于标记/复制算法
- 并行收集的多线程收集器
收集器的目标不是缩短用于线程的停顿时间,而是达到一个可控制的吞吐量:CPU 用于运行用户线程的时间与 CPU 总耗时的比值。
用户停顿时间和吞吐量的区别
比如,把新生代内存区调小一些,每次 GC 可以更快,用户停顿时间较小;然而,这样会导致 GC 的频率变高。相同的一段时间内,GC 频率高了,虽然每次 GC 的时间短了,但总体上用于执行 GC 的时间多了,用于执行用户线程的 CPU 时间自然就减少了。
因此,在后台计算、不需要太多交互性的分析型任务应当更追求吞吐率。
3.5.4 Serial Old 收集器
是 Serial 收集器的老年代版本,是一个单线程收集器。使用标记/整理算法。
3.5.5 Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记/整理算法。
3.5.6 CMS (Concurrent Mark Sweep) 收集器
以获取 最短回收停顿时间 为目标的 老年代 收集器。一些基于 Web 的 Java 应用会较为关注服务的响应速度,希望停顿时间尽可能短,给用户带来良好的交互体验。CMS 收集器基于标记/清除算法实现,分为四个阶段:
- 初始标记 (CMS initial mark)
- Stop The World
- 标记 GC Roots 能直接关联到的对象,速度很快
- 并发标记 (CMS concurrent mark)
- 遍历对象图
- 耗时较长,但可以与用户线程并发
- 重新标记 (CMS remark)
- Stop The World
- 修正并发标记期间导致标记变动的对象
- 并发清除 (CMS concurrent sweep)
- 清理标记后已经死亡的对象
- 不需要移动存活对象,因此可以与用户线程并发
特性:并发收集、低停顿。
缺点:
- CMS 对 CPU 资源非常敏感
- 由于在并发时占用了一定线程,将导致应用程序变慢,总吞吐量降低
- 无法处理 浮动垃圾 (Floating Garbage)
- 在并发标记或并发清理阶段,用户线程还是会产生垃圾
- 这些垃圾只能在下一次 GC 时才被清理 - 浮动垃圾
- 如果此时用户线程需要的内存已经不够用了,就会触发 Stop The World 并进行一次 Full GC
- 并发失败 (Concurrent Mode Failure)
- 冻结用户线程,启动 Serial Old 进行老年代垃圾收集
- 因此,CMS 收集器在老年代使用到一定空间后就要被激活工作
- 基于标记/清除算法,导致大量内存碎片的产生
- 进而也会导致 Full GC
3.5.7 Garbage First (G1) 收集器
被 Oracle 官方称为全功能垃圾收集器 (Fully-Featured Garbage Collector),是一款主要面向 服务端应用 的垃圾收集器,希望能在未来替代 CMS 收集器。G1 可以面向堆内存的任何部分组成回收集 (Collection Set) 进行回收,不管是新生代还是老年代。衡量标准不再是分代,而是哪块内存的垃圾多,回收收益最大。
G1 不再坚持固定大小或固定数量的分代区域划分,而是将连续的 Java 堆划分为多个大小相等的独立区域 (Region),每个 Region 可以根据需要扮演新生代或老年代的角色。Region 中还有专门用于存储大对象的 Humongous 区域。Region 是单次回收的最小单元。G1 追踪每个 Region 中垃圾堆积的价值大小,以允许的收集停顿时间为参数,优先处理回收价值最大的 Region (Garbage First 的由来)。
为保证 G1 收集器在有限的时间内获得尽可能高的收集效率:
- Region 间的引用问题如何解决?
- 每个 Region 都维护自己的记忆集
- 存储了双向卡表 (我指向谁,谁指向我)
- G1 有着更高的内存负担
- 如何保证收集线程与用户线程互不干扰地运行?
- G1 使用 SATB 算法实现
- 划分出 Region 的一部分空间用于并发回收过程中的新对象分配
- 如果内存回收速度赶不上内存分配速度,G1 也要被迫冻结用户线程
- 如何建立可靠的停顿预测模型?
- G1 会记录每个 Region 的回收耗时等统计信息
- 预测哪些 Region 组成的回收集可以在不超过停顿时间的约束下获得最高的收益
G1 的运作过程可划分为以下四个步骤:
- 初始标记 (Initial Marking)
- 标记 GC Roots 能直接关联到的对象
- 需要 停顿线程,但耗时很短
- 并发标记 (Concurrent Marking)
- 从 GC Roots 开始对对堆中的对象进行可达性分析
- 最终标记 (Final Marking)
- 对用户线程做 短暂的暂停
- 处理并发结束后少量的 SATB 记录
- 筛选回收 (Live Data Counting and Evacuation)
- 更新 Region 的统计数据,对各 Region 的回收价值和成本进行排序
- 选择任意多个 Region 作为回收集,将决定回收的 Region 中的存活对象复制到空的 Region 中,再清理旧 Region 的全部空间
- 需要 暂停用户线程,由多个收集器线程并行完成
G1 的目标是在延迟可控的前提下,获得尽可能高的吞吐量,可以 由用户指定期望停顿时间 是 G1 的一个很强大的功能。但是也别自己作死把期望停顿时间调得很小,这样 G1 计算得出每次只能 GC 很少的 Region 才能满足停顿时间的要求,回收速度跟不上分配速度,导致垃圾堆积,最终导致 Full GC 反而降低性能。
从 G1 开始,垃圾收集器的设计导向变为,追求 GC 速度能够应付内存分配速率。
G1 的特性:
- G1 运作期间不会产生内存碎片
- 从整体来看是基于标记/整理算法实现的
- 从局部上看是基于标记/复制算法实现的
- 有利于程序长时间运行
- G1 为了 GC 而产生的内存占用和运行时额外开销都比 CMS 高
- G1 的记忆集消耗可能会占堆的 20% 乃至更多空间
- CMS 和 G1 都用到了写屏障
- 由于 G1 写屏障中的操作更为复杂
- CMS 的写屏障是直接的同步操作
- G1 的写屏障是消息队列 + 异步处理