垃圾收集器知识点4:Java中垃圾收集器的实现

本文深入解析Java8中的四种垃圾收集器:Serial、Parallel、CMS和G1,详细分析它们的实现原理、日志以及应用场景。重点介绍了CMS和G1,它们旨在减少长时间停顿,提高应用响应速度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

java8垃圾收集器组合一览

我们知道,针对年轻代和年老代的垃圾回收需要采取不同的gc算法(因为年轻代的内存经常被回收,而年老代的内存很少被回收,根据这个特性所采取的的算法往往是不一样的)。
在java8中,针对年轻代和年老代的垃圾回收,java8采用垃圾收集器组成下列组合;
在这里插入图片描述
现在常用的垃圾收集器组合有4种(上图黑色字体加粗的):

  • 年轻代和年老代使用串行gc(gc就是垃圾收集器garbage collection)
  • 年轻代和年老代使用并行gc
  • 年轻代使用并行gc,年老代使用并发标记清除垃圾收集器
  • 年轻代和老年代使用G1垃圾收集器

serial GC(串行GC)的实现

串行gc对年轻代使用标记-复制算法(mark-copy),对年老代使用标记-清除-整理算法(mark-sweep-compact)。不管是年轻代还是年老代,在触发gc时都会导致全线暂停(STW)。所以串行gc的缺点是不能充分利用多核CPU,JVM在进行垃圾收集时都只能使用单核(不然为啥叫串行呢[doge])。
JVM让年轻代和年老代使用串行GC的指令如下:

java -XX:+UseSerialGC 你要执行的class文件

这个指令执行后项目的JVM就会采取串行GC的方式进行垃圾回收,我们需要打开GC日志进行分析所以启动指令改为:

java ‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps ‐XX:+PrintGCTimeStamps -XX:+UseSerialGC 你要执行的class文件

串行gc日志分析

接下来根据网上的gc日志图片学习一波gc日志分析
在这里插入图片描述

Minor GC(小型GC)

在这里插入图片描述
上面这个截图就是清理年轻代的gc事件,下面分析一波日志:

  1. 2015‐05‐26T14:45:37.987‐0200:GC事件的开始时间,-0200表示西二时区,中国所在的东8区是+0800.
  2. 151.126:GC事件开始时,相对于JVM启动时的间隔时间,单位秒。
  3. allocation Failure:触发GC的原因。即本次GC是由于年轻代中没有空间来存放新的数据接口引起的。
  4. DefNew:垃圾收集器在日志中的名称。DefNew表示在年轻代中使用单线程、标记复制算法,全线暂停的垃圾收集器。
  5. 629119K‐>69888K:在垃圾收集之前和之后年轻代的使用量。
  6. (629120K):年轻代总的空间大小。
  7. 1619346K‐>1273247K: 在垃圾收集之前和之后整个堆内存的使用情况。
  8. (2027264K): 可用堆的总空间大小。
  9. 0.0585007 secs: GC事件持续的时间,以秒为单位。
    10.[Times: user=0.06 sys=0.00, real=0.06 secs] : GC事件的持续时间, 通过三个部分来衡量 ,user – 在此次垃圾回收过程中, 所有 GC线程所消耗的 CPU 时间之和。sys – GC过程中中操作系统调用和系统等待事件所消耗的时间。real – 应用程序暂停的时间。因为串行垃圾收集器(Serial Garbage Collector)只使用单线程, 因此 real time 等 于 user 和 system 时间的总和。

可以从上面的日志片段了解到, 在 GC事件中,JVM 的内存使用情况发生了怎样的变化。此次垃圾收集之前, 堆内存总 的使用量为 1,619,346K。其中,年轻代使用了 629,119K。可以算出,老年代使用量为 990,227K。 更重要的信息蕴含在下一批数字中, 垃圾收集之后, 年轻代的使用量减少了 559,231K, 但堆内存的总体使用量只下降 了 346,099K。 从中可以算出,有 213,132K 的对象从年轻代提升到了老年代。
在这里插入图片描述

Full GC(完全GC)

在这里插入图片描述

  1. Tenured: 用于清理老年代空间的垃圾收集器名称。Tenured 表明使用的是单线程的全线暂停垃圾收集 器, 收集算法为 标记­清除­整理(mark­sweep­compact )。
  2. [Metaspace: 6741K‐>6741K(1056768K)] : 关于 Metaspace 空间, 同样的信息。可以看出, 此次 GC过程 中 Metaspace

为什么说这个gc是full GC,因为由日志可知本次gc不仅清理的年轻代,还清理了年老代和元空间,根据定义:minor GC是清理年轻代,Major GC是清理年老代,Full GC是清理整个堆空间。
在这里插入图片描述

Parallel GC(并行GC)的实现

并行垃圾收集器在年轻代使用标记-复制算法(mark-copy),在年老代使用标记-清除-整理算法(mark-sweep-compact)。年轻代和老年代的垃圾回收都会触发 STW 事件,暂停所有的应用线程来执行垃圾收集。两者在执行 标记和 复制/整理阶段时都使用多个线程, 因此得名“(Parallel)”。通过并行执行, 使得 GC时间大幅减少。
JVM执行如下指令可以使用并行GC:

java -XX:+UseParallelGC 需要执行的class

并行GC在执行垃圾回收期间,所有CPU都在并行清理垃圾,暂停时间(STW)更短。缺点是此 GC的所有阶段都不能中断, 所以并行 GC很容易出现长时间的卡顿(连并行GC都容易出现长时间卡顿,串行GC就不用我说了吧)。

并行GC日志分析

直接上一张网上常见的并行GC日志图:
在这里插入图片描述

Minor GC(小型GC)

在这里插入图片描述

  • PSYoungGen:垃圾收集器的名称。PSYoungGen表明使用的是多线程的全线暂停垃圾收集器,收集算法为标记-复制算法。

这里讲一下在并行GC中real的计算,real≈(user+sys)/线程数,为啥是约等于而不是等于,原因是总有一定比例的处理过程是不能并行处理的,及不能保证线程之间的完全并行。
在这里插入图片描述

Full GC

在这里插入图片描述

  • Ergonomics:触发垃圾回收的原因。Ergonomics表示JVM内部环境认为此时可以进行一次垃圾收集,即主动GC。
  • ParOldGen:垃圾收集器的名称。ParOldGen表示多线程的全线暂停收集器,用于回收年老代,回收算法为标记-清除-整理算法(mark-sweep_compact)。
    在这里插入图片描述

CMS(Concurrent Mark and Sweep) GC的实现(重点GC)

CMS的官方名称是“Mostly Concurrent Mark and Sweep Collector”(主要并发-标记-清除-垃圾收集器)。对年轻代采用并行标记-复制算法,对老年代使用并发标记-清除(Mark-Sweep)算法。

CMS的设计目标是避免在老年代垃圾收集时出现长时间的卡顿。方法是:

  1. 不对老年代进行整理,而是使用空闲列表来管理内存空间的回收。(代价就是老年代内存碎片化)
  2. 在mark-and-sweep(标记清除)阶段的大部分工作和应用线程一起并发执行(所以并发的gc线程会和应用线程争抢CPU时间,默认情况下CMS的并发线程数等于CPU内核数的1/4)

JVM通过如下指令可以指定使用CMS垃圾收集器:

java -XX:+UseConcMarkSweepGC 要执行的类

如果服务器是多核 CPU,并且主要调优目标是降低延迟, 那么使用 CMS 是个很明智的选择. 减少每一次 GC停顿的时 间,会直接影响到终端用户对系统的体验, 用户会认为系统非常灵敏。 因为多数时候都有部分 CPU资源被 GC消耗, 所 以在 CPU资源受限的情况下,CMS 会比并行 GC的吞吐量差一些。

CMS GC日志分析

2015‐05‐26T16:23:07.219‐0200: 64.322: [GC (Allocation Failure) 64.322: [ParNew: 613404K‐>68068K(613440K), 0.1020465 secs] 10885349K‐>10880154K(12514816K), 0.1021309 secs] [Times: user=0.78 sys=0.01, real=0.11 secs] 
2015‐05‐26T16:23:07.321‐0200: 64.425: [GC (CMS Initial Mark) [1 CMS‐initial‐mark: 10812086K(11901376K)] 10887844K(12514816K), 0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2015‐05‐26T16:23:07.321‐0200: 64.425: [CMS‐concurrent‐mark‐start] 
2015‐05‐26T16:23:07.357‐0200: 64.460: [CMS‐concurrent‐mark: 0.035/0.035 secs][Times: user=0.07 sys=0.00, real=0.03 secs] 
2015‐05‐26T16:23:07.357‐0200: 64.460: [CMS‐concurrent‐preclean‐start] 
2015‐05‐26T16:23:07.373‐0200: 64.476: [CMS‐concurrent‐preclean: 0.016/0.016 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
2015‐05‐26T16:23:07.373‐0200: 64.476: [CMS‐concurrent‐abortable‐preclean‐start] 
2015‐05‐26T16:23:08.446‐0200: 65.550: [CMS‐concurrent‐abortable‐preclean: 0.167/1.074 secs] [Times: user=0.20 sys=0.00, real=1.07 secs] 
2015‐05‐26T16:23:08.447‐0200: 65.550: [GC (CMS Final Remark) [YG occupancy: 387920 K (613440 K)] 65.550: [Rescan (parallel) , 0.0085125 secs] 65.559: [weak refs processing, 0.0000243 secs] 65.559: [class unloading, 0.0013120 secs] 65.560: [scrub symbol table, 0.0008345 secs] 65.561: [scrub string table, 0.0001759 secs] [1 CMS‐remark: 10812086K(11901376K)] 11200006K(12514816K), 0.0110730 secs] [Times: user=0.06 sys=0.00, real=0.01 secs] 
2015‐05‐26T16:23:08.458‐0200: 65.561: [CMS‐concurrent‐sweep‐start] 
2015‐05‐26T16:23:08.485‐0200: 65.588: [CMS‐concurrent‐sweep: 0.027/0.027 secs][Times: user=0.03 sys=0.00, real=0.03 secs] 
2015‐05‐26T16:23:08.485‐0200: 65.589: [CMS‐concurrent‐reset‐start] 
2015‐05‐26T16:23:08.497‐0200: 65.601: [CMS‐concurrent‐reset: 0.012/0.012 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Minor GC

在这里插入图片描述

  • parNew:垃圾收集器。表示在年轻代中使用的多线程全线暂停垃圾收集器,收集算法是标记-复制(mark-copy),用来配合老年代的CMS使用。
    在这里插入图片描述
Full GC

在进行老年代的并发回收时,可能会伴随多次年轻代的小型GC,因此本次的fullGC日志格式跟上面的有些许不同。
在这里插入图片描述

  • CMS Initial Mark:初始标记,第一次全线暂停事件,用来标记老年代中所有存活对象,包括GC ROOT的直接引用,以及由年轻代中存活对象所引用的老年代对象。
    在这里插入图片描述
  • Concurrent Mark:并发标记,垃圾收集器遍历老年代,标记所有的存活对象,在此过程中标记操作与应用程序同时运行,因此在标记过程中对象的引用关系还会发生变化。在这里插入图片描述
  • Concurrent Preclean:并发预清理,因为并发标记导致的一些引用可能已经发生变化,需要设置一个脏区去记录这些发生改变的区域,这个脏区就是所谓的卡片标记(Card Marking)。在这里插入图片描述
  • Concurrent Abortable Preclean:并发可取消的预清理。目的是为了减少下一个STW重新标记阶段的工作量,结束条件比如扫描多长时间(默认5秒)或者Eden区使用占比达到期望比例(默认50%)。
  • Final Remark:最终标记,这是此次GC事件的第二次全线暂停(第一次是初始标记Initial Mark)。目的是完成老年代所有存活对象的标记。暂停的原因是避免出现无法跟上应用程序变化速度的情况。通常 CMS 会尝试在年轻代尽可能空的情况运行 final remark 阶段, 以免接连多次发生 STW 事件,因为在预清理阶段总会去考虑老年代存活对象对年轻代对象的引用。
  • Concurrent Sweep:并发清除,与应用程序并发执行,不需要STW停顿,目的是删除未使用的对象并回收占用的空间。
  • Concurrent Reset:并发重置,重置 CMS 算法相关的内部数据, 为下一次 GC 循 环做准备。

G1(Garbage First) GC的实现(重点)

G1(垃圾优先)垃圾收集器设计的目的是为了解决停顿时间和时间分布不可预期的问题(串行、并行GC会造成长停顿,而CMS因为老年代内存碎片化的原因造成停顿时间不可预期)。

G1是一款软实时垃圾收集器,通过设置特定的性能指标:比如任意 1 秒暂停时间不得超过 5 毫秒,Garbage­First GC 会尽力达成这个目标(有很大的概率会满足, 但并不完 全确定,具体是多少将是硬实时的[hard real­time])。

为了做到这种STW停顿时间和分布变成可预期及可配置的效果,G1不再给堆区分年轻代和老年代,而是划为多个可存放对象的小堆区(资料说是2048个,每个小堆区的大小默认是堆内存的1/2000,所以不应该默认是2000个小堆区吗?),每一个小堆区都可能是新生代、老年代、存活区

在这里插入图片描述
这样就可以做到每次GC只处理一小部分的小堆区,这些被处理的小堆区被称为回收集(collection set)。每次暂停都会收集所有的年轻代小堆区,但可能只包含一部分老年代小堆区,如下图:

在这里插入图片描述
G1的另一项创新是在并发阶段估算每个小堆区存活对象的总数,根据垃圾最多的小堆区会被优先收集的原则构建回收集.

JVM启用G1收集器的指令如下:

java -XX:+UseG1GC 要执行的class类

Evacuation Pause:Fully Young(转移暂停:纯年轻代模式)

在应用程序刚启动时, G1 还未执行过(not­yet­executed)并发阶段, 也就没有获得任何额外的信息, 处于初始的 fully­ young 模式. 在年轻代空间用满之后, 应用线程被暂停, 年轻代堆区中的存活对象被复制到存活区, 如果还没有存活 区,则选择任意一部分空闲的小堆区用作存活区。

复制的过程称为转移(Evacuation), 这和前面讲过的年轻代收集器基本上是一样的工作原理。

G1的并发标记(Concurrent Marking)

G1的并发标记通过 Snapshot­At­The­Beginning( ) 的方式, 在标记阶 段开始时记下所有的存活对象。即使在标记的同时又有一些变成了垃圾. 通过对象是存活信息, 可以构建出每个小堆 区的存活状态, 以便回收集能高效地进行选择。

这些信息在接下来的阶段会用来执行老年代区域的垃圾收集。在两种情况下是完全地并发执行的:

  1. 如果在标记 阶段确定某个小堆区只包含垃圾;
  2. 在 STW 转移暂停期间, 同时包含垃圾和存活对象的老年代小堆区。

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

  • 阶段1:Initial Mark(初始标记)。此阶段标记所有从 GC root 直接可达的对象。在 CMS 中需要一次 STW 暂停, 但 G1 里面通常是在转移暂停的同时处理这些事情, 所以它的开销是很小的.
  • 阶段2:Root Region Scan(root区扫描)。此阶段标记所有从 “根区域” 可达的存活对象。 根区域包括: 非空的区域, 以及在标记过程中不得不收集的区域。因为在并发标记的过程中迁移对象会造成很多麻烦, 所以此阶段必须在下一 次转移暂停之前完成。如果必须启动转移暂停, 则会先要求根区域扫描中止, 等它完成才能继续扫描. 在
  • 阶段3:Concurrent mark(并发标记)。此阶段非常类似于 CMS: 它只是遍历对象图, 并在一个特殊的位图中标记能访 问到的对象. 为了确保标记开始时的快照准确性, 所有应用线程并发对对象图执行的引用更新,G1 要求放弃前面阶段 为了标记目的而引用的过时引用。这是通过使用 Pre‐Write 屏障来实现的,(不要和之后介绍的 Post‐Write 混淆, 也不要和多线程开发中的内存屏障 (memory barriers)相混淆)。Pre­Write 屏障的作用是: G1在进行并发标记时, 如果程序将对象的某个属性做了变更, 就 会在 log buffers 中存储之前的引用。 由并发标记线程负责处理。
  • 阶段4:remark(再次标记)。和 CMS 类似,这也是一次 STW 停顿,以完成标记过程。对于 G1,它短暂地停止应用线程, 停止并发更新日志的写入, 处理其中的少量信息, 并标记所有在并发标记开始时未被标记的存活对象。
  • 阶段5:cleanup(清理)。最后这个小阶段为即将到来的转移阶段做准备, 统计小堆区中所有存活的对象, 并将小堆区 进行排序, 以提升 GC的效率. 此阶段也为下一次标记执行所有必需的整理工作(house­keeping activities): 维护并发 标记的内部状态。 最后要提醒的是, 所有不包含存活对象的小堆区在此阶段都被回收了。有一部分是并发的: 例如空堆区的回收,还有大 部分的存活率计算, 此阶段也需要一个短暂的 STW 暂停, 以不受应用线程的影响来完成作业.

Evacuation Pause:Mixed(转移暂停:混合模式)

能并发清理老年代中整个整个的小堆区是一种最优情形,
但有时候并不是这样。并发标记完成之后, G1将执行一次混 合收集(mixed collection), 不只清理年轻代, 还将一部分老年代区域也加入到 collection set 中。

混合模式的转移暂停(Evacuation pause)不一定紧跟着并发标记阶段。有很多规则和历史数据会影响混合模式的启 动时机。比如, 假若在老年代中可以并发地腾出很多的小堆区,就没有必要启动混合模式。 因此, 在并发标记与混合转移暂停之间, 很可能会存在多次 fully­young 转移暂停。

Remembered sets (历史记忆集)是用来支持不同的小堆区进行独立回收的。例如,在收集 A、B、C区时, 我们必须要 知道是否有从 D区或者 E 区指向其中的引用, 以确定他们的存活性. 但是遍历整个堆需要相当长的时间, 这就违背了增 量收集的初衷, 因此必须采取某种优化手段. 其他 GC算法有独立的 Card Table 来支持年轻代的垃圾收集一样, 而 G1 中使用的是 Remembered Sets。
在这里插入图片描述
每个小堆区都有一个 remembered set, 列出了从外部指向本区的所有引用。这些引用将被视为附加的 GC root. 注意,在并发标记过程中,老年代中被确定为垃圾的对象会被忽略, 即使有外部引用指向他们: 因为在这种情 况下引用者也是垃圾。

为了维护 remembered set, 在程序运行的过程中, 只要写入某个字段,就会产生一个 Post­Write 屏障。如果生成的引 用是跨区域的(cross­region),即从一个区指向另一个区, 就会在目标区的 Remembered Set 中,出现一个对应的条目。 为了减少 Write Barrier 造成的开销, 将卡片放入 Remembered Set 的过程是异步的, 而且经过了很多的优化. 总体上 是这样: Write Barrier 把脏卡信息存放到本地缓冲区(local buffer), 有专门的 GC线程负责收集, 并将相关信息传给被 引用区的 remembered set。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值