1、为什么要有GC?

  • 本质上就是内存资源的有限性(收集垃圾)

2、如何回收垃圾呢?

2.1、引用计数法

  • 有引用,计数器 +1
  • 无引用,计数器 -1

2.1.1、产生的问题:

  • 循环依赖(跟事务,线程死锁一个道理)

样例:(证据)

public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}

如何解决?

  • 引用追踪 => 标记清除算法

2.2、可达性分析算法(一般用于标记清除和标记整理算法中)

将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象 GC Roots根节点:当前正在执行的方法里的局部变量和输入参数,活动线程(Active threads),所有类的静态字段(static field),JNI 引用

#yyds干货盘点#JVM升级篇九(GC篇)_堆内存#yyds干货盘点#JVM升级篇九(GC篇)_堆内存_02

上面的引用的类型有

  • 强引用:普通的变量引用

public static User user = new User();

  • 软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。

public static SoftReference user = new SoftReference(new User());

  • 使用场景:浏览器的后退按钮
  • 为什么?
  • 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
  • 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
  • 弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用

public static WeakReference user = new WeakReference(new User());

  • 虚引用

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用

3、垃圾收集的算法有什么呢?

  • 标记清除算法(Mark and Sweep)
  • 定义
  • Marking(标记): 遍历所有的可达对象,并在本地内存(native)中分门别类记下。
  • Sweeping(清除): 这一步保证了,不可达对象所占用的内存,在之后进行内存分配时可以重用。
  • 用处
  • 并行 GC 和 CMS 的基本原理
  • 优势(优点):
  • 可以处理循环依赖,只扫描部分对象 #yyds干货盘点#JVM升级篇九(GC篇)_老年代_03
  • 缺点
  • 效率问题 (如果需要标记的对象太多,效率不高)
  • 空间问题(标记清除后会产生大量不连续的碎片)
  • 如何解决?
  • 压缩,STW 标记和清除大量对象!!!!
  • 标记复制算法
  • 定义
  • 以将内存分为大小相同的两块,每次使用其中的一块。当这一块的 内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对 内存区间的一半进行回收。 #yyds干货盘点#JVM升级篇九(GC篇)_老年代_04
  • 标记整理算法
  • 根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回到收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存 #yyds干货盘点#JVM升级篇九(GC篇)_java_05
  • 分代收集算法
  • 为什么会有分代?

#yyds干货盘点#JVM升级篇九(GC篇)_堆内存_06

  • 分代假设:大部分新生对象很快无用;存活较长时间的对象,可能存活更长时间。
  • 所以JVM会有内存池划分 #yyds干货盘点#JVM升级篇九(GC篇)_堆内存_07
  • 不同类型对象不同区域,不同策略处理。
  • 分代收集 #yyds干货盘点#JVM升级篇九(GC篇)_堆内存_08
  • 对象分配在新生代的 Eden 区,标记阶段 Eden 区存活的对象就会复制到存活区;
  • 注意:为什么是复制,不是移动???
  • 两个存活区 from 和 to,互换角色。对象存活到一定周期会提升到老年代。
  • 由如下参数控制提升阈值 -XX:+MaxTenuringThreshold=15
  • 老年代默认都是存活对象,采用移动方式:
  • 1. 标记所有通过 GC roots 可达的对象;
  • 2. 删除所有不可达对象;
  • 3. 整理老年代空间中的内容,方法是将所有的存活对象复制,从老年代空间开始的地方依次存放。
  • 持久代/元数据区
  • 1.8 之前 -XX:MaxPermSize=256m
  • 1.8 之后 -XX:MaxMetaspaceSize=256m

4、垃圾收集器

4.1、串行 GC(Serial GC)/ParNewGC => 标记复制

应用: -XX:+UseSerialGC

如:java -jar -XX:+UseSerialGC microservice-eureka-server.jar

算法: 年轻代:mark-copy(标记-复制)算法 老年代:mark-sweep-compact(标记-清除-整理)算法 共同点:

  • 都是单线程的垃圾收集器,不能并行处理,会触发STW,停止所有线程
  • 不能充分利用多核CPU,只能用单核
  • 单CPU利用高,暂停时间长,易卡死

使用:

  • 只适合几百MB的堆内存,并且单核的CPU

为什么?

  • 串行 GC中的串行,跟我们实际的队列是一样的,先进先出,所以就有个问题,容易阻塞,并且不能充分利用多核,所以单核最好,
  • 因此,只适合几百MB的堆内存,并且单核的CPU

注意: -XX:+USeParNewGC 改进版本的 Serial GC,可以配合 CMS 使用

4.2、并行 GC(Parallel GC)=>eden:标记复制 old:标记整理(Java 8默认GC)

应用:

-XX:+UseParallelGC

-XX:+UseParallelOldGC

-XX:+UseParallelGC

-XX:+UseParallelOldGC

算法:

年轻代:mark-copy(标记-复制)算法

老年代:mark-sweep-compact(标记-清除-整理)算法

-XX:ParallelGCThreads=N 来指定 GC 线程数, 其默认值为 CPU 核心数。

  • 优点:
  • 适用于多核服务器,主要目标增加吞吐量。
  • 对系统资源的有效使用,达到最高吞吐量
  • 在GC期间所有CPU内核都在并行清理垃圾,总暂停时间更短
  • 在两次GC周期的间隔期,没有GC在运行,不会消耗内存

4.3、CMS GC=>eden:标记复制 old:标记清除

4.3.1、六大阶段:

#yyds干货盘点#JVM升级篇九(GC篇)_java_09

解释:

  • 阶段 1: Initial Mark(初始标记)
  • 暂停所有的其他线程(STW),并记录下gc roots标记所有的根对象(包括根对象直接引用的对象,以及被年轻代中所有存活对象所引用的对象(老年代单独回收)),速度很快 #yyds干货盘点#JVM升级篇九(GC篇)_老年代_10
  • 阶段 2: Concurrent Mark(并发标记)
  • CMS GC 遍历老年代,标记所有的存活对象,从前一阶段 “Initial Mark” 找到的根对象开始算起。 “并发标记”阶段,就是与应用程序同时运行,不用暂停的阶段 #yyds干货盘点#JVM升级篇九(GC篇)_java_11
  • 阶段 3: Concurrent Preclean(并发预清理)
  • 此阶段同样是与应用线程并发执行的,不需要停止应用线程。 因为前一阶段【并发标记】与程序并发运行,可能有一些引用关系已经发生了改变。如果在并发标记过程中引用关系发生了变化,JVM 会通过“Card(卡片)”的方式将发生了改变的区域标记为“脏”区,这就是所谓的 卡片标记(Card Marking)。
  • (漏标解决:写屏障 + 增量更新)
  • 写屏障+增量更新:当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D 记录下来。
  • 示例:
void post_write_barrier(oop* field, oop new_value) {
remark_set.add(new_value); // 记录新引用的对象
}

#yyds干货盘点#JVM升级篇九(GC篇)_老年代_12

  • 阶段 4: Final Remark(最终标记)
  • 最终标记阶段是此次 GC 事件中的第二次(也是最后一次)STW 停顿。本阶段的目标是完成老年代中所有存活对象的标记。因为之前的预清理阶段是并发执行的,有可能 GC 线程跟不上应用程序的修改速度。所以需要一次 STW 暂停来处理各种复杂的情况。通常 CMS 会尝试在年轻代尽可能空的情况下执行 Final Remark 阶段,以免连续触发多次 STW 事件
  • 阶段 5: Concurrent Sweep(并发清除)
  • 开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理。 #yyds干货盘点#JVM升级篇九(GC篇)_老年代_13
  • 阶段 6: Concurrent Reset(并发重置)
  • 重置本次GC过程中的标记数据

4.3.2、应用:

-XX:+UseConcMarkSweepGC

4.3.3、算法:

年轻代:mark-copy(标记-复制)算法 老年代:mark-sweep(标记-清除)算法

4.3.4、优缺点

优点: 并发收集、低停顿 缺点: - 对CPU资源敏感(会和服务抢资源); - 无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了); - 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数- XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理 - 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并 发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收

4.3.5、三色标记法(CMS中Concurrent Sweep(并发清除)的底层实现)

在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。 这里我们引入“三色标记”来给大家解释下,把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以 下三种颜色:

  • 黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过 灰色对象) 指向某个白色对象。
  • 灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
  • 白色: 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。

4.3.6、CMS的相关核心参数

  1. -XX:+UseConcMarkSweepGC:启用cms
  2. -XX:ConcGCThreads:并发的GC线程数
  3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一 次
  5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设 定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引 用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
  8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
  9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;

4.4、G1

#yyds干货盘点#JVM升级篇九(GC篇)_堆内存_14

  • G1 的全称是 Garbage-First,意为垃圾优先,哪一块的垃圾最多就优先清理它。
  • G1 GC 最主要的设计目标是:将 STW 停顿的时间和分布,变成可预期且可配置的。
  • 事实上,G1 GC 是一款软实时垃圾收集器,可以为其设置某项特定的性能指标。为了达成可预期停顿时间的指标,G1 GC 有一些 独特的实现。
  • 首先,堆不再分成年轻代和老年代,而是划分为多个(通常是2048个)可以存放对象的小块堆区域(smaller heap regions)。 每个小块,可能一会被定义成 Eden 区,一会被指定为 Survivor区或者Old 区。在逻辑上,所有的 Eden 区和 Survivor 区合起来就是年轻代,所有的 Old 区拼在一起那就是老年代。
  • -XX:+UseG1GC -XX:MaxGCPauseMillis=50


#yyds干货盘点#JVM升级篇九(GC篇)_堆内存_15

  • 这样划分之后,使得 G1 不必每次都去收集整

个堆空间,而是以增量的方式来进行处理: 每 次只处理一部分内存块,称为此次 GC 的回收 集(collection set)。每次 GC 暂停都会收集所 有年轻代的内存块,但一般只包含部分老年代 的内存块。(漏标解决:写屏障 + SATB) - 写屏障 + SATB意义:当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d = null),我们可以利用写屏障,将B原来成员变量的引用对象D记录下来。

void pre_write_barrier(oop* field) {
oop old_value = *field; // 获取旧值
remark_set.add(old_value); // 记录原来的引用对象
}
  • G1 的另一项创新是,在并发阶段估算每个小堆块存活对象的总数。构建回收集的原则是: 垃 圾最多的小块会被优先收集。这也是 G1 名称的由来。

4.4.1、G1 GC--配置参数

  • -XX:+UseG1GC:启用 G1 GC;
  • -XX:G1NewSizePercent:初始年轻代占整个 Java Heap 的大小,默认值为 5%;
  • -XX:G1MaxNewSizePercent:最大年轻代占整个 Java Heap 的大小,默认值为 60%;
  • -XX:G1HeapRegionSize:设置每个 Region 的大小,单位 MB,需要为 1、2、4、8、16、32 中的某个值,默认是堆内存的1/2000。如果这个值设置比较大,那么大对象就可以进入 Region 了;
  • -XX:ConcGCThreads:与 Java 应用一起执行的 GC 线程数量,默认是 Java 线程的 1/4,减少这个参数的数值可能会提升并行回收的效率,提高系统内部吞吐量。如果这个数值过低,参与回收垃圾的线程不足,也会导致并行回收机制耗时加长;
  • -XX:+InitiatingHeapOccupancyPercent(简称 IHOP):G1 内部并行回收循环启动的阈值,默认为 Java Heap的 45%。这个可以理解为老年代使用大于等于 45% 的时候,JVM 会启动垃圾回收。这个值非常重要,它决定了在什么时间启动老年代的并行回收;
  • -XX:G1HeapWastePercent:G1停止回收的最小内存大小,默认是堆大小的 5%。GC 会收集所有的 Region 中的对象,但是如果下降到了 5%,就会停下来不再收集了。就是说,不必每次回收就把所有的垃圾都处理完,可以遗留少量的下次处理,这样也降低了单次消耗的时间;
  • -XX:G1MixedGCCountTarget:设置并行循环之后需要有多少个混合 GC 启动,默认值是 8 个。老年代 Regions的回收时间通常比年轻代的收集时间要长一些。所以如果混合收集器比较多,可以允许 G1 延长老年代的收集时间。
  • -XX:+G1PrintRegionLivenessInfo:这个参数需要和 -XX:+UnlockDiagnosticVMOptions 配合启动,打印 JVM 的调试信息,每个Region 里的对象存活信息。
  • -XX:G1ReservePercent:G1 为了保留一些空间用于年代之间的提升,默认值是堆空间的 10%。因为大量执行回收的地方在年轻代(存活时间较短),所以如果你的应用里面有比较大的堆内存空间、比较多的大对象存活,这里需要保留一些内存。
  • -XX:+G1SummarizeRSetStats:这也是一个 VM 的调试信息。如果启用,会在 VM 退出的时候打印出 Rsets 的详细总结信息。

如果启用 -XX:G1SummaryRSetStatsPeriod 参数,就会阶段性地打印 Rsets 信息。

  • -XX:+G1TraceConcRefinement:这个也是一个 VM 的调试信息,如果启用,并行回收阶段的日志就会被详细打印出来。
  • -XX:+GCTimeRatio:这个参数就是计算花在 Java 应用线程上和花在 GC 线程上的时间比率,默认是 9,跟新生代内存的分配比例一致。这个参数主要的目的是让用户可以控制花在应用上的时间,G1 的计算公式是 100/(1+GCTimeRatio)。这样如果参数设置为9,则最多 10% 的时间会花在 GC 工作上面。Parallel GC 的默认值是 99,表示 1% 的时间被用在 GC 上面,这是因为 Parallel GC 贯穿整个 GC,而 G1 则根据 Region 来进行划分,不需要全局性扫描整个内存堆。
  • -XX:+UseStringDeduplication:手动开启 Java String 对象的去重工作,这个是 JDK8u20 版本之后新增的参数,主要用于相同String 避免重复申请内存,节约 Region 的使用。
  • -XX:MaxGCPauseMills:预期 G1 每次执行 GC 操作的暂停时间,单位是毫秒,默认值是 200 毫秒,G1 会尽量保证控制在这个范围内。

4.4.2、处理步骤

1、年轻代模式转移暂停(Evacuation Pause)

G1 GC 会通过前面一段时间的运行情况来不断的调整自己的回收策略和行为,以此来比较稳定地控制暂 停时间。在应用程序刚启动时,G1 还没有采集到什么足够的信息,这时候就处于初始的 fully-young 模式。当年轻代空间用满后,应用线程会被暂停,年轻代内存块中的存活对象被拷贝到存活区。如果还 没有存活区,则任意选择一部分空闲的内存块作为存活区。 拷贝的过程称为转移(Evacuation),这和前面介绍的其他年轻代收集器是一样的工作原理。

2、并发标记(Concurrent Marking)

同时我们也可以看到,G1 GC 的很多概念建立在 CMS 的基础上,所以下面的内容需要对 CMS 有一定的理解。 G1 并发标记的过程与 CMS 基本上是一样的。G1 的并发标记通过 Snapshot-At-The-Beginning(起始快 照)的方式,在标记阶段开始时记下所有的存活对象。即使在标记的同时又有一些变成了垃圾。通过对象的存 活信息,可以构建出每个小堆块的存活状态,以便回收集能高效地进行选择。 这些信息在接下来的阶段会用来执行老年代区域的垃圾收集。 有两种情况是可以完全并发执行的:

  • 一、如果在标记阶段确定某个小堆块中没有存活对象,只包含垃圾;
  • 二、在 STW 转移暂停期间,同时包含垃圾和存活对象的老年代小堆块。

当堆内存的总体使用比例达到一定数值,就会触发并发标记。这个默认比例是 45%,但也可以通过 JVM参数 InitiatingHeapOccupancyPercent 来设置。和 CMS 一样,G1 的并发标记也是由多个阶段组成,其中一些阶 段是完全并发的,还有一些阶段则会暂停应用线程。

  • 阶段 1: Initial Mark(初始标记)
  • 此阶段标记所有从 GC 根对象直接可达的对象。
  • 阶段 2: Root Region Scan(Root区扫描)
  • 此阶段标记所有从 "根区域" 可达的存活对象。根区域包括:非空的区域,以及在标记过程中不得不收集的区域。
  • 阶段 3: Concurrent Mark(并发标记)
  • 此阶段和 CMS 的并发标记阶段非常类似:只遍历对象图,并在一个特殊的位图中标记能访问到的对象。
  • 阶段 4: Remark(再次标记)
  • 和 CMS 类似,这是一次 STW 停顿(因为不是并发的阶段),以完成标记过程。 G1 收集器会短暂地停止应用线程, 停止并发更新信息的写入,处理其中的少量信息,并标记所有在并发标记开始时未被标记的存活对象。
  • 阶段 5: Cleanup(清理)
  • 最后这个清理阶段为即将到来的转移阶段做准备,统计小堆块中所有存活的对象,并将小堆块进行排序,以提升GC 的效率,维护并发标记的内部状态。 所有不包含存活对象的小堆块在此阶段都被回收了。有一部分任务是并发的: 例如空堆区的回收,还有大部分的存活率计算。此阶段也需要一个短暂的 STW 暂停。

3、转移暂停: 混合模式(Evacuation Pause (mixed))

并发标记完成之后,G1将执行一次混合收集(mixed collection),就是不只清理年轻代,还将一部 分老年代区域也加入到回收集中。混合模式的转移暂停不一定紧跟并发标记阶段。有很多规则和历史数 据会影响混合模式的启动时机。比如,假若在老年代中可以并发地腾出很多的小堆块,就没有必要启动 混合模式。 因此,在并发标记与混合转移暂停之间,很可能会存在多次 young 模式的转移暂停。 具体添加到回收集的老年代小堆块的大小及顺序,也是基于许多规则来判定的。其中包括指定的软实时 性能指标,存活性,以及在并发标记期间收集的 GC 效率等数据,外加一些可配置的 JVM 选项。混合收 集的过程,很大程度上和前面的 fully-young gc 是一样的。

4.4.3、注意事项

特别需要注意的是,某些情况下 G1 触发了 Full GC,这时 G1 会退化使用 Serial 收集器来完成垃圾的清理工作,它仅仅使用单 线程来完成 GC 工作,GC 暂停时间将达到秒级别的。

  1. 并发模式失败
  • G1 启动标记周期,但在 Mix GC 之前,老年代就被填满,这时候 G1 会放弃标记周期。

解决办法:增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads 等)。

  1. 晋升失败
  • 没有足够的内存供存活对象或晋升对象使用,由此触发了 Full GC(to-space exhausted/to-spaceoverflow)。

解决办法: - a) 增加 –XX:G1ReservePercent 选项的值(并相应增加总的堆大小)增加预留内存量。 - b) 通过减少 –XX:InitiatingHeapOccupancyPercent 提前启动标记周期。 - c) 也可以通过增加 –XX:ConcGCThreads 选项的值来增加并行标记线程的数目。

  1. 巨型对象分配失败
  • 当巨型对象找不到合适的空间进行分配时,就会启动 Full GC,来释放空间。
  • 解决办法:增加内存或者增大 -XX:G1HeapRegionSize

4.5、Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old,CMS,G1的对比

#yyds干货盘点#JVM升级篇九(GC篇)_老年代_16

4.6、常见GC组合(重要)

#yyds干货盘点#JVM升级篇九(GC篇)_老年代_17

常用的组合为:

(1)Serial+Serial Old 实现单线程的低延迟 垃圾回收机制;

(2)ParNew+CMS,实现多线程的低延迟垃 圾回收机制;

(3)Parallel Scavenge和Parallel Scavenge Old,实现多线程的高吞吐量垃圾 回收机制。

4.7、GC 如何选择(如何选择垃圾收集器?)

选择正确的 GC 算法,唯一可行的方式就是去尝试,一般性的指导原则:

  1. 如果系统考虑吞吐优先,CPU 资源都用来最大程度处理业务,用 Parallel GC;
  2. 如果系统考虑低延迟有限,每次 GC 时间尽量短,用 CMS GC;
  3. 如果系统内存堆较大,同时希望整体来看平均 GC 时间可控,使用 G1 GC。

对于内存大小的考量:

  1. 一般 4G 以上,算是比较大,用 G1 的性价比较高。
  2. 一般超过 8G,比如 16G-64G 内存,非常推荐使用 G1 GC。

4.8、ZGC

zgc,parallel,g1比较:

#yyds干货盘点#JVM升级篇九(GC篇)_堆内存_18#yyds干货盘点#JVM升级篇九(GC篇)_堆内存_19#yyds干货盘点#JVM升级篇九(GC篇)_java_20

使用:-XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xmx16g ZGC 最主要的特点包括:

  1. GC 最大停顿时间不超过 10ms
  2. 堆内存支持范围广,小至几百 MB 的堆空间,大至 4TB 的超大堆内存(JDK13 升至 16TB)
  3. 与 G1 相比,应用吞吐量下降不超过 15%
  4. 当前只支持 Linux/x64 位平台,JDK15 后支持 MacOS 和Windows 系统

4.9、Shenandoah GC

#yyds干货盘点#JVM升级篇九(GC篇)_java_21#yyds干货盘点#JVM升级篇九(GC篇)_堆内存_22

-XX:+UnlockExperimentalVMOptions - XX:+UseShenandoahGC -Xmx16g Shenandoah GC 立项比 ZGC 更早,设计为 GC 线程与应用线程并发执行的方式,通过实 现垃圾回收过程的并发处理,改善停顿时间, 使得 GC 执行线程能够在业务处理线程运行 过程中进行堆压缩、标记和整理,从而消除 了绝大部分的暂停时间。 Shenandoah 团队对外宣称 Shenandoah GC 的暂停时间与堆大小无关,无论是 200MB还是 200 GB的堆内存,都可以保障具有 很低的暂停时间(注意:并不像 ZGC 那样保 是 200 GB的堆内存,都可以保障具有 很低的暂停时间(注意:并不像 ZGC 那样保 证暂停时间在 10ms 以内)。

4.9.1、ShennandoahGC 与其他 GC 的 STW 比较

#yyds干货盘点#JVM升级篇九(GC篇)_老年代_23

4.10、GC总结

Java 目前支持的所有 GC 算法,一共有 7 类:

  1. 串行 GC(Serial GC): 单线程执行,应用需要暂停;
  2. 并行 GC(ParNew、Parallel Scavenge、Parallel Old): 多线程并行地执行垃圾回收,关注与高吞吐;
  3. CMS(Concurrent Mark-Sweep): 多线程并发标记和清除,关注与降低延迟;
  4. G1(G First): 通过划分多个内存区域做增量整理和回收,进一步降低延迟;
  5. ZGC(Z Garbage Collector): 通过着色指针和读屏障,实现几乎全部的并发执行,几毫秒级别的延迟,线性可扩展;
  6. Epsilon: 实验性的 GC,供性能分析使用;
  7. Shenandoah: G1 的改进版本,跟 ZGC 类似。

可以看出 GC 算法和实现的演进路线:

  1. 串行 -> 并行: 重复利用多核 CPU 的优势,大幅降低 GC 暂停时间,提升吞吐量。
  2. 并行 -> 并发: 不只开多个 GC 线程并行回收,还将 GC 操作拆分为多个步骤,让很多繁重的任务和应用线程一起并发执行,减少了单次 GC 暂停持续的时间,这能有效降低业务系统的延迟。
  3. CMS -> G1: G1 可以说是在 CMS 基础上进行迭代和优化开发出来的,划分为多个小堆块进行增量回收,这样就更进一步地降低了单次 GC 暂停的时间
  4. G1 -> ZGC::ZGC 号称无停顿垃圾收集器,这又是一次极大的改进。ZGC 和 G1 有一些相似的地方,但是底层的算法 和思想又有了全新的突破。

"脱离场景谈性能都是耍流氓" 目前绝大部分 Java 应用系统,堆内存并不大比如 2G-4G 以内,而且对 10ms 这种低延迟的 GC 暂停不敏感,也就是说处理一个业务步骤,大概几百毫秒都是可以接受的,GC 暂停 100ms 还是 10ms 没多大区别。另一方面,系统的吞吐量反而往往是我们追求的重点,这时候就需要考虑采用并行 GC。 如果堆内存再大一些,可以考虑 G1 GC。如果内存非常大(比如超过 16G,甚至是 64G、128G),或者是对延迟非常敏感(比如高频量化交易系统),就需要考虑使用本节提到的新 GC(ZGC/Shenandoah)。