【Java虚拟机】垃圾回收器
垃圾回收器
并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
GC发展历史
虚拟机需要有收集垃圾的机制(Garbage collection),对应的产品称为Garbage Collector(垃圾回收器)。
- 1999年随JDK1.3.1一起来的是串行方式的Serial GC,它是第一款GC, parNew垃圾收集器是Serial收集器的多线程版本(并发)。
- 2002年:2月26日,Para11e1GC和Concurrent Mark Sweep随JDK1.4.2一起发布。Para11el GC在JDK6之后成为HotSpot虚拟机默认垃圾回收器。
- 2012年,在JDK1.7u4版本中,G1可用。
- 2017年,JDK9中G1变成默认的垃圾收集器。
- 2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。
- 20年9月,JDK11发布:引入Epsilon垃圾回收器,又被称为"No-Op(无操作)"回收器。同时,引人ZGC:可伸缩的低延迟垃圾回收器(Experimental),专注于减少暂停时间的同时仍然压缩堆。
- 2019年3月,JDK12发布:增强G1,自动返回未用堆内存给操作系统。同时,引入ShenandoahGC:低停顿时间的GC(Experimental)。
- 2019年9月,JDK13发布:增强ZGC,自动返回未用堆内存给操作系统。
- 2020年3月,JDK14发布:删除CMS垃圾回收器。扩展ZGC在macos和windows上的应用。
- 2020年9月,JDK15发布:ZGC由实验阶段变成生成使用阶段。
- 2021年3月,JDK16发布:ZGC标记-压缩两种移动模式无缝切换(跨区移动,就地移动)。STW时间缩短到1ms以内。
经典垃圾回收器
HotSpot中的垃圾收集器:
如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1
各种垃圾收集器对比:
名称 | 类型 | 模式 | 全局 | 局部 | 描述 | |
---|---|---|---|---|---|---|
Serial | 新生代 | 客户端 | 1、单线程工作,在进行垃圾收集时,必须暂停其他所有工作线程。2、客户端模的默认新生代收集器。3、简单高效,内存消耗小,没有线程交互的开销。4、Serial 收集器对于运行在客户端模式下的虚拟机 来说是一个很好的选择。 | |||
ParNew | 新生代 | 服务端 | 1、Serial 收集器的多线程并行版本。 | |||
Serial Old | 老年代 | 标记整理 | 1、单线程收集器,通常用在客户端模式。2、在 服务端模式下,它也可能有两种用途:JDK 5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用,另外一种就是作为 CMS 收集器发生失败时的后备预案, 在并发收集发生 Concurrent Mode Failure 时使用。 | |||
Parallel Scavenge | 新生代 | 标记复制 | 1、吞吐量优先收集器,能够并行收集的多线程收集器,目标则是达到一个可控制的吞吐量,高吞吐量,具有自适应调节策略。2、Parallel Scavenge 收集器架构中本身有 PS MarkSweep 收集器来进行老年代收集,并非直接调用 Serial Old 收集器,但是这个 PS MarkSweep 收集器与 Serial Old 的实现几乎是一样的。 | |||
Parallel Old | 老年代 | 标记整理 | 1、支持多线程并发收集,通常用在服务器。2、在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器这个组合 | |||
CMS | 老年代 | 标记清除 | 增量更新 | 1、并发低停顿收集器,以获取最短回收停顿时间为目标的收集器。2、追求服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验,CMS 收集器就非常符合这类应用的需求。3、在 JDK 5 中使用 CMS 来收集老年代的时候,新生代只能选择 ParNew 或者 Serial 收集器中的一个,JDK9之后,ParNew 和 CMS 只能互相搭配使用。 | ||
G1 | 面向全堆 | 标记整理 | 标记复制 | 原始快照 | 1、全功能的垃圾收集器,通常用在服务端,不再需要其他新生代收集器的配合工作。2、CMS 继承者和替代者。3、开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式。4、JDK 9 发布之日,G1 宣告取代 Parallel Scavenge 加 Parallel Old 组合, 成为服务端模式下的默认垃圾收集器,而 CMS 则沦落至被声明为不推荐使用 (Deprecate)的收集器 |
Serial 收集器
Serial 收集器是最基础、历史最悠久的收集器,新生代收集器,单线程工作的收集器
优点:
- 简单高效
- 额外内存消耗最小
- 没有线程交互的开销
对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
缺点:
- 单线程工作
“单线程”的意义并不仅仅是说明它只会使用一个处理器 或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停 其他所有工作线程,直到它收集结束,即“Stop The World”。
应用场景:
迄今为止,它依然是 HotSpot 虚拟机 运行在客户端模式下的默认新生代收集器。Serial 收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。
ParNew 收集器
ParNew 收集器实质上是 Serial 收集器的多线程并行版本,支持多线程并行收集,运行在服务端模式下
除了同时使用多条线程进 行垃圾收集之外,其余的行为包括 Serial 收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一致,在实现上这两种收集器也共用了相当多的代码。
优点:
在处理器核心非常多的环境中,ParNew 对于垃圾收集时系统资源的高效利用还是很有好处
默认开启的收集线程数与处理器核心数量相同,可以使用-XX:ParallelGCThreads 参数来限制垃圾收集的线程数
在单核心处理器的环境中绝对不会有比 Serial 收集器更好的效果,甚至由于存在线程交互的开销,在通过超线程(Hyper-Threading)技术实现的伪双核处理器环境中都不能百分之百保证超越 Serial 收集器。
缺点:
和Serial收集器一样存在Stop The World问题
应用场景:
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。
JDK 9 开始,ParNew 加 CMS 收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。官方希望它能完全被 G1 所取代,甚至还取消了 ParNew 加 Serial Old 以及 Serial 加 CMS 这两组收集器组合的支持(其实原本也很少人这样使用),并直接取消了 XX:+UseParNewGC 参数,这意味着 ParNew 和 CMS 从此只能互相搭配使用,再也没有其他收集器能够和它们配合了。
ParNew 可以说是 HotSpot 虚拟机中第一款退出历史舞台的 垃圾收集器。
Parallel Scavenge 收集器
吞吐量优先收集器,新生代收集器,基于标记-复制算法,也是能够并行收集的多线程收集器,目标则是达到一个可控制的吞吐量,提供自适应调节策略。
特点:
Parallel Scavenge 收集器的特点是它的关注点与其他收集器不同,CMS 等收集器的 关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的 目标则是达到一个可控制的吞吐量(Throughput)
停顿时间越短就越适合需要与用户交互或需要保 证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。
精确控制吞吐量:
Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是
- 控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis 参数
- 直接设置吞吐量大小的-XX:GCTimeRatio 参数
-XX:MaxGCPauseMillis 参数,允许的值是一个大于 0 的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的
-XX:GCTimeRatio 参数,值应当是一个大于 0 小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数
自调节策略:
自适应调节策略也是 Parallel Scavenge 收集器区别于 ParNew 收集器的一 个重要特性。
- 控制自适应的调节策略开关的-XX: +UseAdaptiveSizePolicy 参数
-XX:+UseAdaptiveSizePolicy 参数,当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整细节参数以提供最合适的停顿时间或者最大的吞吐量
使用场景:
手工优化存在困难的话,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成是不错的选择。
只需要把基本的内存数据设置好(如-Xmx 设置最大堆), 然后使用-XX:MaxGCPauseMillis 参数(更关注最大停顿时间)或-XX:GCTimeRatio (更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚 拟机完成了
注意:
Parallel Scavenge 收集器架构中本身有 PS MarkSweep 收集器来进行老年代收集,并非直接调用 Serial Old 收集器,但是这个 PS MarkSweep 收集器与 Serial Old 的实现几乎是一样的
Serial Old 收集器
Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法,主要使用在客户端模式,也可在Server模式下使用。
应用场景:
- 在 JDK 5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用,
- 作为 CMS 收集器发生失败时的后备预案, 在并发收集发生 Concurrent Mode Failure 时使用
Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于 标记-整理算法实现。这个收集器是直到 JDK 6 时才开始提供,主要使用在服务器。
应用场景:
在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器这个组合
CMS 收集器
并发低停顿收集器,并发收集、 低停顿、基于标记-清除算法,JDK 5提供,以获取最短回收停顿时间为目标。
HotSpot 虚拟机中第一款真正意义上支持 并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作
应用场景:
对于关注服务的响应速度,希望系统停顿时间尽可能短,良好的交互体验的应用,CMS 收集器就非常符合这类应用的需求。
如web程序、b/s服务。
在 JDK 5 中使用 CMS 来收集老年代的时候,新生代只能选择 ParNew 或者 Serial 收集器中的一个。ParNew 收集器是激活 CMS 后(使用-XX:+UseConcMarkSweepGC 选项)的默认新生代收集器
可以使用-XX:+/- UseParNewGC 选项来强制指定或者禁用它。
但自 JDK 9 开始,ParNew 加 CMS 收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了,而且 ParNew 和 CMS 只能互相搭配使用。
运作过程分为四个步骤:
1、初始标记(CMS initial mark)
仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要“Stop The World”。
2、并发标记阶段(CMS concurrent mark)
从 GC Roots 的 直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线 程,可以与垃圾收集线程一起并发运行;
3、重新标记阶段(CMS remark)
修正并发标记期间, 因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远 比并发标记阶段的时间短,需要“Stop The World”。
4、并发清除阶段(CMS concurrent sweep)
清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以 与用户线程一起工作,所以从总体上来说,CMS 收集器的内存回收过程是与用户线程 一起并发执行的
三个明显的缺点:
1、CMS 收集器对处理器资源非常敏感
2、无法处理“浮动垃圾”(Floating Garbage),有可能出现 “Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的 Full GC 的产生
3、基于“标记-清除”算法实现 的收集器,这意味着收集结束时会有大量空间碎片产生,导致大对象无法分配空间,不得不提前触发一次Full GC。。
Garbage First 收集器
Garbage First(简称 G1),“全功能的垃圾收集器”(Fully-Featured Garbage Collector),基于停顿时间 模型”(Pause Prediction Model)的收集器,一个面向全堆的收集器,不再需要其他新生代收集器的配合工作, 它开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式,主要面向服务端应用的垃圾收集器,是CMS 的继承者和替代者。
JDK 9 发布之日,G1 宣告取代 Parallel Scavenge 加 Parallel Old 组合, 成为服务端模式下的默认垃圾收集器,而 CMS 则沦落至被声明为不推荐使用 (Deprecate)的收集器
Mixed GC 模式:可以面向堆内存任何部分来组成回收集(Collection Set,一 般简称 CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。
G1 收集器的运作过程大致可划分为以下四个步骤:
1、初始标记(Initial Marking)
仅仅只是标记一下 GC Roots 能直接关联到的对象, 并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行 Minor GC 的时候同步完成的,所以 G1 收集器在这个阶段实际并没有额外的停顿。
2、并发标记(Concurrent Marking)
从 GC Root 开始对堆中对象进行可达性分析,递 归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发 执行。当对象图扫描完成以后,还要重新处理 SATB 记录下的在并发时有引用变动的对 象。
3、最终标记(Final Marking)
对用户线程做另一个短暂的暂停,用于处理并发阶段 结束后仍遗留下来的最后那少量的 SATB 记录。
4、筛选回收(Live Data Counting and Evacuation)
负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的 存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
G1 收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控 的情况下获得尽可能高的吞吐量
与CMS的比较
优势:
1、可以指定最大停顿时间
2、分 Region 的内存布局、按收益动态确定回收集这些创新性设计带来的红利
3、运作期间不会产生内存空间碎片
劣势:
1、在用户程序运行过程中,G1 为了垃圾收集产生的内存占用(Footprint) 要比 CMS 要高。
2、程序运行时的额外执行负载(Overload)要比 CMS 要高。
低延迟垃圾收集器
Shenandoah 和 ZGC,低延迟垃圾收集器,都可以在任意可管理的堆容量下, 实现垃圾收集的停顿都不超过十毫秒的目标。
Shenandoah 收集器
Shenandoah 是一款只有 OpenJDK 才会包含,而 OracleJDK 里反而不存在的收集器,“免费开源版”比“收费商业版”功能更多,这是相对罕见的状况
项目要求用到 Oracle 商业支持的话,就不得不把 Shenandoah 排除在选择范围之外
与 G1 有三个明显的不同之处:
- 支持并发的整理算法
- 目前默认不使用分代收集
- Shenandoah 摒弃了记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨 Region 的引用关系
Shenandoah 收集器的工作过程大致可以划分为以下九个阶段
1、初始标记(Initial Marking)
与 G1 一样,首先标记与 GC Roots 直接关联的对象, 这个阶段仍是“Stop The World”的,但停顿时间与堆大小无关,只与 GC Roots 的数量相 关。
2、并发标记(Concurrent Marking)
与 G1 一样,遍历对象图,标记出全部可达的对象,这个阶段是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象 图的结构复杂程度
3、最终标记(Final Marking)
与 G1 一样,处理剩余的 SATB 扫描,并在这个阶段统 计出回收价值最高的 Region,将这些 Region 构成一组回收集(Collection Set)。最终标记阶段也会有一小段短暂的停顿。
4、并发清理(Concurrent Cleanup)
这个阶段用于清理那些整个区域内连一个存活对象都没有找到的 Region(这类 Region 被称为 Immediate Garbage Region)。
5、并发回收(Concurrent Evacuation)
并发回收阶段是 Shenandoah 与之前 HotSpot 中 其他收集器的核心差异。在这个阶段,Shenandoah 要把回收集里面的存活对象先复制一份到其他未被使用的 Region 之中。Shenandoah 将会通过读屏障和被称为“Brooks Pointers”的转发指针来解决复制对象与用户线程并行所面临的问题。并发回收阶段运行的时间长短取决于回收集的大小。
6、初始引用更新(Initial Update Reference)
并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。引用更新的初始化阶段实际上并未做什么具体的处理,设立这个阶段只是为了建立一个线程集 合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务 而已。初始引用更新时间很短,会产生一个非常短暂的停顿。
7、并发引用更新(Concurrent Update Reference)
真正开始进行引用更新操作,这个 阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引 用更新与并发标记不同,它不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值即可。
8、最终引用更新(Final Update Reference)
解决了堆中的引用更新后,还要修正存在于 GC Roots 中的引用。这个阶段是 Shenandoah 的最后一次停顿,停顿时间只与 GC Roots 的数量相关。
9、并发清理(Concurrent Cleanup)
经过并发回收和引用更新之后,整个回收集中所 有的 Region 已再无存活对象,这些 Region 都变成 Immediate Garbage Regions 了,最后 再调用一次并发清理过程来回收这些 Region 的内存空间,供以后新对象分配使用。
三个最重要的并发阶段(并发标记、并发回收、并发引用更新):
黄色的区域代表的是被选入回收 集的 Region,绿色部分就代表还存活的对象,蓝色就是用户线程可以用来分配对象的内 存 Region
参考
Shenandoah 发表的论文:
https://www.researchgate.net/publication/306112816_Shenandoah_An_opensource_concurrent_compacting_garbage_coll
Aleksey Shipilev 在 DEVOXX 2017 上的主题演讲《Shenandoah GC Part I: The Garbage Collector That Could》:
https://shipilev.net/talks/devoxx-Nov2017-shenandoah.pdf
博客:
Jvm垃圾回收器(终结篇) - 不二尘 - 博客园 (cnblogs.com)
JVM的垃圾回收机制 总结(垃圾收集、回收算法、垃圾回收器) - aspirant - 博客园 (cnblogs.com)
【GC收集器】和【内存分配与回收的策略】 - 倔强的老铁 - 博客园 (cnblogs.com)
JVM的垃圾回收机制 总结(垃圾收集、回收算法、垃圾回收器) - aspirant - 博客园 (cnblogs.com)
JVM Client模式和Server模式 - 知乎 (zhihu.com)
hotspot 虚拟机的server和client模式_hotspot client 模式-CSDN博客
Parallel Scavenge垃圾回收器线上调优实战 - 掘金 (juejin.cn)
GC之Parallel Scavenge收集器 - 简书 (jianshu.com)
为什么 JDK 8 默认使用 Parallel Scavenge 收集器? - 知乎 (zhihu.com)
深入理解JVM(二)——JVM在什么时候启动的-CSDN博客
深入理解Java之jvm启动流程-腾讯云开发者社区-腾讯云 (tencent.com)
JVM之G1回收器和常见参数配置 - 知乎 (zhihu.com)
JVM调优(二)经验参数设置 - 小勇DW3 - 博客园 (cnblogs.com)
GC参考手册 —— GC 调优(命令篇) - chen_hao - 博客园 (cnblogs.com)
JVM之G1回收器和常见参数配置 - 知乎 (zhihu.com)