垃圾回收篇:垃圾回收器

垃圾回收器有哪些

  • 串行回收器:Serial、Serial Old

  • 并行回收器:ParNew、Parallel Scavenge、Parallel Old

  • 并发回收器:CMS、G1

    • 并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。

    • 并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部系统资源,此时应用程序的处理的吞吐量将受到一定影响。

7款经典收集器与垃圾分代之间的关系:

image-20231129204808724

七种收集器之间的组合关系:

image-20231129205125873

注:这个关系不是一成不变的,由于维护和兼容性测试的成本,在 JDK 8 时将 Serial+CMS、ParNew+Serial Old 这两个组合声明为废弃(JEP 173),并在 JDK 9 中完全取消了这些组合的支持(JEP 214)。

Serial 收集器

  • Serial 收集器是最基础、历史最悠久的收集器,曾经(在 JDK 1.3.1 之前)是HotSpot 虚拟机新生代收集器的唯一选择。
  • Serial收集器作为HotSpot中Client模式默认新生代垃圾收集器
  • Serial收集器采用复制算法、串行回收和“stop the world”机制方式执行内存回收
  • 除了年轻代Serial收集器还提供了老年代垃圾收集器Serial Old收集器:Serial Old收集器也采用串行回收和“Stop The World”机制2,只不过内存回收算法采用的是标记整理算法。Serial Old是运行在client模式下的默认的老年代的垃圾收集器;Serial Old在Server模式下主要有俩个用途:1、与新生代的Parallel Scavenge配合使用;2、作为老年代CMS收集器的后背垃圾收集方案。

image-20231129210400525

优势:简单而高效(与其他收集器的单线程相比),对于限定单个CPU来说。Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高效的收集效率。运行在client模式下的虚拟机是个不错的选择

在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。

总结:这种收集器现在已经基本不再使用,因为要限定在单核CPU才可以用,现在已经很少有单核CPU,而且对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在java web应用程序中是不会使用的。

ParNew 收集器

ParNew 收集器实质上是 Serial 收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括 Serial 收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX: PretenureSizeThreshold、-XX:HandlePromotionFailure 等)、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一致,

image-20231129211428524

ParNew 收集器除了支持多线程并行收集之外,其他与 Serial 收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的 HotSpot 虚拟机,尤其是 JDK 7 之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。

  • 对于新生代,回收次数频繁,使用并行方式高效
  • 对于老年代回收次数少,使用串行方式节省资源(CPU并行需要切换线程,串行可以省去切换线程的资源)

Parallel Scavenge 收集器

Parallel Scavenge 收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器

❓Parallel Scavenge 的诸多特性从表面上看和 ParNew 非常相似,那它有什么特别之处呢?

  • 和ParNew收集器不同,Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量,它也被称为吞吐量最优先的垃圾收集器
  • 自适应调节策略也是Parallel Scavenge与ParNew收集器的一个重要区别

高吞吐量则可以高效率的利用CPU时间,尽快完成程序的运算任务,主要适合后台运算而不需要太多的交互的任务。因此。常见正在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。

所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,即:

image-20231129213759157

Parallel收集器在JDK1.6时提供了用于执行老年代收集的Parallel Old收集器,用来替代老年代的Serial Old收集器

Parallel Old收集器采用了标记整理算法,但同样也是基于并行回收和”Stop The World“机制

image-20231129214234760

说明:在程序吞吐量优先的应用场景中Parallel收集器和Parallel Old收集器的组合在Server模式下内存回收性能很不错。java8默认使用的就是这个垃圾收集器。

参数配置:

  • -XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务(java8默认开启)

  • -XX:+UseParallelOldGC 手动指定老年代使用Parallel并行收集器执行内存回收任务(java8默认开启)

  • -XX:+ParallelGCThreads设置年轻代并行收集器的线程数。一般的,最好与CPU数量相等,以免过多的线程数影响垃圾收集性能;在默认情况下,当CPU小于8个,ParallelGCThreads的值等于CPU数量;当CPU大于8个,ParallelGCThreads的值等于3+【5*CPU_Count】/8

  • -XX:MaxGCPauseMillis 参数允许的值是一个大于 0 的毫秒数,设置垃圾收集器的最大停顿时间(即STW的时间),单位是毫秒。

    注意:设置此参数收集器将尽力保证内存回收花费的时间不超过用户设定值。不过大家不要异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些,收集 300MB 新生代肯定比收集 500MB 快,但这也直接导致垃圾收集发生得更频繁,原来 10 秒收集一次、每次停顿 100 毫秒,现在变成 5 秒收集一次、每次停顿 70 毫秒。停顿时间的确在下降,但吞吐量也降下来了。

  • -XX:GCTimeRatio 参数的值则应当是一个大于 0 小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。与前一个参数-XX:MaxGCPauseMillis 有一定的矛盾性。暂停时间越长,Ratio参数就容易超过设定的比例。

  • -XX:+UseAdaptiveSizePolicy 设置Parallel Scavenge收集器具有自适应调节策略。

    • 在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄参数会被自动调整。已到达在堆大小、吞吐量和停顿时间之间的平衡点。
    • 在手动调优比较困难的场合,可以直接使用这种自适应的方式。仅指定虚拟机的最大堆、目标吞吐量和停顿时间让虚拟机自己完成调优工作。

CMS 收集器

在 JDK 5 发布时,HotSpot 推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器——CMS 收集器。这款收集器是 HotSpot 虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作。

CMS收集器的关注点是尽可能的缩短垃圾收集时候用户线程的停顿时间。停顿时间越短(低延迟)就越适合于用户交互的程序,良好的相应速度就能提升用户体验。目前很大一部分的java应用集中在互联网的B/S系统的服务器上,这类应用尤其重视服务的相应速度,希望系统的停顿时间最短,来给用户带来较好的体验。

  • CMS采用的是标记清除算法并且也会"stop the world"

遗憾的是,CMS 作为老年代的收集器,却无法与 JDK 1.4.0 中已经存在的新生代收集器 Parallel Scavenge 配合工作[1],所以在 JDK 5 中使用 CMS 来收集老年代的时候,新生代只能选择 ParNew 或者 Serial 收集器中的一个。ParNew 收集器是激活 CMS 后(使用-XX:+UseConcMarkSweepGC 选项)的默认新生代收集器。

image-20231129222214341

  • 初始标记(STW):在这个阶段,程序中所有的工作线程都将会因为STW机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象,一但标记完成之后就会恢复之前被暂停的所有应用线程,由于直接关联对象比较小,所以这里的速度非常快。
  • 并发标记:从 GC Roots的直接关联对象开始遍历整个对象图的过程,整个过程耗时较长但是不需要暂停用户线程,可以和用户线程一起并发运行。
  • 重新标记(STW):由于在并发标记阶段,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户线程继续执行而导致标记产生变动的那一部分对象的标记记录(比如:由不可达变为可达对象的数据),这个阶段的停顿时间会比初始标记阶段稍长一些,但也远比并发标记的时间短。
  • 并发清除:此阶段清理删除标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以和用户线程并发的。

优点:并发收集;低延迟

弊端:

1.会产生内存碎片,导致并发清除后,用户线程可用的空间不足,在无法分配大对象的情况下,不得不提前full GC;

2.CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分的线程而导致应用程序变慢,总吞吐量会降低。

3.CMS无法处理浮动垃圾。在 CMS 的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS 无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。由于 CMS 收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的 Full GC 的产生。同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此 CMS 收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。

G1 收集器

Garbage First(简称 G1)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿的同时,还兼具高吞吐量的性能特征

JDK 9 发布之日,G1 宣告取代 Parallel Scavenge 加 Parallel Old 组合,成为服务端模式下的默认垃圾收集器,而 CMS 则沦落至被声明为不推荐使用(Deprecate)的收集器[1]

image-20231129225933148

虽然 G1 仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。G1 收集器之所以能建立可预测的停顿时间模型,是因为它将 Region 作为单次回收的最小单元,即每次收集到的内存空间都是 Region 大小的整数倍,这样可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。

  • G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续)。使用不同的Region来表示Eden、幸存者0区、幸存者1区和老年代等。
  • G1 GC有计划地避免在整个java堆中进行全区域垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先2列表,每次根据允许的收集时间,优先收集回收价值最大的Region。
  • 由于这种方式的侧重点在于回收垃圾的最大量的区间Region,所以这也是为啥取名为垃圾优先(Garbage First)。

具体步骤:

  • 初始标记(Initial Marking):仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region 中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行 Minor GC 的时候同步完成的,所以 G1 收集器在这个阶段实际并没有额外的停顿。
  • 并发标记(Concurrent Marking):从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理 SATB 记录下的在并发时有引用变动的对象。
  • 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。
  • 筛选回收(Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

参数设置:

  • -XX:+UseG1GC 手动指定G1(java9之后默认)
  • -XX:G1HeapRegionSize 设置每个Region的大小,值是2的幂,范围是1MB到32MB,目标是根据最小的java堆大小划分出约2048个区域。默认是堆内存的1/2000。
  • -XX:MaxGCPauseMillis 设置期望到达的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是200ms
  • -XX:ParallelGCThreads 设置stw时候GC线程数的值。最多为8
  • -XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右
  • -XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的java堆占用率阀值,超过此值,就触发GC。默认值是45。

各GC使用场景

image-20231129233918321