Appearance
垃圾收集器
内容来源:本篇为学习整理,转载自网络公开技术文章(原始出处未存档,版权归原作者所有); 结构与表述有编辑加工。
看在前面
前言
JDK 11 前 HotSpot 虚拟机所包含的全部可用的垃圾收集器及组合关系如下图:
<img width="878" alt="截屏2023-11-30 14 14 57" src="https://github.com/Mein-Augenstern/MUYI/assets/34135120/36cfd810-9f4c-44aa-b6ad-2679fda32fcb">
关于并发与并行的概念声明
并行:同一时间有多条垃圾收集线程工作,通常默认此时用户线程处于等待状态
并发:同一时间垃圾收集线程与用户线程都在运行
吞吐量和回收停顿时间与回收算法的一些想法
吞吐量 : 用户代码时间 / (用户代码时间 + 垃圾收集时间)
回收停顿时间 :用户程序被暂停的时间,通常我们叫作「Stop The World」
一般评估一个垃圾收集器的标准主要看这两个指标,吞吐量越高越好、回收停顿时间越短越好。但是二者又互相矛盾:
如果说为了高吞吐量,我们可以少进行 GC,不到万不得已不 GC。 但是如果这样,在真正需要 GC 时候累积了太多对象需要处理。GC 一次耗时又很长,回收停顿时间变大了。
反过来如果为了最短回收停顿时间,我们不停的 GC ,系统资源的消耗会导致吞吐量下降。
因此:一个 GC 算法只可能针对两个目标之一(最大吞吐量或最小回收停顿时间)。或尝试找到一个二者的折衷。
理解了这些就容易理解为什么会有这么多收集器,每个收集器解决的问题及应用场景。
Serial/Serial Old 收集器
Serial 是一个新生代收集器,基于标记-复制算法实现
Serial Old 是一个老年代收集器,基于标记-整理算法实现
两者都是单线程收集,需要「Stop The World」
应用场景:
客户端模式下的默认新生代收集器
对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的
对于单核处理器或处理器核心数较少的环境来说,Serial 收集器由于没有线程交互的开销
ParNew 收集器
ParNew 收集器是是一款新生代收集器。与 Serial 收集器相比,支持垃圾收集器多线程并行收集。ParNew垃圾回收器更适合与CMS收集器一起在Web服务器和实时应用中使用。用途是减少垃圾回收期间的停顿时间。
补充知识
ParNew和Parallel Scavenge都是Java虚拟机(JVM)中的垃圾回收器(Garbage Collector,GC),它们都用于回收新生代(Young Generation)的垃圾。尽管两者的目标相似,即减少应用程序的停顿时间并提高垃圾回收的效率。但它们在实现和使用的场景上有一些差异。
1、ParNew(Parallel New Generation)垃圾回收器:
- ParNew是CMS(Concurrent Mark-Sweep)垃圾回收器的新生代版本,它是多线程的。
- ParNew可以与老年代的CMS垃圾回收器配合使用,通常在需要较短停顿时间的场景中使用。
- ParNew适用于多核CPU环境,因为它可以利用多核资源并行地执行垃圾回收。
- ParNew垃圾回收器在Java 9中已被标记为废弃(deprecated)。
2、Parallel Scavenge(Parallel Young Generation)垃圾回收器:
- Parallel Scavenge也是一个新生代垃圾回收器,它同样采用并行的方式来回收垃圾。但它更关注于达到一个可控的吞吐量(Throughput)。
- 吞吐量是指CPU用于运行应用程序的时间与CPU总消耗时间的比值。即:运行应用代码的时间比上应用代码与垃圾收集的时间总和。
- Parallel Scavenge收集器经常与老年代的Parallel Old垃圾回收器一起使用。使用场景:需要高吞吐量(例如后台处理与批处理系统)。
- Parallel Scavenge收集器提供了几个参数来精细控制吞吐量和暂停时间,比如 -XX:GCTimeRatio 和 -XX:MaxGCPauseMillis。
总的来说,ParNew垃圾回收器更适合与CMS收集器一起在Web服务器和实时应用中使用。用途是减少垃圾回收期间的停顿时间。而Parallel Scavenge垃圾回收器则适用于需要高吞吐量的场景,如后台计算型任务。它允许开发人员更多地控制暂停时间与吞吐量之间的平衡。
随着Java版本的更新和发展,JVM引入了新的垃圾回收器,如G1(Garbage-First)和ZGC(Z Garbage Collector)。它们提供了更先进的垃圾回收技术,针对不同的使用场景和性能要求。
Parallel Scavenge/Parallel Old 收集器
Parallel Scavenge 收集器是一款新生代收集器,基于标记-复制算法实现
Parallel Old 收集器是一款老年代收集器,基于标记-整理算法实现
两者都支持多线程并行收集,需要「Stop The World」
控制吞吐量为目标
特性
- 可控制的吞吐量
两个参数用于精确控制吞吐量,分别是
控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis ( >0 的毫秒数)
设置吞吐量大小的 -XX:GCTimeRatio(0-100 的整数)
- 自适应的调节策略
-XX:+UseAdaptiveSizePolicy 当这个参数被激活之后,就不需要人工指定以下细节参数:
- 新生代的大小(-Xmn)
- Eden 与 Survivor 区的比例(-XX:SurvivorRatio)
- 晋升老年代对象大小(-XX:PretenureSizeThreshold)等
虚拟机会根据当前系统的运行情况收集性能监控信息。动态调整这些参数,以提供最合适的停顿时间或者最大的吞吐量。
CMS 收集器
CMS(Concurrent Mark Sweep)是一个老年代收集器,基于标记-清除算法实现
以最短回收停顿时间为目标
实现步骤
初始标记:
标记 GC Roots 能直接关联到的对象,速度很快,需要「Stop The World」
并发标记:
从 GC Roots 的直接关联对象开始遍历整个对象图,并发执行
重新标记:
为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。此阶段需要「Stop The World」
并发清除:
清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,并发执行
缺点
并发阶段占用一部分线程,CMS 默认启动的回收线程数是(处理器核心数量 +3)/4
并发标记和清理阶段,程序可能会有垃圾对象不断产生最终导致 Full GC
为了支持并发标记和清理阶段程序运行,超过参数值 -XX:CMSInitiatingOccupancyFraction 后临时使用 Serial Old 收集器执行一次 Full GC
基于标记-清除算法实现会有大量空间碎片产生。CMS 收集器不得不进行 Full GC 时启动内存碎片的合并整理过程。由于这个内存整理必须移动存活对象,因此无法并发执行。
CMS 专属参数(-XX:CMSInitiatingOccupancyFraction、-XX:+UseCMSInitiatingOccupancyOnly、-XX:CMSFullGCsBeforeCompaction、-XX:+CMSScavengeBeforeRemark 等)的源码级详解,见 CMS垃圾收集器参数。
应用场景
关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。
补充知识
1、promotion failed以及concurrent mode failure 在JVM中是什么意思?
在Java虚拟机(JVM)中执行垃圾回收(Garbage Collection,GC)时,promotion failed 和 concurrent mode failure 是两种可能出现的问题。它们通常与使用 Concurrent Mark Sweep(CMS)垃圾回收器时相关。
1)Promotion Failed(晋升失败):
当一个对象从年轻代(Young Generation)晋升到老年代(Old Generation)时,需要在老年代中找到足够的连续内存空间。这个空间用来存放该对象。如果在老年代中找不到足够的空间来存放要晋升的对象,这个过程就会失败。这就是所谓的“晋升失败”(Promotion Failed)。晋升失败通常会触发一次完整的垃圾回收(Full GC),以清理老年代中的空间。
2)Concurrent Mode Failure(并发模式失败):
CMS垃圾回收器的设计目的是尽量减少应用程序停顿时间,它通过并发的方式清理老年代。并发模式意味着垃圾回收线程和应用程序线程会同时运行。然而,在并发阶段有两种情况会触发“并发模式失败”(Concurrent Mode Failure)。一是老年代被填满,没有足够的空间分配给新晋升的对象。二是垃圾回收器无法在应用程序分配对象的速度上跟上清理速度。
并发模式失败会导致CMS回退到停止-全世界(Stop-The-World,STW)的模式。它执行一次完整的垃圾回收,以清理出足够的空间。
这两种情况都会导致JVM的停顿时间增加,因为需要执行完整的垃圾回收。这对于那些需要低延迟的应用程序来说可能是个问题。
为了避免这些问题,可以考虑以下几种解决方案:
- 适当增加老年代的大小,给对象晋升提供更多的空间。
- 调整CMS的启动条件,更早地开始垃圾回收,留下更多的空间来处理晋升的对象。
- 考虑使用其他类型的垃圾回收器,比如G1(Garbage-First)收集器。它设计上更适合处理大堆内存,且可以更平滑地管理内存晋升。
- 使用JVM监控工具监视内存使用情况,以便及时调整和优化垃圾回收策略。
需要注意的是,随着Java 9和更高版本的发布,CMS垃圾回收器已经被标记为废弃(deprecated)。并且在最新的Java版本中可能已经被移除。G1垃圾回收器通常被推荐用作CMS的替代品,并且在较新的Java版本中成为默认的垃圾回收器。
2、CMS垃圾回收器可以用于新生代么
不,CMS(Concurrent Mark-Sweep)垃圾回收器不能用于新生代。CMS是专门为Java堆中的老年代(Old Generation)设计的垃圾回收器。它的主要目标是减少老年代垃圾回收时的停顿时间。这使应用程序能够在垃圾回收期间仍然保持较高的响应性。
CMS工作流程主要包括以下几个阶段:
- 初始标记(Initial Mark):这是一个需要暂停所有工作线程(Stop-The-World pause)的阶段,但是此暂停通常很短。
- 并发标记(Concurrent Mark):在这个阶段,CMS回收器在应用线程运行的同时,并发地标记出所有存活的对象。
- 重新标记(Remark):为了标记在并发标记阶段产生的变动,这个阶段将暂停应用线程。此暂停也比全堆垃圾回收时短。
- 并发清除(Concurrent Sweep):在这个阶段,CMS回收器并发地清除未被标记的对象,释放空间。
对于新生代的垃圾回收,JVM提供了其他的回收器,如:
- Serial:单线程回收器,适用于单核处理器或者小型堆内存的场景。
- ParNew:基于Serial回收器,但是多线程执行,经常与CMS一起使用作为新生代的垃圾回收器。
- Parallel Scavenge:也是多线程回收器,注重达到一个可控制的吞吐量。和Parallel Old回收器一起使用处理新生代和老年代。
CMS的设计焦点是减少老年代垃圾回收的停顿时间。它并不适合新生代的对象高速分配和回收特性。在实践中,ParNew经常是与CMS搭配使用的新生代垃圾回收器。因为ParNew是多线程的,能够有效地处理多核心下新生代的并行垃圾回收工作。
G1 收集器
术语介绍
在G1(Garbage-First)垃圾收集器的上下文中,"Region" 和 "Humongous" 都是与其内部工作机制相关的术语:
Region (区域):G1垃圾收集器将Java堆划分为多个大小相等的区域(Regions)。这些区域是G1管理堆内存的基本单元。每个区域可以是Eden、Survivor或Old Generation中的一部分。这样的设计使得G1能够更灵活地管理内存。它只回收那些包含较多垃圾的区域,而不是整个Java堆。从而减少了单次垃圾回收的停顿时间。
Humongous (巨型):G1收集器中的“Humongous”区域是指专门用于存储大对象的区域。在G1的术语中,任何比一个区域一半还大的对象都被视为巨型对象(Humongous Object)。巨型对象会直接被分配到连续的Humongous区域中,以避免在小的区域之间拆分。当这些区域填满后,G1会触发垃圾回收来回收空间。这种方法的一大优点是可以快速分配大对象。缺点是可能会造成内存的碎片化。因为即使只有很小一部分的巨型对象还在使用,整个Humongous区域也不能被回收。
G1垃圾收集器的目标是为了提供一种可以预测暂停时间的收集器。它允许用户指定期望的停顿时间目标,并尽可能在这个时间范围内完成垃圾回收。这对于需要低延迟的应用程序来说非常有用。通过将堆分为多个区域并特别处理大对象。G1提供了一种在吞吐量和停顿时间之间平衡的垃圾回收方式。
特性
G1 是一款主要面向服务端应用的垃圾收集器。
从整体来看是基于「标记-整理」算法实现的收集器,但从局部(两个 Region 之间)上看又是基于「标记-复制」算法实现
"G1 即是新生代又是老年代收集器",无需组合其他收集器。
<img width="900" alt="截屏2023-11-30 14 15 57" src="https://github.com/Mein-Augenstern/MUYI/assets/34135120/9512384c-4ac2-4fa4-9f38-f42306d9f05b">
较旧的垃圾收集器(Serial,Parallel,CMS)将堆分为三个部分:固定内存大小的年轻代,老年代和永久代 (元空间)
<img width="871" alt="截屏2023-11-30 14 16 13" src="https://github.com/Mein-Augenstern/MUYI/assets/34135120/0b136d8d-1313-4e6b-9340-150996ac8d54">
特性
Region 区域:把连续的 Java 堆划分为多个大小相等的独立 Region。每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。
Humongous 区域:专门用来存储大对象。只要大小超过了一个 Region 容量一半的对象即可判定为大对象。
基于停顿时间模型:消耗在垃圾收集上的时间大概率不超过 N 毫秒的目标。使用参数-XX:MaxGCPauseMillis 指定,默认值是 200 毫秒。
Mixed GC 模式:可以面向堆内存任何部分来组成「回收集」回收。衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。
无内存空间碎片:G1 算法实现意味着运作期间不会产生内存空间碎片。这点存有疑问,原因如下:
- G1(Garbage-First)垃圾收集器是设计来减少内存碎片并允许更可预测的停顿时间的一种收集器。与传统的垃圾收集器相比,G1通过将堆内存划分成多个区域(Regions)来管理内存。这种设计在很大程度上减少了内存碎片问题。G1可以通过复制存活对象到不同区域的方式,在执行垃圾回收的同时整理内存(Compaction)。这有助于维持堆内存的连续性,从而最小化内存碎片。
- 然而,虽然G1收集器可以显著减少内存碎片的问题,但并不意味着它完全消除了内存碎片。G1在处理特别大的对象时,会使用所谓的“Humongous”区域来存储它们。巨型对象(即大小超过区域容量一半的对象)直接在连续的Humongous区域中分配。如果巨型对象较多,它们可能会占用大量的连续区域。而这些区域在巨型对象未被回收前无法被重新利用。这可能导致一定程度的内存碎片和空间利用率问题。
- 总的来说,G1垃圾收集器通过区域划分和在垃圾回收时的对象移动来减少内存碎片。但对于特别大的对象,仍然存在一定的内存碎片风险。为了处理这种情况,G1会定期执行完全垃圾回收(Full GC)。它在必要时清理Humongous区域中的对象,进一步减少内存碎片。
每个 Region 的大小可以通过参数-XX:G1HeapRegionSize 设定,取值范围为 1 MB~32 MB,且应为 2 的 N 次幂。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中。G1 的大多数行为都把 Humongous Region 作为老年代的一部分来看待。
大致实现步骤
- 初始标记:
仅仅只是标记 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值。这让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。需要「Stop The World」,但耗时很短,而且是借用进行 Minor GC 的时候同步完成的。
- 并发标记:
从 GC Root 开始对堆中对象做可达性分析,递归扫描整个堆里的对象图,找出要回收的对象。这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理 SATB 记录下的在并发时有引用变动的对象。
- 最终标记:
对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。
- 筛选回收:
负责更新 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 最佳实践
- 不要设置年轻代的大小
如果通过 -Xmn 显式地指定了年轻代的大小, 则会干扰到 G1 收集器的默认行为。
G1 在垃圾收集时将不再关心暂停时间指标。所以从本质上说,设置年轻代的大小将禁用暂停时间目标。 G1 在必要时也不能够增加或者缩小年轻代的空间。 因为大小是固定的,所以对更改大小无能为力。
- 响应时间指标
设置 XX:MaxGCPauseMillis=N 时不应该使用平均响应时间 (ART, average response time) 作为指标。而应该考虑使用目标时间的 90% 或者以上作为响应时间指标。即 90% 的用户请求响应时间不会超过预设的目标值。暂停时间只是一个目标,并不能保证总是满足。
- 什么是转移失败 (Evacuation Failure)?
对 Survivors 或晋升对象进行 GC 时如果 JVM 的 Heap 区不足就会发生提升失败。堆内存不能继续扩充,因为已经达到最大值了。
当使用 -XX:+PrintGCDetails 时将会在 GC 日志中显示 to-space overflow。
这是很昂贵的操作。
GC 仍继续所以空间必须被释放
拷贝失败的对象必须被放到正确的位置
CSet 指向区域中的任何 RSets 更新都必须重新生成 (regenerated)
所有这些步骤都是代价高昂的
如何避免转移失败 (Evacuation Failure)
- 要避免避免转移失败, 考虑采纳下列选项
增加堆内存大小
增加空闲空间的预留内存百分比 -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。
收集器运行示意图
<img width="842" alt="截屏2023-11-30 14 17 11" src="https://github.com/Mein-Augenstern/MUYI/assets/34135120/0998d145-05ea-4b74-89e1-9b56b9a92f44">
<img width="859" alt="截屏2023-11-30 14 19 22" src="https://github.com/Mein-Augenstern/MUYI/assets/34135120/1caf2251-2b30-447d-940d-7d9323f75d7a">
<img width="785" alt="截屏2023-11-30 14 19 37" src="https://github.com/Mein-Augenstern/MUYI/assets/34135120/67b5b70b-79b4-496e-a125-a9483a249e4e">
选择合适的收集器
选择收集器的考虑点:
运行模式
客户端模式,低内存(Serial/Serial Old)
服务端模式
高吞吐量
低延迟
硬件规格、系统架构
JDK 发行版本
一般情况,服务端模式下结合 JDK 发行版本,通常我们使用最新的垃圾回收器。
JDK9 前,我们会在 CMS 和 G1 间选择。对于大概 4 GB 到 6 GB 之间的堆内存(不大于 6 GB),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