目录
前言
什么是G1 GC?
重要语义讲解
标记过程
注意事项
适用场景分析
应用建议
问题
常用配置参数
前言
上次我们讲了CMS GC, 这次我们讲解G1 GC;在开始之前我们要思考下我们为什么学G1 GC?学习后有什么好处?
- 成为更好的Java开发工程师,在遇到服务性能问题、GC问题时,能够通过了解到的G1知识快速定位、解决相关问题
- 在面试时GC问题也是常问的知识点,G1GC作为大多数工程师了解不是很多的知识领域,如果稍微深入理解,就能形成更大的领先优势,无论是被面试还是面试别人
- 学习G1中的优化技巧、原理,有机会能够举一反三应用到平时的工作设计中
- 满足自己的好奇心,了解一项事物背后的运行流程。
什么是G1 GC?
官网描述如下:
The Garbage-First (G1) collector is a server-style garbage collector, targeted for multi-processor machines with large memories. It meets garbage collection (GC) pause time goals with a high probability, while achieving high throughput. The G1 garbage collector is fully supported in Oracle JDK 7 update 4 and later releases. The G1 collector is designed for applications that: > * Can operate concurrently with applications threads like the CMS collector. > * Compact free space without lengthy GC induced pause times. > * Need more predictable GC pause durations. > * Do not want to sacrifice a lot of throughput performance. > * Do not require a much larger Java heap.
从官网的描述中,它是专门针对以下应用场景设计的:
像CMS收集器一样,能与应用程序线程并发执行。
整理空闲空间更快。
需要GC停顿时间更好预测。
不希望牺牲大量的吞吐性能。
不需要更大的Java Heap。
G1 的全称是 Garbage-First,意为垃圾优先,哪一块的垃圾最多就优先清理它。
G1 GC 最主要的设计目标是: 将 STW 停顿的时间和分布,变成可预期且可配置的。
G1 GC 堆不再分成年轻代和老年代,而是划分为多个 (通常是 2048个) 小块: Region。
每个小块,可能一会被定义成 Eden 区,,一会被指定为 Survivor区或者Old 区。
启动参数:XX:+UseG1GC -XX:MaxGCPauseMillis=50
重要语义讲解
1.Region(分区)
Java堆被划分为多个相同大小的独立区块(也称为 Region 或分区),每一个区块都是连续的虚拟内存范围。这种使用区块的方式使得 G1 能够跟踪哪些区块填充满了,哪些被部分使用,哪些是空的。
每个区块都可能在不同的时间点用于特定的角色。例如,在堆的初始阶段,一个区块可能处于 Eden Space(伊甸园区),用于存储新创建的对象。当进行垃圾收集时,任何仍然存活的对象会被移动到另一个区块中,原先的区块会被清空,可以被当做 Survivor Space(幸存者区)或 Old Generation(老年代)。这种方式使得垃圾收集可以在后台进行,并且只影响堆中的一部分区块,而不是整个堆。
根据不同的角色,区块在G1 GC中可被划分为以下几种类型:
- Eden Space:存储新创建的对象。
- Survivor Space:存储从Eden Space经过一次 Minor GC 后仍然存活的对象。
- Old Generation:存储长时间存活的对象。包含了Survivor到达一定年龄后转入的对象。
- Humongous:用于存储大对象,大对象是指需要连续的空间来存储的大小超过Region一半的对象。
G1 GC 通过精心管理这些区块去提供尽可能地挤出最多的可用空间并且尽量地使STW(Stop-The-World)停顿时间降到最低。G1垃圾收集器会跟踪每个区块中的垃圾数量,并且在垃圾回收时,优先回收含有最多垃圾的区块,这也是 Garbage-First 名称的由来。
2.Card(卡片)
在Garbage-First(G1)收集器中,“卡片(Card)”和“卡表(Card Table)”是一种被用于优化并发标记阶段和维护跨代引用的重要机制。
在Java堆中,存储了实际的Java对象数据。当应用线程修改了对象引用,如某个对象的字段从指向一个老年代的对象改为指向另一个年轻代的对象时(或者反过来),我们称之为发生了写操作或者是产生了跨代引用。为了追踪这些跨代引用的变化,G1采用了“卡片标记”和“卡表”机制。
Java堆被划分成连续的、固定大小的块,每个块称为“卡片”。默认情况下,每个卡片大小为512字节,这个值可以通过-XX:G1HeapRegionSize=N 进行调整。
对应于这些卡片,G1维护了一个“卡表”数据结构。每个卡表条目都对应一个卡片,并存储一个标记位,用于记录这个卡片是否包含了跨代引用。
当应用线程产生写操作时,使用“写屏障(write barrier)”机制,将对应的卡表条目标记为“脏(dirty)”,表示这个卡片可能包含了新的跨代引用。
在并发标记阶段,G1 GC不需要扫描整个堆空间,而是可以通过扫描卡表中的脏卡片,来查找和更新所有的跨代引用。这种方式大大提高了标记效率,减小了并发标记阶段带来的性能开销。
因此,“卡片”和“卡表”机制在G1 GC中是非常关键的,它们在保证并发标记准确性和提高垃圾收集性能方面起着至关重要的作用。
3.CSet(待回收Region集合)
CSet(Collection Set)或简称为CSet是指即将要被回收的一组区块(或称为Region)的集合。
在G1 GC的设计中,回收的操作并非在整个Java堆中进行,而是选定一部分包含大量垃圾的Region加入到CSet中,然后针对CSet进行垃圾回收。一个CSet可以包含来自新生代(如Eden区或Survivor区)和老年代的Region,当回收这些Region时,JVM将暂停应用线程,因此这个过程也被称为暂停阶段(Pause Phase)。
选择哪些Region加入到CSet中通过一个称为"预测模型"(Predictive Model)的模块控制。这个模块会记录下过去每次垃圾回收的信息,包括每个Region的对象存活率、单次回收的时间等等,还会考虑-XX:MaxGCPauseMillis参数制定的期待的最大停顿时间,通过这些数据预测回收哪些Region可以在期待的时间内获得最大的空间回收效果。
需要注意的是,CSet的选择需要在每次垃圾回收之前进行,因此,它主要基于上一次垃圾回收结束后堆的状态进行预测。也就是说,CSet并不总是包含最多垃圾的Region,而是“预期在给定的时间内,可以被清理并提供最大可用空间的Region集合”。
因此,CSet是G1垃圾回收中非常关键的一个概念,通过动态调整CSet中的Region,G1可以做到在有限的时间内最大限度地回收垃圾,提供更多的可用空间,从而达到优化性能的效果。
4.RSet(引用索引集合)
G1 GC中的Remembered Set,简称RSet,是为了解决跨Region引用问题而引入的一种数据结构。
一个Heap Region内可能存在指向其他Region的引用,这就构成了跨Region引用。当进行某个Region的垃圾回收时,如果要找到所有引用了此Region中对象的其他对象,按常规思维就需要扫描整个堆区,这会消耗大量时间,效率极低。
为了解决这个问题,G1引入了RSet,每个Region有对应的RSet。当Region A中的对象objA引用了Region B中的对象objB时,就会在Region B的RSet中记录这个引用关系。
记录的是引用的来源,而不是引用的目的地,举个例子,如果Region B的RSet中有一条记录指向Region A,那就说明Region A中有对象引用了Region B中的对象。
在进行某个Region的回收时,只需要去查找它的RSet,就可以找到所有引用了这个Region中对象的其他对象,无需扫描整个堆区,大大提高了效率。
RSet的维护需要依赖于卡表(Card Table),当程序执行过程中新的跨代引用关系被创建时,G1 GC采用写屏障(Write Barrier)技术,将对应的卡片标记为 "dirty",然后后台的垃圾回收线程会定期处理这些"dirty"的卡片,并相应地更新RSet。
因此,RSet和卡表一起,解决了G1跨Region引用的问题,使得G1在进行垃圾回收时无需对整个堆进行扫描,提高了处理效率。
5.SATB(snapshot-at-the-beginning)
SATB(Snapshot-At-The-Beginning),即“快照在开始”,是一种并发标记算法,被用于G1和Shenandoah等GC中。
Java的垃圾收集器在标记阶段需要确定哪些对象是存活的。在并发标记期间,由于应用线程还在运行,所以对象的引用可能会发生变动,这足以导致在标记过程中出现一些问题。比如,如果一个对象从根可达变得不可达,那它就可能会被漏标,因而在最后被错误地回收了。
SATB就是为了解决这个问题,SATB是通过在并发标记开始时做一个快照,该快照阐明了在并发标记(并行GC线程和应用线程)开始时所有存活对象的状态。并发标记过程会标记出在快照中可达的对象以及在并发标记过程中新创建的对象。这就确保了,任何在并发标记开始时可达的对象,不会被漏标。
当对象引用发生变动时,记住初始集(Remember Set)会记录该引用,写屏障(Write Barrier)会在应用线程修改引用关系时工作。如果一个对象在初始快照中是可达的,即使后来变不可达,应用线程在试图使这个对象不可触及(比如把引用它的变量设置成null)时,写屏障将这个对象添加到一个称为SATB队列的结构中,标志线程会从SATB队列中取出对象并把它标记出来。
因此,通过保护并发标记开始时的状态,并监控并记录并发标记期间的状态变化,SATB能够有效防止应用线程的写操作干扰垃圾标记的过程,准确找到堆内存中所有存活的对象。
6.Marking bitmap(位图)
Marking Bitmap 或者称之为标记矩阵,是 Java 虚拟机 (JVM) 中用于管理哪些内存区块已经分配给了哪些对象的数据结构。每个字节都表示一个内存单元,而每个内存单元上都打上了一个标记,表明它是否被分配给了对象。
当一个对象被实例化时,它的起始内存空间将被标记为 “已分配”;当一个对象被销毁时,相应的内存空间将被标记为 “未分配”。这样,我们就可以根据标记矩阵知道哪些内存区块没有被分配,可以重新分配给下一个正在运行的对象。
7.TAMS(写入屏障)
TAMS (Thread Local Allocation Buffer Top-at-Mark-Start) 是G1垃圾收集器的一个重要机制之一。它是对堆中所有已分配对象的一个标记,用于在进行垃圾收集的时候,快速地确定哪些对象是"新对象"。
在G1中,堆被分成若干个小的区域(Region),而GC的过程一次也只处理一个或一部分Region,这时候发生了并发标记,就需要TAMS来进行标记,帮助区分标记周期开始之前和之后分配的对象。
在垃圾收集开始时,TAMS设置为所有Region的top值,对于已分配对象,它的top值小于或等于TAMS,在回收期间,如果有新的对象被分配,则这些新对象的地址会大于TAMS,这样就能清晰地标记出哪些对象是新分配的。
写屏障(Write Barrier)是TAMS的一种实现方式,当一个对象写入一个引用字段时,JVM会检查引用的对象地址是否大于TAMS,进而决定是否要把相应的卡表条目标记为脏。
简而言之,TAMS主要用于G1 GC过程中,区分新旧对象,通过写入屏障以及卡表协同工作,能够更高效的进行垃圾收集。
上面的这些可以了解下,最好化为自己的理解,不要死记硬背。
标记过程
- 初始标记(Initial Mark):这是一次STW事件,对存活的对象做一个初始的标记,并在初始标记完成后,立刻开启并发标记阶段。在这个阶段,G1会标记从GC根对象直接可达的对象。
- 并发标记(Concurrent Marking):这是G1开始并发标记的阶段,由于是和用户线程同时进行,所以还没有对新生代进行回收。这个阶段标记出所有活跃的对象。
- 最终标记(Final Marking):这也是一次STW事件。G1会处理在并发标记期间,新生代对象引用老年代对象产生的SATB记录集合(计数在阶段2的记录),并进行必要的处理。
- 筛选回收(Evacuation):最后又是一次STW事件,G1回收算法会分析哪一块区域的垃圾多,那么优先回收这块区域。这个阶段会根据阶段3的记忆集合的记录复制(移动对象,减少碎片)object到其他区域,然后释放被回收的Region(可直接回收)。
- 并发重置 (Concurrent Reset):并发重置是G1获取下一次并发标记所需要的数据。在重置阶段,G1会重置用于下一次并发标记的内部状态,清理自由区,以及对诸如记忆集等数据结构进行必要的初始化。这个阶段是在停顿后,并发执行的。
G1垃圾收集器的目标是提供可预测的停顿时间,并且避免整堆的垃圾收集。而不像其他收集器可能在老年代满时才进行完整垃圾收集。而在G1中,它平均在整个Java堆上分布了垃圾收集的工作负载,并且能够仅回收一部分区域来最大程度地回收Java堆,而不需要一次性回收整个Java堆。
注意事项
特别需要注意的是,某些情况下 G1 触发了 Ful GC,这时 G1 会退化使用 Serial 收集器来完成垃圾的清理工作,它仅仅使用单线程来完成 GC 工作,GC 暂停时间将达到秒级别的。
1.并发模式失败
G1 启动标记周期,但在 Mix GC 之前,老年代就被填满,这时候 G1 会放弃标记周期解决办法: 增加堆大小,或者调整周期 (例如增加线程数-XX:ConcGCThreads 等)
2.晋升失败
没有足够的内存供存活对象或晋升对象使用,由此触发了 Full GC(to-space exhausted/to-space overflow)
解决办法:
1.增加-XX:G1ReservePercent 选项的值 (并相应增加总的堆大小)增加预留内存量
2.通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。
3.也可以通过增加 -XX:ConGCThreads 选项的值来增加并行标记线程的数目
3.巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动 Full GC,来释放空间解决办法: 增加内存或者增大 -XX:G1HeapRegionSize
适用场景分析
基于其基本的优势,如并行与并发兼备、分代收集、空间整合、可预测的停顿时间模型(即:软实时soft real-time)等优势,其主要适用场景如下:
面向服务端应用,针对具有大内存、多处理器的机器(在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒),在普通大小的堆里表现并不惊喜。
最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;
用来替换掉JDK1.5中的CMS收集器,在下面的情况时,使用G1可能比CMS好:
超过50%的Java堆被活动数据占用;
对象分配频率或年代提升频率变化很大;
GC停顿时间过长(长于0.5至1秒)
HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器均使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。
应用建议
介绍一些关于 G1 垃圾回收器优化的一般性建议:
1.调整区域大小:在使用 G1 垃圾回收器时,可以根据应用内存大小和分配情况调整各个区域的大小。例如,可以增加年轻代的大小,以减少对象晋升老年代的频率,同时调整老年代的大小,以充分利用多线程并发处理垃圾回收。
2.调整并发线程数:在进行 G1 垃圾回收时,可以根据机器配置和应用负载情况调整并发垃圾回收线程数。例如,可以增加并发垃圾回收线程数以加速垃圾回收速度和提高吞吐量。
3.设置目标停顿时间:在进行 G1 垃圾回收时,可以通过设置目标停顿时间来平衡垃圾回收速度和响应时间。例如,可以设置较短的目标停顿时间来保证应用的响应速度,但这可能会导致垃圾回收的效率降低。
4.分析 GC 日志:在进行 G1 垃圾回收优化时,可以通过分析 GC 日志来了解垃圾回收的实际情况,从而进行针对性的调优。例如,可以根据 GC 日志来确定哪些对象占用了大量内存,从而进行内存泄漏的排查和解决。
需要注意的是,G1 垃圾回收器的优化需要根据具体的应用场景和需求进行,不能一概而论。在进行 G1 垃圾回收器的优化时,需要结合实际情况进行参数调整和性能监控,以达到最优的性能和稳定性表现。
问题
1.Region(分区)的使用解决了什么问题,有什么好处?为什么这么使用?
在传统的垃圾收集器中,如Parallel Collector和CMS收集器,Java堆被划分为两个或三个固定的区域:新生代(Young Generation)、老年代(Old Generation)和持久代(PermGen,Java 8 后已被 Metaspace 取代)。这种方法的问题是,当某一代的空间用尽时,需要进行一次完整的垃圾收集,这会造成较长时间的应用暂停,也就是通常所说的"Stop-The-World"(STW)事件。
G1 GC通过划分区块来解决这个问题。Java堆被切分为多个独立的、大小相等的区块(Region)。各个区块可以独立使用,根据程序的实际需求变化它的角色,如新生代、老年代等。
这种做法的优势在于:
- 精准控制暂停时间:G1能够预测哪些区块的回收能在最短的时间内回收最多的空间,从而在有限的时间内完成更高效的回收。
- 避免空间浪费:最小化空间浪费是G1设计的一个主要目标。因为每个区块都能充当任何角色,所以不存在浪费空间的问题。
- 避免完全垃圾收集(Full GC):在G1中,Full GC将会非常罕见,因为它可以持续地清理垃圾,避免堆被填满。
- 更好的利用硬件资源:通过并行和并发的方式,G1可以同时使用多个CPU核心进行垃圾回收,使得垃圾回收更加高效。
总的来说,G1 GC通过region的方式,提供了一种更为灵活、高效的内存管理方式,可以预测和精确控制GC暂停时间,提高应用的性能和可预测性,特别适合需要大内存和低延迟的应用。
常用配置参数
课程链接:阿里云盘分享
今天就到这里吧,感觉有用的小伙伴可以点个赞,你的支持就是我更新的最大动力!
参考文档:
Java Hotspot G1 GC的理解总结_g1gc_张彦峰ZYF的博客-CSDN博客
【Java】G1 GC_51CTO博客_java gc