前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM垃圾回收机制

JVM垃圾回收机制

原创
作者头像
羽毛球初学者
修改2024-10-12 20:08:09
830
修改2024-10-12 20:08:09
举报
文章被收录于专栏:JAVA基础知识

对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块 内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”。堆的大小可以通过参数 –Xms、-Xmx 来指定。

从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代。默认的,新生代与老年代的比例的值为 1:2 (该值可以通过参数 –XX:NewRatio 来指定 )。其中新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 – XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

垃圾判断(垃圾标记)

在堆里存放着几乎所有的Java对象实例,在GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象(有用对象),哪些是死亡对象(垃圾对象)。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。

简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣告为已经死亡。判断对象存活一般有两种方式:引用计数算法和可达性分析算法。

引用计数法

引用计数算法(Reference Counting)比较简单,对每个对象保存一个整形的引用计数器属性,用于记录对象被引用的情况。它的优点是:

  • 实现简单,垃圾对象便于辨识;
  • 判定效率高,回收没有延迟性。

但是缺点也很明显:

  • 需要单独的字段存储计数器,增加了存储空间的开销;
  • 每次赋值都需要更新计数器,增加了时间开销;
  • 无法处理循环引用的情况,针对这点,Java的垃圾回收器中没有使用这个方法进行垃圾判断。

可达性分析

相较于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数法中循环引用的问题,防止内存泄漏的发生。

可达性分析是以根对象集合(GC Roots)为起始点,按从上至下的方式搜索被跟对象集合所连接的目标对象是否可达。使用可达性分析算法后,内存中存活的对象都会被根对象集合直接或间接连接着,搜索所过的路 径称为引用链(Reference Chain)。如果目标对象没有和任何引用链相连,则是不可达的,就判定对象已经死亡,可以标记为垃圾对象。

GC Roots 对象有以下几类:

  • 虚拟机栈中引用的对象,比如:Java线程中,当前所有正在被调用的方法的引用类型参数、局部变量等;
  • 本地方法栈中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象,比如:字符串常量池里的引用;
  • 所有被同步锁synchronized持有的对象;
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,常驻的异常对象,系统类加载器。

分析工作必须在一个能保障一致性的快照中进行,这点不满足的话,分析结果的准确性就无法保证。这点也是导致GC进行时必须“Stop The World”的一个重要原因。即使是号称几乎不会停顿的CMS垃圾回收器 中,枚举根节点时也是必须要停顿的。

垃圾回收算法

标记-清除算法

标记-清除(Mark-Sweep)算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:

  • 标记:从GC Roots开始遍历,标记所有被引用的对象。一般是在对象头中记录是否是可达对象。
  • 清除:对堆内存从头到尾遍历,如果发现某个对象的对象头中没有标记为可达对象,则将其回收。

它的优点是:

  • 不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。

但是缺点也比较明显:

  • 标记和清除过程的效率低;
  • 需要使用一个空闲列表来记录所有的空闲区域以及大小,对空闲列表的管理会增加分配对象时的工作量;
  • 会产生大量不连续的内存碎片。

复制算法

复制算法主要是将内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的对象,交换两个内存的角色,最后完成垃圾回收。

对于这种算法来说,如果存活的对象过多的话则要执行较多的复制操作,效率会变低,因此它适合存活率较低的情况。在年轻代中就是使用的复制算法。

它的优点是:

  • 没有标记和清除的过程(进行垃圾判断时即可直接进行复制),实现简单,运行高效;
  • 复制过去以后保证空间的连续性,不会出现“碎 片”问题。

缺点是:

  • 需要两倍的内存空间;
  • 如果存活对象较多,那么复制操作就比较多,效率相对会降低。

标记-整理算法

标记-整理分为“标记”和“整理”两个阶段:

  • 标记:和标记清除算法一样,从GC Roots开始标记所有被引用的对象。
  • 整理:将所有的存活对象压缩到内存的一端,按顺序排放。之后清理外边界的空间(清理垃圾)。

标记-整理算法的最终效果等同于标记-清除算法执行后,再进行一次内存碎片整理,因此也可以把它称为标记-清除-压缩算法。 它的优点是消除了标记-清除算法中内存碎片问题。但是缺点也比较明显:

  • 在移动对象的同时,如果对象被其他对象引用,还需要调整引用的地址;
  • 移动过程中,需要全程暂停用户应用程序,即STW。

垃圾清除

按照所清除垃圾的位置来区分,垃圾清除可要分为 Minor GC 和 Full GC 两种。

Minor GC

Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。新生代几乎是所有 Java 对象出生的地 方,即 Java 对象申请的内存以及存放都是在这个地方。当一个对象被判定为 "死亡" 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。

当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳,则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域, 并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。

需要注意的是,老年代对象不仅仅是由新生代晋升过来的,有些大对象(即需要分配一块较大的连续内存空间 ) 在创建时是直接进入到老年代。

Full GC

Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。

垃圾回收器

现在收集器基本都是采用的分代收集算法,其中 Serial 收集器、ParNew 收集器、Parallel Scavenge 收集器主要负责新生代的回收,Serial old 收集器、Parallel Old 收集器、CMS 收集器主要负责老年代的回收,G1 收集器则同时负责两个区域的回收。

Serial 收集器

Serial 收集器用于新生代的垃圾回收中,采用简单的复制算法,以串行的方式执行,单线程、简单高效(限定单个CPU的环境来说),Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(即STW)。

Serial old 收集器

Serial Old 收集器是Serial 收集器的老年代版本,不同的是它采用标记-整理算法。

ParNew 收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本。除了使用多线程外其余行为均和Serial收集器一模 一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。ParNew 收集器默认开启的收集线程数与 CPU 的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。

Parallel Scavenge 收集器

Parallel Scavenge 收集器与吞吐量关系密切,故也称为吞吐量优先收集器(吞吐量 = 用户线程执行时间/总时间 * 100%)。 它是属于新生代的、采用复制算法的多线程收集器,目标是达到一个可控制的吞吐量,同时它还有GC自适应调节策略,这是与 ParNew 收集器最重要的区别。

Parallel Scavenge收集器可设置 -XX:+UseAdptiveSizePolicy 参数,当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

Parallel Scavenge收集器使用两个参数控制吞吐量:XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间; XX:GCRatio 直接设置吞吐量的大小。

Parallel Scavenge的设计目标是通过并行执行来实现高吞吐量。它更注重整体系统的工作效率而不是单次垃圾回收的停顿时间,适用于对实时性要求相对较低的应用。

Parallel Old 收集器

Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,是多线程的,采用标记-整理算法实现。

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS 收集器主要用于要求低延迟(即提高响应速度)的互联网项目。设置使用 CMS 收集器参数是:- XX:+UseConcMarkSweepGC。CMS 收集器采用的算法是标记-清除算法。

CMS 垃圾收集器特点:

  • CMS 只会回收老年代和永久代(JDK1.8 为元数据区,CMS 收集器默认不会对永久代进行垃圾回收,如希望对永久代进行垃圾回收,可通过设置参数:-XX:+CMSClassUnloadingEnabled 开启对永久代的垃圾回收,该参数默认关闭);
  • CMS是一种预处理垃圾回收器,它需要在老年代内存耗尽前完成垃圾回收,否则会导致并发回收失败(并发失败会退化为SerialOld 单线程垃圾回收器),因此,CMS 有一个触发垃圾回收的阀值(参数:-XX:CMSInitiatingOccupancyFraction,默认值为92%),即老年代或永久代内存达到 92% 开始进行垃圾回收。

CMS垃圾回收过程主要分为初始标记、并发标记、并发预处理、可终止预处理、重新标记、并发清除、并 发重置七个步骤。

Initial Mark(初始化标记)

初始标记主要是标记存活的对象,存活对象包含两部分:

  • 标记老年代中所有的GC Roots直接引用的对象;
  • 标记年轻代中直接引用的老年代的存活对象。
初始标记
初始标记

Concurrent Mark(并发标记)

在初始标记的基础上,进行并发标记。这一步骤主要是 tracinng 的过程,用于标记所有可达的对象。这个过程会持续比较长的时间,但却可以和用户线程并行。在这个阶段的执行过程中,可能会产生很多变化:比如对象从新生代晋升到了老年代、对象被分配到老年代、老年代或者新生代的对象引用发生了变化。在这个阶段受到影响的老年代对象所对应的卡页,会被标记为 dirty,用于后续重新标记阶段的扫描。

在并发标记阶段,由于标记期间与应用程序并行,对象间的引用关系可能发生变化,因此采用三色标记的方式对对象进行标记,标记过程分为三种颜色:白色、灰色、黑色。

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

标记过程的具体步骤:

  1. 初始时,所有对象都在 【白色集合】中。
  2. 将GC Roots直接引用到的对象转移【灰色集合】中。
  3. 从灰色集合中获取对象A:
  4. 将A对象引用到的其他对象全部转移【灰色集合】中。
  5. 将A对象转移【黑色集合】中。
  6. 重复步骤3-4-5,直至【灰色集合】为空时结束,结束后仍在【白色集合】的对象即为不可达,可以进行回收。
并发标记
并发标记

因为在标记过程中可能存在引用对象的变化,所以三色标级存在多标和漏标问题。(漏标问题:未被访问到的对象,被【黑色集合】中的对象引用了,这时候该对象就被漏标了。这种情况使用增量更新方法解决:将新增的引用维护到一个集合中,将引用的源头变为灰色,等待重新标记阶段再重新进行一次扫描。 如:当D的引用指向了C,则会将C变为灰色,并将C放到一个新增引用的集合中,在重新标记阶段会将C作为根节开始继续向下扫描。多标问题:已被扫描过的对象,后来被清空了,这时候不会清除它。这种问题不会产生bug,等待下次回收即可。)

Concurrent Preclean(并发预清理)

重新扫描前一个阶段标记的 Dirty 对象,并标记被 Dirty 对象直接或间接引用的对象,然后清除 Card 标识。

Concurrent Abortable Preclean(可中止的并发预清理)

可终止预处理阶段与并发预处理节点一样,主要是处理并发阶段因引用关系发生变更而未标记到的存活对象(即:扫描所有标记为 Dirty 的 Card)。为什么需要这个阶段?因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。进入可终止预处理阶段的是有条件的,通过配置 CMSScheduleRemarkEdenSizeThreshold (默认值 2M)参数来控制,当新生代Eden 区对象超过 2M时才会进入,如果新生代的对象太少就没有必要执行该阶段,直接执行重新标记阶段。在该阶段,主要循环的做两件事:

  1. 处理 From 和 To 区的对象,标记可达的老年代对象
  2. 和上一个阶段一样,扫描处理Dirty Card中的对象

这个逻辑不会一直循环下去,打断这个循环的条件有三个:

  1. 可以设置最多循环的次数 CMSMaxAbortablePrecleanLoops,默认是0,表示没有循环次数的限制。
  2. 如果执行这个逻辑的时间达到了阈值 CMSMaxAbortablePrecleanTime,默认是5s,会退出循环。
  3. 如果新生代Eden区的内存使用率达到了阈值 CMSScheduleRemarkEdenPenetration,默认50%,会退出循环。(这个条件能够成立的前提是,在进行Precleaning时,Eden区的使用率小于十分之一)

如果在循环退出之前,发生了一次YGC,对于后面的Remark阶段来说,大大减轻了扫描年轻代的负担,但是发生YGC并非人为控制,所以只能祈祷这5s内可以来一次YGC。

最后CMS还提供了参数 CMSScavengeBeforeRemark,表示进入重新标记前是否强行执行一次 Minor GC(默认关闭,建议开启,开启方式:-XX:+CMSScavengeBeforeRemark)。

Final Remark(重新标记)

预清理阶段也是并发执行的,并不一定是所有存活对象都会被标记,因为在并发标记的过程中对象及其引用关系还在不断变化中。因此,需要有一个 STW 的阶段来完成最后的标记工作,这就是重新标记阶段(CMS标记阶段的最后一个阶段),其主要目的是重新扫描之前并发处理阶段的所有残留更新对象。

Concurrent Sweep(并发清理)

并发清理阶段主要工作是清理所有的死亡对象,回收被占用的空间。由于是并发阶段,此时仍然会产生一些不可达对象,称为浮动垃圾。这些只能在下个回收周期才能被回收。

Concurrent Reset(并发重置)

并发重置阶段,将清理并恢复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构,为下一个垃圾收集周期做好准备。

由于CMS采用标记-清除算法,回收过程会产生内存碎片。如果内存碎片过多时,会给大对象分配带来影 响(如:老年代剩余空间足够,却没有足够的连续内存空间分配给大对象,从而触发Full GC)。针对这种情况,CMS提供了两个参数进行优化:

  • UseCMSCompactAtFullCollection(默认开启):表示在要进行Full GC时,进行内存碎片整理。内存整理的过程是无法并发的,所以停顿时间会变长。
  • CMSFullGCsBeforeCompaction:表示执行指定次数不压缩的Full GC后,执行一次带压缩的Full GC。默认值为0,表示每次进入Full GC时都进行碎片整理。

注意:CMSFullGCsBeforeCompaction参数虽然会降低Full GC压缩频率,减少停顿时长,但是会加剧内存碎片的产生,增加Full GC触发频率,因此,设置时需要在Full GC停顿时长和内存碎片数量之间做权衡。

CMS 回收器的缺点

  • 并发清理阶段存在浮动垃圾;
  • Full GC 算法是标记清除,会产生磁盘碎片;
  • 新生代配合 ParNew GC 使用,存在STW问题,如果heap很大,可能GC时间很大,影响线上服务。

G1 收集器

G1 收集器(Garbage-First)从 JDK7 开始被引入,在 JDK9 被设为默认垃圾收集器,目标就是彻底替换掉 CMS 收集器。G1 收集器在逻辑上还是划分 Eden、survivor 和 old,但是物理上他们不是连续的。G1 收集器将内存分成一个个的 Region,且不要求各部分是连续的。每个 Region 的大小在 JVM 启动时就确定,JVM 通常生成 2048个 左右的 Heap 区,根据堆内存的总大小,区的 size 范围为 1-32 Mb(2的n次方),一般 4M。这样的划分使得 GC 不必每次都去收集整个堆空间,而是以增量的方式来处理:每次只处理一部分小堆区,称为此次的回收集(collection set)。 每次暂停都会收集所有年轻代的小堆区, 但可能只包含一部分老年代小堆区。G1 的另一项创新是在并发阶段估算每个小堆区存活对象的总数。用来构建回收集的原则是垃圾最多的小堆区会被优先收集,这也是 garbage-first 名称的由来。G1 的内存模型如下图所示:

G1收集器内存模型
G1收集器内存模型

G1 收集器是物理分区,逻辑分代的,图中红色区域是年轻代,包含 Eden 区(红色不带S)和 Survivor 区(红色带S);蓝色区域是老年代,包含 Old 区(蓝色不带H)和 Humongous 区(跨多个区域组成的大对象区域,蓝色带H);灰色区域表示空闲区(Free 区)。H 区保存比标准 region 区大50%及以上的对象,存储在一组连续的区中,需要注意的是每一个H 区最多只能保存一个巨型对象,剩余空间得不到利用,会有内存碎片。同时由于转移巨型对象会影响 GC 效率,所以在标记阶段发现巨型对象不再存活时,会被直接回收。划分成 Region 的好处在于,G1 能够根据需要动态调整不同代的内存大小。例如,如果新生代空间不足,G1 可以从 Free 类型的 Region 中划分一块成为 Eden 类型的 Region。

RSet (Remembered Set)

G1 位了避免STW 的整堆扫描,在每个 Region 都维护了一个已记忆集合(RSet),其内部类似一个反向指针,用于记录不同 Region 之间的跨 Region 引用关系。例如,有两个对象 A 和 B,且 A 在 RegionA 上,B 在 RegionB 上,对象 A 的某个属性是 B,那就意味着 A 引用着 B。此时,RegionB 对应的 RSet 上就会记录着 A 引用着 B,即 RSet 上记录的是别的区域对本区域对象的引用。RSet 的数据结构类型可分为以下三种:

  • 稀疏模式(Sparse):通过哈希表方式实现。Key 是别的 RegionID,而值是 Card 地址数组,即别的Region对应的Card的地址。
  • 细粒度模式(Fine-grained):通过哈希表方式实现。Key 是别的 RegionID,而值是直接引用地址的数组。
  • 粗粒度模式(Coarse-grained):一个 RegionID 数组,里面分别是别的 Region 的 ID。

CSet(Collect Set)

收集集合(CSet)代表每次 GC 暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中。候选老年代分区的 CSet 准入条件,可以通过活跃度阈值 -XX:G1MixedGCLiveThresholdPercent (默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据 CSet 对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。由上述可知,G1的收集都是根据CSet进行操作的,年轻代收集与混合收集没有明显的不同,最大的区别在于两种收集的触发条件。

内存划分与 LAB(本地缓冲区)

当需要对新的内存区域进行划分时,了解 LAB 的概念非常重要。由于 Region 通常较大,内存申请和划分往往需要更小的粒度。因此,引入了 LAB,它允许更细粒度的内存分配。

CARD

为了更有效地管理内存,G1 将每个 Region 进一步划分为多个 Card,通常大小为 512B。这样,一个 Region 就包含多个 Card,Card 是 G1 进行内存管理和垃圾回收的最小单位。一个对象可能跨越多个 Card,或者一个 Card 内存储多个对象。

CardTable

为了全局管理 Card,引入了 CardTable。CardTable 用于记录 Card 内对象的引用情况,是 G1 垃圾回收过程中的关键数据结构。CardTable 可以被理解为一个字节数组,其中 Card 的首地址就是数组的下标,下标对应的值表示该 Card 上的对象是否发生了引用修改。

LAB (Local Allocation Buffer 本地缓冲区)

LAB 是每个线程的私有内存分配区域,减少线程间的竞争,用于加速对象的分配过程。这里的私有内存是堆内存里Eden区域中的内存,只不过对应的线程管理自己负责部分的内存区域,而且如果使用完可以重新申请LAB。

G1 垃圾回收算法整体是使用“标记-整理”算法,Region 之间基于“复制”算法。G1 的运行过程与CMS 大体相似,分为以下四个步骤:

  • 初始标记(Initial Marking):仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用执行 Minor GC 的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
  • 并发标记(Concurrent Marking):从GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,并发时有引用变动的对象会产生漏标问题,G1 中会使用 SATB(Snapshot-At-The-Beginning) 来解决。
代码语言:txt
复制
SATB算法是一种垃圾回收算法,它通过在并发标记过程中采用快照技术,
确保在标记过程中应用线程不会干扰标记线程的工作,从而提高了垃圾回收的效率和可靠性。
SATB算法的基本思想可以概括为以下几点:
    1、并发标记前快照‌:在并发标记开始之前,对内存中的对象进行快照,这个快照记录了标记开始时的对象状态。
    2、独立标记‌:基于这个快照,标记线程独立进行标记,而应用线程不会直接修改这个快照中的对象,
       从而保证了标记过程的准确性。
    3、新分配对象的处理‌:应用线程新分配的对象都被认为是活跃对象,这些对象在下一个并发标记周期进行标记。
    4、引用关系变更处理‌:在并发标记过程中,如果对象的引用关系发生变化(例如引用被删除),
       这些变化在后续的标记周期中被单独处理,确保了垃圾回收的正确性。
  • 最终标记(Final Marking):对用户线程做一个短暂的暂停,用于处理并发标记阶段仍遗留下来的最后那少量的 SATB 记录(漏标对象)。
  • 筛选回收(Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多个收集器线程并行完成的。

G1 有两个 TAMS 指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象的分配。并发回收时新分配的对象地址都必须在这两个指针之上,G1 收集器默认在这个地址上的对象是存活的,不纳入回收范围。

G1的GC类型可分为以下两种:

Y-GC

在分配一般对象(非巨型对象)时,当所有 Eden 的 region 使用达到最大阀值并且无法申请足够内存时会出发 YGC。YGC 会回收所有 Eden 以及 Survivor 区,并且将存活对象复制到 Old 区以及另一部分的 Survivor 区。因为 YoungGC 会进行根扫描,所以会 STW。YoungGC 的回收过程如下:

  • 根扫描,跟 CMS 类似,Stop the world,扫描 GC Roots 对象;
  • 处理 Dirty card(即保存了对象的小区域),更新 RSet(分区间对象的引用关系);
  • 扫描 RSet,扫描 Old 区对象对于本young区的引用;
  • 拷贝扫描出的存活的对象到 Survivor2 或 Old 区;
  • 处理引用队列,软引用,弱引用,虚引用。

Mixed-GC

一次 YoungGC 之后,老年代占据堆内存的百占比超过 InitiatingHeapOccupancyPercent(默认45%)时,就会触发MixedGC。混合回收都是基于复制算法进行的,把要回收的 Region 区存活的对象放入其他 Region,然后这个 Region 全部清理掉,这样就会不断空出来新的 Region。混合回收停止条件可以由参数 -XX:G1HeapWastePercent 控制,默认值5%,表示空出来的区域大于整个堆的5%,就会立即停止混合回收了。如正常默认回收次数是8次,但是可能到了4次,空闲 Region 大于整个堆的 5%,就不会再进行后续回收了。MixGC 过程如下:

  • 标记GC roots:一般直接复用 YoungGC 中的结果;
  • 根分区扫描(RootRegionScan):这个阶段 GC 的线程可以和应用线程并发运行。其主要扫描初始标记以及之前YGC对象转移到的 Survivor 分区,并标记 Survivor 区中引用的 对象。所以此阶段的 Survivor 分区也叫根分区(RootRegion);
  • 并发标记(ConcurrentMark):会并发标记所有非完全空闲的分区的存活对象,使用了SATB算法,标记各个分区;
  • 最终标记(Remark):主要处理 SATB 缓冲区,以及并发标记阶段未标记到的漏网之鱼 (存活对象),会STW。
  • 清除阶段(Clean UP):整理堆分区,调整相应的 RSet(比如如果其中记录的 Card 中的对象都被回收,则这个卡片的也会从RSet中移除),如果识别到了完全空的分区,则会清理这个分区的RSet。这个过程会STW。
  • 对存活对象进行转移(复制算法),转移到其他可用分区,所以当前的分区就变成了新的可用分区。使用复制转移主要是为了解决分区内的碎片问题。

Full-GC

G1 在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发 FullGC。开始版本FullGC 使用的是 stop the world 的单线程的 Serial Old 模式。JDK10 以后, Full GC 已经是并行运行,在很多场景下,其表现还略优于 Parallel GC 的并行 Full GC 实现。

官方文档:Garbage-First Garbage Collection

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 垃圾判断(垃圾标记)
    • 引用计数法
      • 可达性分析
      • 垃圾回收算法
        • 标记-清除算法
          • 复制算法
            • 标记-整理算法
            • 垃圾清除
              • Minor GC
                • Full GC
                • 垃圾回收器
                  • Serial 收集器
                    • Serial old 收集器
                      • ParNew 收集器
                        • Parallel Scavenge 收集器
                          • Parallel Old 收集器
                            • CMS 收集器
                              • Initial Mark(初始化标记)
                              • Concurrent Mark(并发标记)
                              • Concurrent Preclean(并发预清理)
                              • Concurrent Abortable Preclean(可中止的并发预清理)
                              • Final Remark(重新标记)
                              • Concurrent Sweep(并发清理)
                              • Concurrent Reset(并发重置)
                              • CMS 回收器的缺点
                            • G1 收集器
                              • RSet (Remembered Set)
                              • CSet(Collect Set)
                              • 内存划分与 LAB(本地缓冲区)
                              • CARD
                              • CardTable
                              • LAB (Local Allocation Buffer 本地缓冲区)
                              • Y-GC
                              • Mixed-GC
                              • Full-GC
                          相关产品与服务
                          应用性能监控
                          应用性能监控(Application Performance Management,APM)是一款应用性能管理平台,基于实时多语言应用探针全量采集技术,为您提供分布式性能分析和故障自检能力。APM 协助您在复杂的业务系统里快速定位性能问题,降低 MTTR(平均故障恢复时间),实时了解并追踪应用性能,提升用户体验。
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档