概述
G1 垃圾回收器在 Java 7 update 4 后引入。它是分代、增量、并行与并发的标记 - 复制回收器,旨在适应内存扩大、处理器增多的情况,降低暂停时间,兼顾吞吐量。
与 CMS 相比,G1 有这些不同:
- 内存连续性:G1 是 compacting 的,回收空间连续,避免了 CMS 因空间不连续导致的问题,如需要更大堆空间、产生更多浮动垃圾。连续空间让 G1 能用 bump - the - pointer 方式分配内存,而非空闲链表方式。
- 内存模型:G1 内存模型和 CMS 差异大。G1 把内存划分为固定大小的 region,每个 region 可为年轻代或老年代,以 region 为单位回收内存。
- 软实时特性:G1 具备软实时特性。用户可设垃圾回收限时,G1 会尽力在该时限内完成,但不保证每次都能做到。合理设定目标,能让 90% 以上的回收时间在时限内 。
分区内存划分
- 分区 Region:G1 把整个堆空间划分成许多大小相同的内存区域(Region),分配对象空间时逐段使用内存。在堆的使用上,G1 只要求对象逻辑上连续,不要求物理连续。而且每个分区并非固定服务于某一代,能按需在年轻代和老年代间切换。启动时可通过参数 - XX:G1HeapRegionSize=n 指定分区大小(范围 1MB~32MB,且必须是 2 的幂),默认整堆有 2048 个分区。
- 卡片 Card:每个分区内又划分成若干 512 Byte 的卡片。这些卡片是堆内存最小可用粒度,所有分区的卡片记录在全局卡片表中。分配的对象会占若干连续卡片,查找分区内对象引用时,可通过记录的卡片来定位(与 RSet 相关)。内存回收时,处理的就是指定分区的卡片。
- 堆 Heap:G1 可用 - Xms/-Xmx 指定堆空间大小。年轻代收集或混合收集时,会依据 GC 与应用的耗时比自动调整堆大小。若 GC 频繁,就增加堆尺寸来降低频率和 GC 占用时间。目标参数 - XX:GCTimeRatio 是 GC 与应用的耗时比,G1 默认值为 9,CMS 默认值为 99(因 CMS 希望 GC 耗时尽可能少)。当空间不足,如对象分配或转移失败,G1 先尝试增加堆空间,若扩容失败则触发 Full GC。Full GC 后也会根据堆尺寸计算结果调整堆空间。
分代内存划分
分代垃圾收集聚焦最近分配的对象,无需扫描整个堆,避免复制长命对象,还能降低响应时间。尽管 G1 分区后内存分配无需紧凑空间,但仍采用分代思想。
和其他垃圾收集器一样,G1 在逻辑上把内存分为年轻代和老年代,年轻代又包含 Eden 空间和 Survivor 空间。不过,年轻代空间并非固定的,现有年轻代分区满时,JVM 会添加新的空闲分区。
年轻代内存会在初始空间(-XX:G1NewSizePercent,默认是整堆的 5%)和最大空间(默认 60%)之间动态变化,由目标暂停时间(-XX:MaxGCPauseMillis,默认 200ms)、扩缩容大小(-XX:G1MaxNewSizePercent)及分区的已记忆集合(RSet)计算得出。
当然,也能通过参数(-XX:NewRatio、-Xmn)设置固定的年轻代大小,但这样暂停目标就失去意义了。
Rset
Remember Set(RSet)即记忆集,是现代垃圾回收算法中的一个重要概念,尤其在分代式和分区式垃圾回收器(如 G1、CMS 等)里发挥着关键作用。下面从其定义、作用、实现方式以及使用场景等方面详细解释如何理解 RSet。
定义
RSet 是一种用于记录跨代或跨区域对象引用关系的数据结构。在 Java 堆中,对象之间存在着各种引用关系,而垃圾回收器在回收某个区域(如新生代、老年代或某个 Region)的对象时,需要知道其他区域的对象是否引用了该区域内的对象。RSet 就是用来记录这些跨区域引用信息的。
作用
减少垃圾回收时的扫描范围
在分代式垃圾回收中,不同代的对象生命周期不同,垃圾回收的频率和方式也不同。例如,新生代的垃圾回收较为频繁,而老年代的垃圾回收相对较少。如果没有 RSet,在进行新生代垃圾回收时,需要扫描整个老年代来确定哪些老年代对象引用了新生代对象,这会带来很大的性能开销。通过使用 RSet,垃圾回收器只需要扫描 RSet 中记录的引用信息,就可以快速定位到跨代引用的对象,从而减少了扫描的范围,提高了垃圾回收的效率。
支持并发和并行垃圾回收
在并发和并行垃圾回收过程中,RSet 可以帮助垃圾回收器在不停止应用程序线程的情况下,准确地识别出哪些对象是存活的。因为 RSet 记录了跨区域的引用信息,垃圾回收器可以根据这些信息来判断对象是否被其他区域的对象引用,从而决定是否将其标记为存活对象。
实现方式
RSet 通常是基于卡表(Card Table)来实现的。卡表是一种字节数组,每个字节对应着堆内存中的一块固定大小的区域,这个区域被称为一个卡页(Card Page),通常大小为 512 字节。当一个对象的引用发生变化时,虚拟机就会将该对象所在的卡页标记为 “脏卡”(Dirty Card)。
RSet 会记录每个区域的卡表信息,当进行垃圾回收时,垃圾回收器会扫描 RSet 中记录的脏卡,从而定位到跨区域引用的对象。例如,在 G1 垃圾回收器中,每个 Region 都有一个对应的 RSet,RSet 记录了其他 Region 中的对象对本 Region 中对象的引用。
使用场景示例
以下是一个简单的示例,展示了 RSet 在 G1 垃圾回收器中的使用场景:
假设 Java 堆被划分为多个 Region,其中 Region 2和Region 3 是新生代,Region 1 是老年代。当 Region 1 中的一个对象引用了 Region 3 中的一个对象时,虚拟机将 Region 3 中该对象所在的卡页标记为脏卡,并将该信息记录到 Region 3 的 RSet 中。
当进行 Region 3 的垃圾回收时,垃圾回收器会扫描 Region 3 的 RSet,找到所有标记为脏卡的区域,然后检查这些区域中的对象是否引用了 Region 1 中的对象。如果有引用,则将被引用的对象标记为存活对象,避免被回收。
总结
RSet 是现代垃圾回收算法中用于记录跨代或跨区域对象引用关系的数据结构,它的主要作用是减少垃圾回收时的扫描范围,提高垃圾回收的效率,同时支持并发和并行垃圾回收。通过基于卡表的实现方式,RSet 可以高效地记录和管理跨区域的引用信息,为垃圾回收器提供准确的对象引用关系。
并发标记算法(三色标记法)
并发标记算法中的三色标记法是一种用于垃圾回收的标记算法,在并发标记过程中,它会出现漏标问题,下面为你详细解释该问题。
三色标记法概述
三色标记法将对象分为三种颜色:
- 白色:表示对象尚未被垃圾回收器访问过,在标记开始阶段,所有对象都是白色。当标记结束后,仍然是白色的对象意味着不可达,会被当作垃圾回收。
- 灰色:表示对象已经被垃圾回收器访问过,但它引用的对象中还有未被访问的。即该对象本身被标记,但它的部分引用链还未完成标记。
- 黑色:表示对象已经被垃圾回收器访问过,并且它引用的所有对象也都被访问过了。黑色对象不会再被重新扫描。
漏标问题的产生
在并发标记过程中,垃圾回收器线程和应用程序线程是同时运行的。应用程序线程可能会在标记过程中修改对象之间的引用关系,这就可能导致漏标问题。具体来说,漏标问题的产生需要同时满足以下两个条件:
- 插入了一条或多条从黑色对象到白色对象的新引用:在标记过程中,如果一个黑色对象新引用了一个白色对象,而垃圾回收器已经完成了对该黑色对象的标记,不会再重新扫描它,那么这个白色对象就可能不会被标记到。
- 删除了全部从灰色对象到该白色对象的直接或间接引用:如果灰色对象原本引用了某个白色对象,在标记过程中应用程序线程删除了这个引用,且没有其他灰色对象引用该白色对象,那么这个白色对象就不会被后续的标记过程访问到。
示例说明
假设存在对象 A、B、C,初始状态下:
- A 是黑色对象(已完成标记)。
- B 是灰色对象(正在标记其引用的对象)。
- C 是白色对象(未被访问)。
- 原本的引用关系是 B -> C。
在并发标记过程中,应用程序线程执行了以下操作:
- 插入了 A -> C 的引用。
- 删除了 B -> C 的引用。
此时,由于 A 是黑色对象不会被重新扫描,C 失去了来自灰色对象 B 的引用,就不会被标记到,最终会被错误地当作垃圾回收,这就是漏标问题。
解决漏标问题的方法
增量更新(Incremental Update)
当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发标记结束之后,再以这些记录中的黑色对象为根,重新进行标记。CMS(Concurrent Mark Sweep)垃圾回收器采用的就是增量更新的方式来解决漏标问题。
比如将A和B记录下来,随后进行重新标记
原始快照(Snapshot At The Beginning,SATB)
当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发标记结束之后,再以这些记录中的灰色对象为根,重新进行标记。G1(Garbage First)垃圾回收器使用的是原始快照的方式来处理漏标问题。
总结
漏标问题是并发标记算法(三色标记法)在并发环境下由于应用程序线程和垃圾回收线程同时操作对象引用而产生的问题。为了避免漏标导致的存活对象被错误回收,需要采用增量更新或原始快照等方法来进行修正,以保证垃圾回收的正确性。
为什么G1采用SATB而不用incremental update?
因为采用incremental update把黑色重新标记为灰色后,之前扫描过的还要再扫描一遍,效率太低。G1有RSet与SATB相配合。Card Table里记录了RSet,RSet里记录了其他对象指向自己的引用,这样就不需要再扫描其他区域,只要扫描RSet就可以了。
也就是说 灰色B–>白色C 引用消失时,如果没有 黑色A–>白色C,引用会被push到堆栈,下次扫描时拿到这个引用,由于有RSet的存在,不需要扫描整个堆去查找指向白色的引用,效率比较高。SATB配合RSet浑然天成。
Rset的维护
Remembered Set(RSet)在垃圾回收机制里起着关键作用,它主要用于记录跨代或跨区域的对象引用信息,以此减少垃圾回收时的扫描范围,提升回收效率。下面详细介绍 RSet 的维护方式。
卡表(Card Table)
RSet 通常基于卡表来实现和维护,卡表是一个字节数组,其中每个字节对应着堆内存中的一块固定大小的区域,这个区域被称作一个卡页(Card Page),一般大小为 512 字节。卡表的每个元素可以理解为一个标志位,用于标记对应卡页的状态。
写屏障(Write Barrier)
写屏障是维护 RSet 的核心机制,它可以看作是在对象引用赋值操作前后插入的一段额外代码,主要有两种类型:写前屏障和写后屏障。写屏障会在对象引用发生改变时触发,从而更新 RSet 的信息。
写前屏障(Pre - write Barrier)
写前屏障在引用赋值操作之前执行,主要用于实现原始快照(Snapshot At The Beginning,SATB)算法。当一个对象的引用要被修改(删除引用)时,写前屏障会将这个要删除的引用记录下来,以便后续重新标记。G1 垃圾回收器使用写前屏障来维护 RSet,防止并发标记时的漏标问题。以下是一个简化的伪代码示例:
java">void pre_write_barrier(Object field, Object new_value) {if (field != null) {// 记录即将删除的引用record_reference_deletion(field);}// 执行引用赋值操作field = new_value;
}
写后屏障(Post - write Barrier)
写后屏障在引用赋值操作之后执行,主要用于实现增量更新(Incremental Update)算法。当一个对象的引用被修改(插入新引用)时,写后屏障会将这个新插入的引用记录下来,后续以这些记录中的对象为根重新进行标记。CMS(Concurrent Mark Sweep)垃圾回收器使用写后屏障来维护 卡表。以下是一个简化的伪代码示例:
java">void post_write_barrier(Object field, Object new_value) {// 执行引用赋值操作field = new_value;if (new_value != null) {// 记录新插入的引用record_reference_insertion(field, new_value);}
}
维护流程
下面详细阐述 RSet 的维护流程:
- 引用修改触发写屏障:当应用程序线程对对象的引用进行修改(插入或删除引用)时,会触发写屏障。
- 判断引用是否跨代或跨区域:写屏障会检查引用的修改是否涉及跨代或跨区域的情况。如果是,则需要更新 RSet。
- 标记脏卡:若引用修改涉及跨代或跨区域,写屏障会将引用所在的卡页标记为 “脏卡”(Dirty Card)。
- 更新 RSet:垃圾回收器在适当的时候(如在并发标记阶段)会扫描所有的脏卡,根据脏卡的信息更新对应的 RSet,记录跨代或跨区域的引用关系。
定期清理和优化
为了避免 RSet 占用过多的内存,垃圾回收器会定期对 RSet 进行清理和优化。例如,当一个 Region 被回收后,与之相关的 RSet 信息也会被清除;同时,垃圾回收器还会对 RSet 进行压缩和合并操作,以减少内存占用。
综上所述,RSet 的维护主要依赖于卡表和写屏障机制。写屏障在对象引用修改时触发,通过标记脏卡和更新 RSet 来记录跨代或跨区域的引用信息,同时垃圾回收器会定期对 RSet 进行清理和优化,以保证其高效性和准确性。
垃圾回收全过程
G1(Garbage-First)垃圾回收器是一款面向服务器端应用的垃圾回收器,旨在满足大内存、多处理器机器的需求,在实现高吞吐量的同时尽可能降低垃圾回收的停顿时间。下面详细阐述 G1 垃圾回收的全过程。
初始标记(Initial Mark)
- 触发时机:当堆内存的使用达到一定阈值(由 -XX:InitiatingHeapOccupancyPercent 参数控制,默认是 45%)时,会触发 G1 垃圾回收的初始标记阶段。
- 操作内容:该阶段会暂停所有应用程序线程(Stop The World,STW),标记出所有的根对象(Root Object)直接引用的对象。根对象包括虚拟机栈中引用的对象、方法区中类静态属性引用的对象、常量引用的对象等。这个阶段的停顿时间相对较短,因为只需要标记根对象直接引用的对象。
并发标记(Concurrent Marking)
- 触发时机:初始标记完成后,进入并发标记阶段。
- 操作内容:此阶段垃圾回收线程与应用程序线程并发执行。垃圾回收器会从初始标记阶段标记的对象开始,遍历整个堆,标记出所有存活的对象。在这个过程中,应用程序线程可以继续创建新对象、修改对象引用等。为了处理并发过程中对象引用的变化,G1 使用了写屏障(Write Barrier)技术,如原始快照(SATB),来记录对象引用的变化,避免漏标问题。
最终标记(Final Marking)
- 触发时机:并发标记阶段完成后,进入最终标记阶段。
- 操作内容:该阶段会再次暂停所有应用程序线程(STW)。由于并发标记阶段应用程序线程可能会修改对象引用关系,导致部分标记信息不准确,所以在这个阶段需要处理这些并发标记过程中记录的引用变化。通过处理写屏障记录的信息,完成对所有存活对象的标记。这个阶段的停顿时间通常比初始标记阶段长一些,但比传统的标记 - 清除算法的标记阶段停顿时间要短。
筛选回收(Cleanup)
- 触发时机:最终标记完成后,进入筛选回收阶段。
- 操作内容:
- 统计信息:首先,G1 会统计各个 Region 的回收价值和成本,包括每个 Region 中存活对象的数量、回收所需的时间等。
- 选择回收区域:根据统计信息,G1 会选择一些回收价值高、成本低的 Region 进行回收。这些 Region 可以是新生代 Region,也可以是老年代 Region。
- 回收操作:对于选中的 Region,G1 会采用复制算法将存活的对象复制到其他空闲的 Region 中,然后将原来的 Region 清空并加入空闲 Region 列表。这个过程同样会暂停所有应用程序线程(STW),但由于只回收部分 Region,所以停顿时间可以得到有效控制。
总结
G1 垃圾回收器通过将堆内存划分为多个大小相等的 Region,采用标记 - 复制算法,结合并发标记和筛选回收的策略,在实现高吞吐量的同时尽可能降低垃圾回收的停顿时间。整个回收过程分为初始标记、并发标记、最终标记和筛选回收四个阶段,每个阶段都有其特定的任务和操作,通过合理的调度和优化,使得 G1 能够高效地处理大内存应用的垃圾回收问题。