本文主要是介绍大白话详解G1垃圾回收器,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
JVM内存结构与G1垃圾回收器概述
众所周知,JVM 的内存结构由以下五部分构成:
- 堆(Heap)
- 栈(Stack)
- 方法区(Method Area)
- 本地方法区(Native Method Area)
- 程序计数器(Program Counter)
垃圾回收器主要管理的是堆内存。本文将详细介绍 G1 垃圾回收器 如何对堆内存进行管理。
G1垃圾回收算法
G1 垃圾回收器 采用以下算法对堆内存进行管理:
- 分代管理
- 复制算法
分代管理
分代管理包括:
- 新生代(包括 Eden 和 Survivor 区)
- 老年代(Old Generation)
- 超大对象区(Humongous Objects)
Region
G1 逻辑上将堆内存划分为多个大小不等的 Region。每个 Region 的大小通常在 2MB 到 32MB 之间,可通过 JVM 参数进行设置。在任何给定时间,所有 Region 必定属于以下类型之一:
- E(Eden)
- S(Survivor)
- O(Old)
- H(Humongous)
- F(Free)
划分成 Region 的好处在于,G1 能够根据需要动态调整不同代的内存大小。例如,如果新生代空间不足,G1 可以从 Free 类型的 Region 中划分一块成为 Eden 类型的 Region。
RSet (Remembered Set)
每个 Region 都会有一个自己的 RSet。RSet 用于记录不同 Region 之间的 跨 Region 引用关系。例如,有两个对象 A 和 B,且 A 在 RegionA 上,B 在 RegionB 上,对象 A 的某个属性是 B,那就意味着 A 引用着 B。此时,RegionB 对应的 RSet 上就会记录着 A 引用着 B,即 RSet 上记录的是别的区域对本区域对象的引用。
RSet 的数据结构类型
- 稀疏模式(Sparse):通过哈希表方式实现。Key 是别的 RegionID,而值是 Card 地址数组。
- 细粒度模式(Fine-grained):通过哈希表方式实现。Key 是别的 RegionID,而值是直接引用地址的数组。
- 粗粒度模式(Coarse-grained):一个 RegionID 数组,里面分别是别的 Region 的 ID。
内存划分与 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垃圾回收器的整个垃圾回收流程,主要分为四步,分别是初始标记、并发标记、再次标记和垃圾回收。简而言之,垃圾回收就是对不需要的对象进行内存释放。那么,如何找到这些不需要的对象呢?我们都知道Java的对象之间存在相互引用,类似于一个网状结构,因此确定第一个需要保留下来的对象就非常关键了。因此,GC Root的概念就出现了。
GC Root分为四类:
- 虚拟机栈(栈中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即常说的native方法中)引用的对象。
凡是被GC Root引用的对象就会被保留下来,然后这些对象所引用的对象也要被保留下来,否则程序无法运行。通过这种方式拓扑开来,所有需要保留下来的对象都会被扫描到。这种算法叫三色标记法:
具体来说,三色标记法的对象初始状态为白色。如果当前对象被扫描到但其引用的对象尚未被扫描完,则该对象被标记为灰色(如对象B,其引用的D对象尚未被扫描)。如果一个对象及其引用的所有对象都被扫描完毕,则该对象被标记为黑色(如A对象)。黑色和灰色对象会被保留下来,而被标记为黑色的对象不会被再次扫描,这就是三色标记法。
这样看来,垃圾回收实际上可以分为两步:一步进行垃圾标记,然后对垃圾进行回收。那么,为什么G1要分为四步呢?这就要从G1的特性来说了。G1的特点是出色的响应时间以及可以设置垃圾回收时的停顿时间。
大家可以想象,代码在运行过程中会伴随着对象的修改。由于堆内存中的对象数量较大,且寻址扫描非常耗时,通常情况下,扫描一遍进行标记,扫描到的对象就保持其当前颜色。例如,在标记初期,A被标记为黑色并保留下来,而E没有被任何对象引用,因此被标记为白色并视为垃圾。然而,如果在标记阶段,垃圾回收线程和程序的工作线程同时运行,E对象可能会在标记后被A对象引用,但由于E已经被标记为白色,若作为垃圾回收,程序将会出错。这种情况也可能在垃圾清理过程中出现。因此,最简单的做法是停止应用线程,只让垃圾回收线程工作,这就是STW(Stop The World)。这种做法显然不符合G1的特性,因此G1将原来的两步分为四步。
初始标记阶段通过找到GC Root引用的对象,这一步需要STW,但非常快速。接下来是并发标记阶段,这一步主要对堆内存中的对象进行拓扑扫描和三色标记。由于对象数量庞大,这一环节耗时较长,因此不能STW,而是让应用线程和垃圾回收线程同时工作。被标记为黑色的对象不会被再次扫描,但这也可能出现前面提到的漏标问题,对应的也可能出现多标的问题,比如之前标记为黑色或者灰色,但是并行标记过程中不再被引用,那么这些被多标的对象就会成为浮游垃圾,本次垃圾回收不会对其回收,下次垃圾回收会回收掉,为了解决漏标问题,出现了两种理论:SATB(原始快照)和增量更新。G1采用前者,而另一种并发垃圾回收器则采用后者。这两种理论的解决方案都基于写屏障技术,写屏障分为写前屏障和写后屏障。
SATB基于写前屏障,增量更新则基于写后屏障。例如,在扫描初期,a.e = e1,此时e1被标记为不可回收,因为它被对象a引用了。在并发标记阶段,应用线程执行a.e = e2;执行这一句时会触发写屏障。写前屏障在读取e2对象的引用时记录下该引用到一个队列里,而写后屏障则记录a对象的引用。因此,这种方式可以将并发标记过程中出现的变化记录下来。
接着进入再次标记过程,这次标记实际上是重新扫描并发标记过程中记录下来的对象,因此也需要STW。由于扫描的对象较少,所以STW时间非常短。最后一步是垃圾回收,这就是整个G1垃圾回收的大致过程。
至于为什么叫原始快照,其实原始快照并不是真实存在的数据结构,而是一个概念。从初始标记到并发标记,中间发生变化的对象会保持原貌被记录下来,这样相当于实现了一个快照的作用。
这是你提供的内容,经过调整并加红关键词的版本:
其实G1有一个很大的优势,这一点不同于其他垃圾回收器,就是对堆内存的管理。其他垃圾回收器虽然也将堆内存划分为新生代(Eden和两个Survivor区)以及老年代(Old),但它们在一开始就指定了这些区域的比例。而G1则是根据Region管理的,每个Region具体属于哪个代(新生代或老年代),是根据程序运行情况和算法动态划分的。
这种动态管理堆内存的方式相较于一开始就固定比例,优势明显。因此,你会发现G1的参数配置都是配置的目标效果,算法会根据你想要的效果来管理堆内存,尽可能达到配置目标效果。
有了以上内容的铺垫我想你基本上了解的大概差不多了,上面只是介绍了大概流程,这样你脑海里大流程通顺了,具体的细节后面进一步补充,比如minorGC和mixGC的时候,上面说垃圾回收扫描堆,其实就是扫描Region,minorGC回收的是新生代,具体其实就是那些被标记为E和S的Region,以及可能存在新生代对象,虽然在新生代,但是并没有被新生代的对象引用,而是被老年代引用,这些对象其实也是要被留下来的,如何不扫描老年代又能发现这些对象呢,要是连老年代一块扫描了,那岂不是FullGC了,就不叫MinorGC了,这就要说一下Rset了,
这篇关于大白话详解G1垃圾回收器的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!