JVM工作原理与实战(三十九):G1垃圾回收器原理

简介: JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了G1垃圾回收器执行流程、年轻代回收原理、卡表(Card Table)、记忆集的生成流程、年轻代回收的详细步骤、混合回收的步骤、初始标记、并发标记、SATB、转移等内容。

一、G1垃圾回收器

1.G1垃圾回收器执行流程

G1(Garbage-First)垃圾回收器是Java HotSpot虚拟机中的一种垃圾收集器,它旨在提供可预测的停顿时间,同时实现高吞吐量。G1垃圾回收器主要通过两种方式进行垃圾回收:年轻代回收(Young GC)和混合回收(Mixed GC)。

年轻代回收:

  • 1.对象分配与判断:新创建的对象首先会被放置在Eden区。G1垃圾回收器通过监控年轻代的使用情况,当判断年轻代区域已满(超过60%容量)时,触发Young GC。
  • 2.存活对象标记:在执行Young GC时,G1首先会精确地标记出Eden和Survivor区域中的存活对象。
  • 3.对象复制与区域清空:根据预设的最大暂停时间和其他配置参数,G1选择某些区域,将存活对象复制到一个新的Survivor区(对象的年龄加1),并清空这些区域。

image.gif
  • 4.性能记录与优化:在执行Young GC过程中,G1垃圾回收器会记录每次回收时每个Eden区和Survivor区的详细耗时数据。这些数据为下次回收提供了宝贵的参考,帮助G1更精确地计算出在给定的最大暂停时间内可以回收的Region数量。例如,如果配置的-XX:MaxGCPauseMillis为n(默认200),每个Region的回收耗时为40ms,那么在一次回收中,G1最多能处理4个Region。
  • 5.循环与移动:后续的Young GC过程与之前相似,Survivor区中的存活对象会被移动到另一个Survivor区。

image.gif
  • 6.老年代与Humongous区:当某个对象的年龄达到预设阈值(默认15)或其大小超过一个Region的一半时,该对象会被移入老年代。这些老年代被称为Humongous区。例如,在堆内存为4G、每个Region为2M的环境中,任何超过1M的对象都会被放入Humongous区。如果对象过大,可能会跨越多个Region。

image.gif

混合回收:

  • 7.触发条件与处理:随着时间的推移,老年代中会出现多个区域。当总堆占有率达到预设阈值(-XX:InitiatingHeapOccupancyPercent默认45%)时,G1会触发混合回收(Mixed GC)。这种回收会处理所有年轻代和部分老年代的对象以及大对象区。混合回收采用复制算法来完成,确保高效的内存回收。

image.gif

二、年轻代回收

1.年轻代回收原理

在G1垃圾回收器的年轻代回收过程中,主要关注的对象是年轻代内存区域中的对象,包括Eden区和Survivor区。由于回收范围限定在年轻代内,因此从GC Root(垃圾回收根)出发,可以轻松地扫描并识别出年轻代中的对象,以及年轻代对象之间的引用关系。

image.gif

然而,年轻代回收面临一个挑战:如何识别和处理老年代对象对年轻代对象的引用。如果老年代中存在对象引用了年轻代中的对象,那么这些年轻代对象在回收过程中不应被错误地清除。

image.gif

为了解决这个问题,G1垃圾回收器采用了一种称为记忆集(RememberedSet,简称RS或RSet)的数据结构。记忆集详细记录了非收集区域(如老年代)对象引用收集区域(年轻代)对象的关系。在年轻代回收时,记忆集中的对象被临时加入到GC Root中,这样垃圾回收器就能够根据引用链准确地判断哪些对象需要回收,哪些对象由于被老年代引用而需要保留。

image.gif

为了进一步优化内存使用,G1垃圾回收器采用了分块记录的策略。它将每个区域中的内存按照一定大小划分成多个块,并为每个块分配一个编号。在记忆集中,不再记录单个对象的引用关系,而是记录对块的引用关系。这样,即使一个块中包含多个对象,也只需要记录一次块的引用,从而显著减少了内存开销。

image.gif

通过这种方式,G1垃圾回收器在年轻代回收过程中能够高效、准确地处理老年代对年轻代的引用问题,确保回收过程的正确性和效率。

2.卡表(Card Table)

每一个Region都配备有一个独立的卡表(Card Table),用于记录该Region中对象的跨代引用情况。当发生从老年代到年轻代的跨代引用时,相应Region的卡表会相应地更新其字节内容。在JDK 8的源码中,如果某个字节被标记为0,这表示该字节所代表的内存块被老年代对象所引用,这种情况被称为“脏卡”。通过这种方式,能够精准地识别出哪些老年代的部分引用了当前Region中的对象。

image.gif

生成记忆集(Remembered Set,简称RS)的过程变得相对直观。系统只需遍历整个卡表,识别并记录下所有标记为脏卡的条目。这些脏卡就构成了记忆集,反映了老年代对年轻代的引用关系。

image.gif

在年轻代回收的标记阶段,垃圾回收器会将记忆集中的对象临时加入到GC Root对象集中。随后,从这些GC Root对象出发,垃圾回收器会遍历并标记其引用链上的所有对象。这一过程确保了所有被老年代引用的年轻代对象都能被正确识别并保留,而未被引用的对象则会被标记为待回收状态。通过这一机制,G1垃圾回收器能够高效地管理内存,确保系统的稳定运行和性能优化。

写屏障:

JVM采用写屏障(Write Barrier)技术来维护卡表,这种技术允许在执行涉及引用关系建立的代码时,在相应指令之前和之后插入特定的操作。这些插入的指令负责更新卡表的状态,确保卡表能够准确反映对象之间的跨代引用关系。

image.gif

值得注意的是,记忆集(Remembered Set)的设计并不会记录新生代内部对象之间的引用关系,即不会记录同一个Region内部的对象引用。这是因为新生代内部的垃圾回收通常通过复制算法来处理,该算法假定同一代内的对象没有相互引用,从而简化了回收过程。因此,记忆集专注于记录那些可能导致跨代引用的情况,即老年代对象引用年轻代对象的情况,以确保垃圾回收的正确性和效率。

3.记忆集的生成流程

记忆集的生成流程是一个精心设计的机制,旨在确保跨代引用的准确性并减少线程冲突。以下是这一流程的详细步骤:

  1. 捕获引用变更信息:首先,JVM利用写屏障技术来捕获所有涉及跨代引用的变更信息。写屏障是一种在对象引用写入时触发的机制,它能够拦截这些操作并提取必要的引用信息。
  2. 记录引用关系到卡表:捕获到的引用变更信息会被记录到卡表中。卡表是一种高效的数据结构,用于快速识别哪些内存块可能包含被老年代引用的年轻代对象。同时,这些变更也会被标记为“脏卡”,并加入到一个专门的脏卡队列中。
  3. 定期生成记忆集:为了避免多个线程同时访问和修改记忆集,导致数据不一致和性能下降,JVM采用了一种称为“精细化”(Refinement)的线程来处理脏卡队列。这个线程会定期从脏卡队列中取出数据,并根据这些数据生成或更新记忆集。通过这种方式,JVM确保了记忆集的生成是一个有序且可控的过程,从而提高了系统的稳定性和性能。

image.gif

通过这种机制,JVM能够高效地管理内存中的对象引用关系,特别是在涉及跨代引用的情况下,从而实现了更加精准和高效的垃圾回收。

4.年轻代回收的详细步骤

年轻代回收的详细步骤如下:

  • Root扫描:这是年轻代回收的起始步骤。在这个阶段,垃圾回收器会扫描所有的静态变量和局部变量,以确定哪些对象是当前可达的(即仍然被引用的)。这一步骤是标记-复制算法的基础。

image.gif

  • 处理脏卡队列:接下来,垃圾回收器会处理脏卡队列中尚未处理的信息。这些信息反映了老年代对象对年轻代的引用情况。通过处理这些信息,垃圾回收器会更新记忆集的数据,确保记忆集包含了所有老年代对当前Region的引用关系。这一步骤确保了跨代引用的准确性。

image.gif

  • 标记存活对象:在完成记忆集的更新后,垃圾回收器会开始标记存活对象。首先,记忆集中的对象会被加入到GC Root对象集合中。然后,从GC Root对象出发,垃圾回收器会遍历整个引用链,将链上的所有对象都标记为存活对象。这一步骤确保了只有仍在被引用的对象才会被保留。
  • 选择回收集合:根据设定的最大停顿时间,垃圾回收器会选择本次收集的区域,这个区域被称为回收集合(Collection Set)。选择回收集合的目的是为了在满足停顿时间要求的同时,尽可能多地回收内存。

image.gif

  • 复制对象:在确定了回收集合后,垃圾回收器会开始复制对象。首先,标记出来的存活对象会被复制到新的区域中,同时它们的年龄会增加1。如果某个对象的年龄达到15(这是一个阈值,表示该对象已经经历了多次复制),那么该对象会被晋升到老年代。最后,旧的区域内存会被直接清空,以便后续使用。
  • 处理特殊引用:在完成对象的复制后,垃圾回收器还需要处理一些特殊类型的引用,包括软引用、弱引用、虚引用以及终结器引用。此外,还需要处理Java Native Interface(JNI)中的弱引用。这些特殊引用在Java内存管理中扮演着重要角色,因此需要在垃圾回收过程中得到妥善处理。

image.gif

5.G1年轻代回收核心技术总结

卡表(Card Table):卡表是G1垃圾回收器中的一个核心组件,每个Region都配备有一个独立的卡表。卡表本质上是一个字节数组,用于记录Region内部对象与老年代对象之间的跨代引用关系。当发生跨代引用时,G1会识别出该引用,并将卡表中相应位置的字节内容修改为0,这样的卡表条目被称为“脏卡”。卡表的主要作用是为生成记忆集(Remembered Set,简称RS或RSet)提供必要的数据支持。卡表的大小与堆的大小直接相关。例如,当堆大小为1GB时,卡表的大小为1GB ÷ 512 = 2MB。这是因为卡表中的每个字节负责监控一定内存范围内的对象引用情况,通常这个范围被设置为512字节。

记忆集(Remembered Set,简称RS或RSet):每个Region都拥有一个独立的记忆集,用于记录从老年代引用到当前Region中对象的详细信息。这些信息包括被引用对象在卡表中的位置等。在标记阶段,垃圾回收器会将记忆集中的对象加入到GC Root对象集合中,并一同进行扫描。这样,垃圾回收器就能够准确地识别出哪些对象是被引用的,从而将它们标记为存活状态。

写屏障(Write Barrier):G1垃圾回收器采用写屏障技术来维护卡表的准确性。写屏障是一种在对象引用写入操作后自动触发的机制,它会在引用关系建立后的代码中插入一段指令。这些指令负责更新卡表的状态,确保卡表能够实时反映对象之间的跨代引用关系。虽然写屏障的引入会带来一定的性能开销,通常这个开销大约在5%~10%之间,但它对于确保垃圾回收的正确性和效率至关重要。

三、混合回收

1.混合回收的步骤

在G1垃圾回收器的运行过程中,随着多次的年轻代回收,会逐渐形成多个Old老年代区域。当整个堆内存的使用率达到一个预设的阈值(默认为45%)时,将触发混合回收(Mixed GC)。混合回收的触发时机不仅限于总堆占有率的阈值达到,它还可能由年轻代回收之后或当分配大对象时触发。在混合回收过程中,垃圾回收器将同时处理整个年轻代和部分老年代的内存。

鉴于老年代中可能存在大量的对象,直接标记所有存活对象可能会消耗较多的时间。为了提升效率,减少应用程序的停顿时间,混合回收的整个标记过程被设计为尽量与用户线程并行执行。这样可以在不影响应用程序性能的前提下,更有效地管理内存。混合回收的具体步骤如下:

  1. 初始标记(Initial Mark):这是一个Stop-The-World(STW)阶段,使用三色标记法来快速标记从GC Root直接可达的对象。这一步确保了回收过程中不会遗漏任何重要的根对象。
  2. 并发标记(Concurrent Mark):在此阶段,标记工作与用户线程并发执行。垃圾回收器遍历对象图,对存活的对象进行标记。这个过程可以充分利用多核处理器的并行能力,提高标记效率。
  3. 最终标记(Final Mark):再次进入STW阶段,处理与SATB(Snapshot-At-The-Beginning)相关的对象标记。SATB是一种在GC开始时捕获对象图快照的技术,它确保了在并发标记期间新创建的对象也能被正确标记。
  4. 清理(Cleanup):另一个STW阶段,清理那些没有任何存活对象的区域。这些区域将被回收,以便后续的内存分配。
  5. 转移(Evacuate):最后,将存活的对象从它们的当前区域复制到其他空闲区域。这个过程可能涉及对象的移动和指针的更新,因此也是STW的。

通过这一系列的步骤,混合回收有效地管理了年轻代和老年代的内存,同时尽量减少了应用程序的停顿时间,提高了整体性能。

2.初始标记

初始标记阶段是混合垃圾回收(Mixed GC)的一个重要组成部分。在这个阶段,所有的用户线程会被暂停,以确保垃圾回收器能够专心标记从GC Root直接可达的对象。由于只关注直接从GC Root出发的引用链,因此这一阶段的停顿时间通常不会过长,从而减少了对应用程序性能的影响。

image.gif

在初始标记中,G1垃圾回收器采用了三色标记法来识别对象的状态。这种方法在原有的双色标记(黑色代表存活,白色代表可回收)基础上增加了一种灰色状态。三色标记法通过引入灰色来标识那些当前对象在GC Root引用链上,但其引用的其他对象尚未完成标记的情况。三色标记的具体定义如下:

  • 黑色:表示当前对象不仅自身在GC Root的引用链上,而且它所引用的所有对象也已经被标记为存活。在位图实现中,黑色对象通过相应的bit位被标识为1。
  • 灰色:表示当前对象在GC Root的引用链上,但其引用的其他对象可能尚未被标记。灰色对象不会直接体现在位图中,而是被放入一个专门的队列中,等待后续处理。
  • 白色:表示对象不在GC Root的引用链上,因此可以被视为可回收的候选对象。在位图实现中,白色对象通过相应的bit位被标识为0。

image.gif

在位图(bitmap)的实现中,G1垃圾回收器通常会使用1个bit来标识8个字节的内容。例如,如果某个对象是黑色的,那么对应的bit位会被设置为1;如果是白色的,则bit位为0。对于灰色对象,由于它们不会直接体现在位图中,因此位图中相应的bit位保持为0,而灰色对象会被单独放入一个队列中,以便后续处理。如果某个对象的大小超过8个字节,通常只会使用其第一个bit位进行处理,以确保内存使用的效率。

image.gif

通过这种方式,G1垃圾回收器能够在初始标记阶段快速准确地识别出从GC Root直接可达的存活对象,为后续的内存回收操作提供基础数据。

3.并发标记

接下来,系统进入并发标记阶段,该阶段将并行处理之前尚未完成的标记任务,同时与用户线程并发执行,以实现更高的效率。

在这一阶段,系统从灰色队列中提取出尚未完成标记的对象B,并对其关联的A和C对象进行标记。在此过程中,系统发现A对象并未引用其他任何对象,因此可以立即将其标记为黑色,表示其已被完全标记且不会被回收。同时,由于B对象已完成了对其所有引用对象的标记,因此也将B对象标记为黑色。然而,C对象引用了另一个对象E,因此C对象被暂时标记为灰色,并将其放入队列中等待进一步处理。随后,系统从队列中获取C对象,并对其进行标记。在这一过程中,系统确认C对象及其引用的E对象均已完成标记,因此将它们都标记为黑色。此时,系统中剩余的对象F由于未被标记,因此被视为白色对象,即垃圾对象,可以被安全地回收。

image.gif

然而,三色标记算法存在一个潜在的问题,即用户线程可能同时修改对象的引用关系,导致标记结果出现错误。例如,在本案例中,正常情况下B和C都应该被标记为黑色。但是,如果在B和C被标记之前,用户线程执行了B.c = null操作,将B到C的引用去除,同时执行了A.c = C操作,添加了A到C的引用,那么就会出现严重问题。因为此时C对象可能仍被错误地标记为白色或灰色,并被错误地视为可回收对象。一旦C对象被错误地回收,而代码中仍然存在对C对象的引用,那么在后续执行过程中就会出现空引用异常等重大问题。

image.gif

G1为了解决这个问题,使用了SATB技术(Snapshot At The Beginning, 初始快照)。

4.SATB

G1垃圾收集器为了克服三色标记算法在并发阶段可能遇到的对象引用变化问题,引入了SATB(Snapshot At The Beginning,初始快照)技术。SATB技术的核心思想是在标记过程的起始阶段捕捉一个对象的快照,并基于这个快照来进行后续的标记工作。

SATB技术的具体实现如下:

  • 在标记阶段开始时,G1垃圾收集器会创建一个当前所有对象的快照。在这个快照之后新生成的对象,由于它们尚未被任何旧对象引用,因此它们会被直接标记为黑色,表示它们是活跃的,不应该被回收。
  • 为了处理在标记过程中可能发生的对象引用变化,G1采用了前置写屏障技术。这种技术会在引用赋值操作(如B.c = null)之前被触发,将即将被改变引用的对象(在这个例子中是c)放入SATB待处理队列中。每个线程都有自己的SATB队列,但最终这些队列会被汇总到一个全局的SATB队列中。

image.gif

  • 在标记阶段的最后,所有用户线程会被暂停,以处理SATB相关的对象标记。这一步是必要的,因为只有在所有线程都停止执行后,我们才能确保所有的引用变化都已经被捕获并处理。在这个阶段,所有线程的SATB队列中剩余的数据会被合并到全局的SATB队列中,并逐一进行处理。
  • 对于SATB队列中的对象,它们默认会被按照存活对象来处理,同时还会处理它们引用的其他对象。这意味着,即使一个对象在标记过程中被解除了引用,只要它曾经被引用过,并且这个引用变化被SATB捕获,那么这个对象就不会被错误地回收。

image.gif

然而,SATB技术也有其缺点。由于它基于初始快照进行标记,因此在本轮垃圾回收过程中,可能会将一些实际上应该被回收的不存活对象错误地标记为存活对象。这些错误标记的对象被称为“浮动垃圾”。这些浮动垃圾需要等到下一轮垃圾回收时才能被正确回收。

5.转移

在垃圾回收过程中的“转移”步骤,通常涉及到将存活的对象从一个内存区域复制到另一个内存区域,以便清理包含大量垃圾对象的区域。

转移步骤详解:

  • 区域选择:根据最终标记的结果,垃圾收集器会分析每个内存区域中垃圾对象所占用的内存大小。在此基础上,结合预期的停顿时间,垃圾收集器会选择转移效率最高的若干个区域进行转移操作。选择的标准通常是基于垃圾对象数量和区域的整体活跃对象比例,以最大化单次转移过程中的清理效率。

image.gif

  • 对象转移:在选择好目标区域后,垃圾收集器会开始转移过程。转移时,首先会处理GC Root直接引用的对象,这些对象通常是垃圾回收过程中的根节点,它们保证了程序的运行不会因垃圾回收而中断。在复制这些对象之后,垃圾收集器会继续转移其他非直接引用的对象,直到所有选定区域中的存活对象都被复制到新的内存区域。

image.gif

  • 引用关系更新:在对象转移完成后,垃圾收集器会清理掉原先区域中的垃圾对象,释放相应的内存空间。如果外部的其他区域对象引用了已经被转移的对象,垃圾收集器还需要更新这些引用关系,确保它们指向新的内存位置。这一步骤是确保程序在垃圾回收后能够继续正确运行的关键。通过更新引用关系,垃圾收集器确保了程序内部的对象引用不会因为内存位置的改变而失效。

image.gif

通过上述步骤,垃圾收集器能够有效地进行内存整理,减少内存碎片,提高内存的使用效率,并为应用程序提供持续稳定的运行环境。


总结

JVM是Java程序的运行环境,负责字节码解释、内存管理、安全保障、多线程支持、性能监控和跨平台运行。本文主要介绍了G1垃圾回收器执行流程、年轻代回收原理、卡表(Card Table)、记忆集的生成流程、年轻代回收的详细步骤、混合回收的步骤、初始标记、并发标记、SATB、转移等内容,希望对大家有所帮助。

相关文章
|
5天前
|
存储 算法 Java
先有JVM还是先有垃圾回收器?
是先有垃圾回收器再有JVM呢,还是先有JVM再有垃圾回收器呢?或者是先有垃圾回收再有JVM呢?历史上还真是垃圾回收更早面世,先有垃圾回收再有JVM。下面我们就来刨析刨析JVM的垃圾回收~
14 0
先有JVM还是先有垃圾回收器?
|
5天前
|
自然语言处理 前端开发 Java
深入浅出JVM(六)之前端编译过程与语法糖原理
深入浅出JVM(六)之前端编译过程与语法糖原理
|
3天前
|
Java 数据库连接 Spring
K8S+Docker理论与实践深度集成java面试jvm原理
K8S+Docker理论与实践深度集成java面试jvm原理
|
5天前
|
Arthas Prometheus 监控
JVM工作原理与实战(四十四):JVM常见题目
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了JVM常见题目等内容。
20 1
|
5天前
|
存储 缓存 算法
深入浅出JVM(二)之运行时数据区和内存溢出异常
深入浅出JVM(二)之运行时数据区和内存溢出异常
|
5天前
|
存储 Java
深入理解Java虚拟机:JVM内存模型
【4月更文挑战第30天】本文将详细解析Java虚拟机(JVM)的内存模型,包括堆、栈、方法区等部分,并探讨它们在Java程序运行过程中的作用。通过对JVM内存模型的深入理解,可以帮助我们更好地编写高效的Java代码,避免内存溢出等问题。
|
5天前
|
Java Linux Arthas
linux上如何排查JVM内存过高?
linux上如何排查JVM内存过高?
621 0
|
5天前
|
存储 缓存 算法
深入浅出JVM(十四)之内存溢出、泄漏与引用
深入浅出JVM(十四)之内存溢出、泄漏与引用
|
5天前
|
存储 缓存 Java
JVM 运行时内存篇
JVM 运行时内存篇
9 0
|
5天前
|
Arthas 监控 Java
JVM工作原理与实战(三十一):诊断内存泄漏的原因
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了诊断内存溢出的原因、MAT内存泄漏检测的原理等内容。
18 0