JVM笔记:经典垃圾回收器

经典垃圾收集器

如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者

image-20230608095518581

7种作用与不同分代的收集器。收集器之间存在连线说明可以搭配使用,其中 CMS 和 G1 两款复杂而广泛使用的收集器。

1. Serial 收集器

新生代单线程收集器,当它进行垃圾收集的时候,必须暂停其他所有的工作线程,直到它收集结束。因为它的简单高效,所有依然是HotSpot虚拟机在客户端模式下的默认新生代收集器。它是所有收集器内额外内存消耗最小的。

Serial 收集器

2. ParNew 收集器

新生代多线程垃圾收集器,是 Serial 收集器的多线程并行版本。目前只有它能与 CMS 收集器配合工作。

ParNew 收集器

3. Parallel Scavenge 收集器

新生代多线程收集器,基于标记-复制算法实现的收集器。Parallel Scavenge 和其他收集器关注点不同,它的关注点在于达到一个可控制的吞吐量,而不是尽量缩短 SWT。

image-20230608105013763

4. Serial Old 收集器

老年代单线程收集器,使用标记-整理算法。

image-20230608110937072

5. Parallel Old 收集器

老年代多线程收集器,基于标记-整理算法。

image-20230608111201028

6. CMS 收集器

老年代多线程收集器,基于标记-清除算法实现,是一种以最低 STW 为目标的收集器。

6.1 运作过程

  1. 初始标记(CMS initial mark)
    • 此步骤仅仅只是标记一下 GC Root 是能直接关联的对象,速度很快,需要 STW。
  2. 并发标记(CMS concurrent mark)
    • 从 GC Roots 的直接关联对象开始遍历整个对象图的过程,时间较长但是不需要停顿用户线程,不需要 STW。
  3. 重新标记(CMS remark)
    • 为了修正在并发标记期间,由于用户从操作导致标记产生变动的那一部分对象的标记记录,需要 STW
  4. 并发清除(CMS concurrent sweep)
    • 清除标记为死亡的对象,不需要 STW。

image-20230608135500422

整个过程中,耗时最长的是并发标记和并发清除阶段。但是这两个阶段的垃圾收集器线程都适合用户线程一起工作的,所以从这个垃圾收集而言,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

6.2 优缺点

  • 优点

    1. 并发收集

    2. 低停顿

      由于这两个优点,它在被称为“并发低停顿收集器”

  • 缺点

    1. CMS 收集器对处理器资源非常敏感。

      • 在并发阶段,CMS 收集器虽然不会让用户线程停顿,但是也占用了一部分线程而导致应用程序变慢,降低了总吞吐量。

        从处理器核心数量来看,CMS 的设置回收线程数是(处理器核心数量 + 3)/ 4,如果处理器核心数量在 4 个以上,CMS 的回收先占只占用不超过 25%。如果处理器核心数量在 4 或 4 一下,CMS 对用户程序的影响就变大了。

        从用户程序执行速度来看,若当前应用本身对处理器而言负载就很高,此时还需要分出一部分的运算能力来执行收集器线程,可能导致用户程序的执行速度大幅度降低。

    2. CMS 收集器无法处理“浮动垃圾”

      • 在 CMS 并发标记和并发清理阶段,用户线程还在继续运行,程序运行自然会产生新的垃圾对象。这一部分垃圾对象出现在标记阶段之后,CMS 无法马上处理,只能留在下一次垃圾收集时再清理。这一部分的垃圾就被称为 “浮动垃圾”。
    3. CMS 并发收集预留空间不足导致“并发失败”

      • CMS 并发处理的时候,必须留出一部分的空间供并发处理时的程序运作使用。如果 CMS 运行期间预留的内存无法满足程序分配对象的需求,就会出现“并发失败”。这时候的虚拟机将启动后备方案:冻结用户线程的执行,临时启用 Serial Old 收集器对老年代进行垃圾收集,但是这样停顿时间就很长。
    4. CMS 基于标记-清除算法,会出现内存碎片

      • CMS 基于标记-清除算法,会出现内存碎片。这样情况下,即使老年代有很多剩余空间,但是无法提供一个足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC。

        但是每次都 Full GC 会导致停顿时间变长,为了解决这个问题,CMS 提供了一个参数:-XX:CMSFullGCsBefore-Compaction,这个参数的作用就是要求 CMS 在执行若干次不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理。

7. G1 (Garbage First)收集器

7.1 内存布局

  1. 内存如何划分

    • G1 不光是一种垃圾收集器的实现,它更是对 Java 堆内存一种颠覆。

      在 G1 的 Java 堆中,不再固定大小以及固定数量的分代区域划分,而是把连续的 Java 堆划分为多个大小相等的独立区域,这种独立区域我们称为 Region。每一个 Region 都可以根据需要,扮演新生代的 Eden 区域、Survivor 区域、老年代区域和 Humongous 区域。G1 收集器也根据不同角色的 Region 采用不同的策略去处理。

      Humongous 区域是专门用来存储大对象。每个 Humongous Region 都是连续内存空间。G1 认为只要大小超过 Region 容量一半的对象即可判定为大对象。对于那些超过整个 Region 容量的超级大对象,会存放在连续多个 Humongous Region 当中。G1 大多数行为都把 Humongous Region 作为老年代的一部分来看(以前数组放方法区,现在就放 Humongous Region 当中)。

image-20230608203914340

  1. 如何回收

    • G1 保留了新生代了老年代的概念,但是新生代和老年代不再固定,它们都是一系列 Region 的动态集合(不要求连续)。

      在 G1 的垃圾回收过程中,Region 是单次回收的最小单元。每次回收的内存空间都是 Region 大小的整数倍。

      G1 垃圾收集器会去追踪每个 Region 的里面的垃圾“价值”的大小,通过回收所获取的空间大小以及回收所需要的时间来判断。后台会维护一个优先级列表,根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些 Region。

7.2 细节问题

  1. Region 中存在跨 Region 引用对象如何解决

    • 使用记忆集来避免全堆作为 GC Roots 扫描。每个 Region 都有维护自己的记忆集,这些记忆集会记录下别的 Region 指向自己的指针,并标记这些指针分别在哪些卡页范围内(记录了谁用了我,它在哪)。

      G1 的记忆集的数据结构是一种哈希表,Key 是别的 Region 的起始指针,Value是一个集合,里面存储的元素是卡表的索引号。这种双向的卡表比原来的卡表实现更加复杂,同时由于 Region 比传统垃圾收集器的分代数量明显要多得多,因此 G1 垃圾收集器比其他的传统垃圾收集器更复杂更高的内存占用负担。

  2. 并发阶段用户改变引用关系怎么办?

    • CMS 收集器采用增量更新算法,G1 使用原始快照算法 SATB 来实现。
  3. 回收阶段中新的对象被创建如何分配内存?

    • G1 为每个 Region 设计了两个名为 TAMS 的指针,把 Region 中的一部分空间划分出来用于并发回收过程中新对象的分配,并发回收时新分配的对象地址必须都在这两个指针位置上。
  4. 并发阶段出现内存不足怎么办

    • 如果内存回收速度比不上内存分配的速度,G1 收集器也要被迫冻结用户线程执行,导致 Full GC 而产生长时间的 STW。
  5. G1 如何建立可靠的停顿模型呢(怎么知道哪些 Region 该收集)

    • 在垃圾收集过程中,G1 收集器会记录每个 Region 的回收耗时、每个 Region 记忆集中的脏卡数量等可测量的步骤花费成本,并分析得出平均值、标准偏差、置信度等统计信息。Region 的统计状态越新越能准确预估其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些 Region 组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。

7.3 回收步骤

大致可以分成四个步骤:

  1. 初始标记(Initial Marking):
    • 仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能够正确地在可用的 Region 中分配对象。需要 SWT,耗时很短。

      TAMS 限定新对象的分配位置。TAMS指针标记了一个位置,表示在这个位置之上的空间是用于并发回收过程中的新对象分配的空间。也就是说,TAMS指针以上的空间可以用于新对象的分配,而TAMS指针以下的空间则不会用于新对象的分配。

      如果你学过 JUC,可以将读写屏障的概念转移过来用于理解。

  2. 并发标记(Concurrent Marking):
    • 从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆内的对象图,找出要回收的对象。扫描完毕之后,还要重新处理 SATB 记录下在并发时有引用变动的对象。不需要 SWT,与用户线程并发执行,耗时长。
  3. 最终标记(Final Marking):
    • 短暂的 STW,用于处理并发阶段结束后仍遗留下来的最后的那少量的 SATB 记录。
  4. 筛选回收(Live Data Counting and Evacuation):
    • 对各个 Region 回收价值和成本进行排序,构成回收集。然后将回收集中的每个 Region 内的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。必须 SWT,由多条收集器线程并发完成。

image-20230608230147691

7.4 预期值设置

我们可以针对 G1 收集器设置不同的期望停顿时间。如果将停顿时间设置的太低,可能会出现停顿时间太短,每次回收集都只占堆内存的很小一部分,收集器收集的速度更不上分配的速度,导致垃圾慢慢堆积。通常将停顿时间设置为一两百毫秒或者两三百毫秒都比较合理的。

7.5. G1 的特点

  1. 可以指定最大停顿时间,分 Region 布局、按照收集动态决定回收集
  2. 从整体上看是基于标记-整理算法实现的收集器,从局部上看是基于标记-复制实现的。
  3. 不会产生内存碎片,垃圾收集完成后可以提供规整的堆内存空间
  4. 和 CMS 相比,无论是垃圾回收产生的内存占用或者程序产生的额外负载都比 CMS 高。
  5. G1 卡表更复杂,每个 Region 都有对应的一个记忆集,可能会占据整个堆内存容量的 20% 乃至更多内存空间。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYWoi0e7' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片