G1垃圾回收器

一、概述

G1(Garbage First)垃圾回收器是在Java7 update 4之后引入的一个新的垃圾回收器。HotSpot为解决CMS算法产生空间碎片和其它一系列的问题缺陷,而提供了另外一种垃圾回收策略。G1是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。

1.1 G1和CMS区别

  1. G1垃圾回收器是compacting的,因此其回收得到的空间是连续的。这避免了CMS回收器因为不连续空间所造成的问题。如需要更大的堆空间,更多的floating garbage。连续空间意味着G1垃圾回收器可以不必采用空闲链表的内存分配方式,而可以直接采用bump-the-pointer的方式;
  2. G1回收器的内存与CMS回收器要求的内存模型有极大的不同。CMS回收器只回收老年代,G1收集器回收年轻代和老年代。G1将内存划分一个个固定大小的region,每个region可以是年轻代、老年代的一个。内存的回收是以region作为基本单位的;
  3. G1还有一个及其重要的特性:软实时(soft real-time)。所谓的实时垃圾回收,是指在要求的时间内完成垃圾回收。“软实时”则是指,用户可以指定垃圾回收时间的限时,G1会努力在这个时限内完成垃圾回收,但是G1并不担保每次都能在这个时限内完成垃圾回收。通过设定一个合理的目标,可以让达到90%以上的垃圾回收时间都在这个时限内。

1.2 使用场景

G1垃圾回收器适合作为服务端垃圾收集器,应用在多处理器和大内存的条件下,可以实现高吞吐量的同时,尽可能满足垃圾收集较短可控的暂停时间,主要针对以下场景设计

  • CMS一样,能与应用程序并发执行
  • 更快速整理空闲空间
  • GC停顿时间更可控
  • 不会牺牲大量吞吐性能
  • 服务端多核CPUJVM内存占用较大的应用(至少大于4G)
  • 应用在运行过程中会产生大量内存碎片、需要经常压缩空间
  • 想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象

二、数据结构

G1垃圾回收器的复杂难懂,有很大一部分原因是因为这些数据结构。

2.1 Heap Region

本质上来说,G1垃圾回收器依然是一个分代垃圾回收器。但是它与一般的回收器所不同的是,它引入了额外的概念:RegionG1垃圾回收器把堆划分成一个个大小相同的Region。在HotSpot的实现中,整个堆默认被划分成2048左右个Region。每个Region的大小在1-32MB之间。

Region的大小必须为2的整数倍,如2MB4MB6MB等,可以通过-XX:G1HeapRegionSize参数手动指定,如果G1HeapRegionSize为默认值,则在堆初始化时计算Region的实践大小。

G1垃圾回收器的分代也是建立在这些Region的基础上的。对于Region来说,它会有一个分代的类型,并且是唯一一个。即每一个Region要么是young的要么是old的。还有一类十分特殊的Humongous。所谓的Humongous,就是一个对象的大小超过了某一个阈值——HotSpot中是Region1/2,那么它会被标记为Humongous。如果我们审视HotSpot的其余的垃圾回收器,可以发现这种对象以前被称为大对象,会被直接分配老年代。而在G1回收器中,则是做了特殊的处理。

G1并不要求相同类型的region要相邻。换言之,就是G1回收器不要求它们连续。当然在逻辑上,分代依旧是连续的。因此,一种典型的分配可能是:

b5f52615c38aa31b.webp

G1 Regions

注:图片来自G1: One Garbage Collector To Rule Them All

其中E代表的是EdenS代表的是SurvivorH代表的是Humongous,剩余的深蓝色代表的是Old(或者Tenured),灰色的代表的是空闲的Region

每一个分配的Region,都可以分成两个部分,已分配的和未被分配的。它们之间的界限被称为top。总体上来说,把一个对象分配到Region内,只需要简单增加top的值。这个做法实际上就是bump-the-pointer。过程如下:

235835bfb8ac.webp

Region内存分配

Region可以说是G1回收器一次回收的最小单元。即每一次回收都是回收NRegion。这个N是多少,主要受到G1回收的效率和用户设置的软实时目标有关。每一次的回收,G1会选择可能回收最多垃圾的Region进行回收。与此同时,G1回收器会维护一个空间Region的链表。每次回收之后的Region都会被加入到这个链表中。

每一次都只有一个Region处于被分配的状态中,被称为current region。在多线程的情况下,这会带来并发的问题。G1回收器采用和CMS一样的TLABs的手段。即为每一个线程分配一个Buffer,线程分配内存就在这个Buffer内分配。但是当线程耗尽了自己的Buffer之后,需要申请新的Buffer。这个时候依然会带来并发的问题。G1回收器采用的是CAS(Compate And Swap)操作。

为线程分配Buffer的过程大概是:

  1. 记录top值;
  2. 准备分配;
  3. 比较记录的top值和现在的top值,如果一样,则执行分配,并且更新top的值;否则,重复1

显然的,采用TLABs的技术,就会带来碎片。举例来说,当一个线程在自己的Buffer里面分配的时候,虽然Buffer里面还有剩余的空间,但是却因为分配的对象过大以至于这些空闲空间无法容纳,此时线程只能去申请新的Buffer,而原来的Buffer中的空闲空间就被浪费了。Buffer的大小和线程数量都会影响这些碎片的多寡。

2.2 记录集合(Remember Set)和卡表(Card Table)

RS(Remember Set)是一种抽象概念,用于记录从非收集部分指向收集部分的指针的集合。

在传统的分代垃圾回收算法里面,RS被用来记录分代之间的指针。在G1回收器里面,RS被用来记录从其他Region指向一个Region的指针情况。因此,一个Region就会有一个RS。这种记录可以带来一个极大的好处:在回收一个Region的时候不需要执行全堆扫描,只需要检查它的RS就可以找到外部引用,而这些引用就是initial mark的根之一。

那么,如果一个线程修改了Region内部的引用,就必须要去通知RS,更改其中的记录。为了达到这种目的,G1回收器引入了一种新的结构,CT(Card Table)——卡表。每一个Region,又被分成了固定大小的若干张卡(Card)。每一张卡,都用一个Byte来记录是否修改过。卡表即这些byte的集合。

从第一感觉,或者出于直觉的考虑,使用一个bit来记录一张卡是否被修改过,就已经足够了。而使用一个byte会造成更多的空间开销。但是实际上,使用一个byte来记录一张卡是否被修改过,会比使用一个bit来记录效率更高。

RS的修改上也会遇到并发的问题。因为一个Region可能有多个线程在并发修改,因此它们也会并发修改RS。为了避免这样一种冲突,G1垃圾回收器进一步把RS划分成了多个哈希表。每一个线程都在各自的哈希表里面修改。最终,从逻辑上来说,RS就是这些哈希表的集合。哈希表是实现RS的一种通常的方式之一。它有一个极大的好处就是能够去除重复。这意味着,RS的大小将和修改的指针数量相当。而在不去重的情况下,RS的数量和写操作的数量相当。

整个关系如下:

e0b8898d895aee05.webp

Remember Set

图中RS的虚线表名的是,RS并不是一个和Card Table独立的,不同的数据结构,而是指RS是一个概念模型。实际上,Card TableRS的一种实现方式。

Remember Set的写屏障

写屏障是指,在改变特定内存的值(实际上也就是写入内存)的时候额外执行的一些动作。在大多数的垃圾回收算法中,都利用到了写屏障。写屏障通常用于在运行时探测并记录回收相关指针(interesting pointer),在回收器只回收堆中部分区域的时候,任何来自该区域外的指针都需要被写屏障捕获,这些指针将会在垃圾回收的时候作为标记开始的根。JAVA使用的其余的分代的垃圾回收器,都有写屏障。举例来说,每一次将一个老年代对象的引用修改为指向年轻代对象,都会被写屏障捕获,并且记录下来。因此在年轻代回收的时候,就可以避免扫描整个老年代来查找根。

G1垃圾回收器的写屏障和RS是相辅相成的,也就是记录Region内部的指针。这种记录发生在写操作之后。对于一个写屏障来说,过滤掉不必要的写操作是十分有必要的。这种过滤既能加快赋值器的速度,也能减轻回收器的负担。G1垃圾回收器采用的双重过滤

  1. 过滤掉同一个Region内部引用;
  2. 过滤掉空引用;

过滤掉这两个部分之后,可以使RS的大小大大减小。

G1的垃圾回收器的写屏障使用一种两级的log buffer结构:

  1. global set of filled buffer:所有线程共享的一个全局的,存放填满了的log buffer的集合;
  2. thread log buffer:每个线程自己的log buffer。所有的线程都会把写屏障的记录先放进去自己的log buffer中,装满了之后,就会把log buffer放到global set of filled buffer中,而后再申请一个log buffer

2.3 收集集合(Collect Set)

Collect Set(CSet):是指在Evacuation阶段,由G1垃圾回收器选择的待回收的Region集合。G1垃圾回收器的软实时的特性就是通过CSet的选择来实现的。对应于算法的两种模式fully-young generational modepartially-young modeCSet的选择可以分成两种:

  1. fully-young generational mode下:顾名思义,该模式下CSet将只包含youngRegionG1将调整youngRegion的数量来匹配软实时的目标;
  2. partially-young mode下:该模式会选择所有的young region,并且选择一部分的old regionold region的选择将依据在Marking cycle phase中对存活对象的计数。G1选择存活对象最少的Region进行回收。

2.4 起始快照算法(snapshot-at-the-beginning)

SATB(snapshot-at-the-beginning),是最开始用于实时垃圾回收器的一种技术。G1垃圾回收器使用该技术在标记阶段记录一个存活对象的快照("logically takes a snapshot of the set of live objects in the heap at the start of marking cycle")。然而在并发标记阶段,应用可能修改了原本的引用,比如删除了一个原本的引用。这就会导致并发标记结束之后的存活对象的快照和SATB不一致。G1是通过在并发标记阶段引入一个写屏障来解决这个问题的:每当存在引用更新的情况,G1会将修改之前的值写入一个log buffer(这个记录会过滤掉原本是空引用的情况),在最终标记(final marking phase)阶段扫描SATB,修正SATB的误差。

SATBlog bufferRS的写屏障使用的log buffer一样,都是两级结构,作用机制也是一样的。

2.5 Marking bitmaps和TAMS

Marking bitmap是一种数据结构,其中的每一个bit代表的是一个可用于分配给对象的起始地址。举例来说:

9d669e21d9f24276.webp

其中addrN代表的是一个对象的起始地址。绿色的块代表的是在该起始地址处的对象是存活对象,而其余白色的块则代表了垃圾对象。

G1使用了两个bitmap,一个叫做previous bitmap,另外一个叫做next bitmapprevious bitmap记录的是上一次的标记阶段完成之后的构造的bitmapnext bitmap则是当前正在标记阶段正在构造的bitmap。在当前标记阶段结束之后,当前标记的next bitmap就变成了下一次标记阶段的previous bitmap

TAMS(top at mark start)变量,是一对用于区分在标记阶段新分配对象的变量,分别被称为previous TAMSnext TAMS。在previous TAMSnext TAMS之间的对象则是本次标记阶段时候新分配的对象。如图:

9a678ca5d37351d4.webp

白色Region代表的是空闲空间,绿色Region代表是存活对象,橙色Region代表的在此次标记阶段新分配的对象。注意的是,在橙色区域的对象,并不能确保它们都事实上是存活的。

三、算法详解

整个算法可以分成两大部分:

  1. 循环标记阶段(Marking cycle phase):也有叫全局并发标记(global concurrent marking),该阶段是不断循环进行的;
  2. 拷贝存活对象(Evacuation phase,STW):或者叫迁移,该阶段是负责把一部分Region的活对象拷贝到空Region里面去,然后回收原本的Region空间,该阶段是STW的;

这两部分可以相对独立执行。

而算法也可以分成两种模式:

  1. 完全年轻代GC(fully-young generational mode):有时候也会被称为young GC,该模式只会回收young region,算法是通过调整young region的数量来达到软实时目标的;
  2. 部分年轻代GC(partially-young mode):也被称为Mixed GC(混合回收),该阶段会回收young regionold region,算法通过调整old region的数量来达到软实时目标;

有趣的地方是不论处在何种模式之下,young region都在被回收的范围内。而old region只能期望于Mixed GC。但是,如同在CMS垃圾回收器中遇到的困境一样,Mixed GC可能来不及回收old region。也就说,在需要分配老年代的对象的时候,并没有足够的空间。这个时候就只能触发一次full GC

算法会自动在young GCMixed GC之间切换,并且定期触发循环标记阶段HotSpotG1实现允许指定一个参数InitiatingHeapOccupancyPercent,在达到该参数的情况下,就会执行循环标记阶段

算法并不使用在对象头增加字段来标记该对象,而是采用bitmap的方式来记录一个对象被标记的情况。这种记录方法的好处就是在使用这些标记信息的时候,仅仅需要扫描bitmap而已。G1统计一个region的存活的对象,就是依赖于bitmap的标记。

3.1 循环标记阶段(Marking cycle)

算法的循环标记阶段大概可以分成五个阶段:

20210123132941254.png

  1. 初始标记(Initial marking phaseSTW):G1收集器扫描所有的根。该过程是和young GC的暂停过程一起的;
  2. 根区域扫描(Root region scanning phase):扫描Survivor Regions中指向老年代的被初始标记标记的引用及引用的对象,这一个过程是并发进行的。但是该过程要在下一个young GC开始之前结束;
  3. 并发标记(Concurrent marking phase):并发标记阶段,标记整个堆的存活对象。该过程可以被young GC所打断。并发阶段产生的新的引用(或者引用的更新)会被SATBwrite barrier记录下来;
  4. 最终标记(Remark phaseSTW):也叫final marking phase。该阶段只需要扫描SATB(Snapshot At The Beginning)的buffer,处理在并发阶段产生的新的存活对象的引用。作为对比,CMSremark需要扫描整个mod union table的标记为dirtyentry以及全部根;
  5. 筛选回收(Cleanup phaseSTW):该阶段会计算每一个region里面存活的对象,并把完全没有存活对象的Region直接放到空闲列表中。在该阶段还会重置Remember Set。该阶段在计算Region中存活对象的时候,是STW(Stop-the-world)的,而在重置Remember Set的时候,却是可以并行的;

3.1.1 初始标记(Initial Marking Phase)

109fa18fee459f3b0f2146e7367b25f2.png

该阶段扫描所有的根,与CMS类似。所不同的是,该阶段是和young GC一起的。这里的young GC实际上是指的就是fully-young generational mode

Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Guide的原文是"This phase is piggybacked on a normal (STW) young garbage collection"。

3.1.2 根区域扫描(Root region scanning phase)

该过程主要是扫描Survivor region中指向老年代的,在initial mark phase标记的引用及其引用的对象。这是一个很奇怪的步骤,因为在前面不论是Parallel Collector还是CMS,都没有这么一个步骤。

要理解这一点,要注意的是,算法的两种模式,不论是young GC还是Mixed GC,都需要回收young region。因为实际上RS是不记录从young region出发的指针,例如,这部分指针包括young region - young region,也包括young region - old region指针。那么就可能出现一种情况,一个老年代的存活对象,只被年轻代的对象引用。在一次young GC中,这些存活的年轻代的对象会被复制到Survivor Region,因此需要扫描这些Survivor region来查找这些指向老年代的对象的引用,作为并发标记阶段扫描老年代的根的一部分。

在理解了这一点的基础上,那么对于阶段必须在下一次young GC启动前完成的要求,也就理解了。因为如果第二次的young GC启动了,那么这个过程中,survivor region就可能发生变化。这个时候执行root region phase就会产生错误的结果。

3.1.3 并发标记(Concurrent marking phase)

8c1d46981fa5af3bf4a663a3751ad38e.png

在标记阶段,会使用到一个marking stack的东西。G1不断从marking stack中取出引用,递归扫描整个堆里的对象图,并且在bitmap上进行标记。这个递归过程采用的是深度遍历,会不断把对象的域入栈。

在并发标记阶段,因为应用还在运行,所以可能会有引用变更,包括现有引用指向别的对象,或者删除了一个引用,或者创建了一个新的对象等。G1采用的是使用SATB的并发标记算法。

使用SATB的两条原则:

  1. All accessible cells at the beginning of the garbage collection are eventually marked during the marked phase;
  2. Newly alocated cells during the garbage collection are never collected during the sweep phase of that garbage collection

G1中,该算法的关键在于,如果在并发标记的时候,出现了引用修改(不包含新分配内存给对象),那么写屏障会把这些引用的原始值捕获下来,记录在log buffer中。而后再处理。后续的所有的标记,都是从原来的值出发,而不是从新的值出发的。

SATB是一个逻辑上存在概念,在实际中并没有任何真的实际的数据结构与之对应。叫这个名字是因为,一旦进入了并发标记阶段,那么在该阶段的运行过程中,即便应用修改了引用,但是因为SATB的写屏障记录下来了原始的值,在遍历整个堆查找存活对象的时候,使用的依然是原来的值。这就是在逻辑上保持了一个snapshot at the beginning of concurrent marking phase

在处理新创建的对象,G1采用了不同的方式。G1用了两个TAMS变量了判断新创建的对象。一个叫做previous TAMS,一个叫做next TAMS。位于两者之间的对象就是新分配的对象。

并发标记阶段,bitmapTAMS的作用如图:

c67e3b72dccbe99a.webp

bitmapTAMS的作用

该图的详细解释如下:

  1. A是第一次marking cycleinitial marking阶段。next bitmap尚未标记任何存活对象,而此时的previous TAMS被初始化为region内存地址起始值,next TAMS被初始化为toptop实际上就是一个region未分配区域和已分配区域的分界点;
  2. B是经过concurrent marking阶段之后,进入了remark阶段。此时存活对象的扫描已经完成了,因此next bitmap构造好了,刚好代表的是当下状态中region中的内存使用情况。注意的是,此时top已经不再与next TAMS重合了,topnext TAMS之间的就是在前面标记阶段之时,新分配的对象;
  3. C代表的是clean up阶段。CB比起来,next bitmap变成了previous bitmap,而在bitmap中标记为垃圾(也就是白色区域的)的对应的region的区域也被染成了浅灰色。这并不是指垃圾对象已经被清扫了,仅仅是标记出来了。同时next TAMSprevious TAMS也交换了角色;
  4. D代表的是下一个marking cycleinitial marking阶段,该阶段和A类似,next TAMS重新被初始化为top的值;
  5. EF就是BC的重复;

3.1.4 最终标记(Remark phase)

19273e58b524b5f62c0a384678d0e176.png

该阶段是一个STW的阶段。引入该阶段的目的,是为了能够达到结束标记的目标。要结束标记的过程,要满足三个条件:

  1. concurrent marking已经追踪了所有的存活对象;
  2. marking stack是空的;
  3. 所有的log都被处理了;

前两个条件是很容易达到的,但是最后一个是很困难的。如果不引入一个STWremark过程,那么应用会不断的更新引用,也就是说,会不断的产生log,因而永远也无法达成完成标记的条件。

3.1.5 清除垃圾(Clean up)

04acece7c842199d9e0f1e1d1ea04bb8.png

该阶段主要完成:

  1. 统计存活对象,这是利用RSbitmap来完成的,统计的结果将会用来排序region,以用于下一次的CSet的选择;
  2. 重置RSet
  3. 把空闲region放到空闲region列表中;

该阶段比较容易引起误解地方在于,Clean up并不会清理垃圾对象,也不会执行存活对象的拷贝。也就是说,在极端情况下,该阶段结束之后,空闲Region列表将毫无变化,JVM的内存使用情况也毫无变化。

3.2 拷贝存活对象(Evacuation)

Evacuation阶段STW的,大概可以分成两个步骤:

第一个步骤是从Region中选出若干个Region进行回收,这些被选中的Region称为Collect Set(简称CSet);
第二个步骤则是把这些Region中存活的对象复制到空闲的Region中去,同时把这些已经被回收的Region放到空闲Region列表中。

这两个步骤又可以被分解成三个任务:

  1. 根据RS的日志更新RS:只有在处理完了RS的日志之后,RS才能够保证是准确的,完整的,这也是EvacuationSTW的重要原因;
  2. 扫描RS和其余的根来确定存活对象:该阶段实际上最主要依赖于RS
  3. 拷贝存活对象:该阶段只要从步骤2中确定的根触发,沿着引用链一直追溯下去,将存活对象复制到新的region就可以。这个过程中,可能有一部分的年轻代对象会被提升到老年代;

3.2.1 Evacuation的时机

Evacuation的触发时机在不同的模式下会有一些不同。在不同的模式下都相同的是,只要堆的使用率达到了某个阈值,就必然会触发Evacuation。这是为了确保在Evacuation的时候有足够的空闲Region来容纳存活对象。

young GC的情况下,G1会选择Nregion作为CSet,该CSet首先需要满足软实时的要求,而一旦已经有Nregion已经被分配了,那么就会执行一次Evacuation

G1会尽可能的执行Mixed GC。唯一的限制就是mix GC也需要满足软实时的要求。

G1触发Evacuation的原则大概是:

  1. 如果被分配的young region数量满足young GC的要求,那么就会触发young GC
  2. 如果被分配的young region数量不满足young GC,就会进一步考察加上old region的数量,能否满足old GC的要求;

为了理解这一点,可以举例来说,假如回收一个old region的时间是回收一个young region的两倍,也就是young region花费时间Told region花费2T,在满足软实时目标的情况下,GC只能回收8Tregion,那么:

  1. 假如应用现在只分配k(k<8)块young region,没有分配任何old region。这个时候又分配了一个old region,那么这个时候会立刻触发一次Mixed GC,此次GC会选择kyoung region和一块old region
  2. 因此,在这种假设下,只要有可以回收的old region的时候,总是会先回收old region
  3. 在没有任何old region的情况下,才有可能触发young region

当然,在一般情况下,这些假设是不成立的。读者可以思考一下,在young GCmixed GC达到软实时的要求下,young regionold region之间回收的花销不同会导致young GCmixed GC会在什么情况下触发。

  • 三色标记法:解释了为什么并发类回收器需要重新标记和stw的过程;
  • card table&remember set:解释了G1为什么可以在GC时不用扫描整个年老代从而对大堆更友好;
  • satb:解释了G1如何处理并发标记过程中的新增对象和引用变更以及浮动垃圾从何而来;
  • collection sets:简要说明为什么G1可以实现回收时间大致可控。

3.3 三色标记法

并发标记时的算法,CMS使用的是三色标记+Incremental Update算法,G1使用的是三色标记+snapshot at the begining(SATB)算法。

bfd340b53935a62dbacd48040b8bd130.png

  • 黑色:自身和成员变量均已标记完成
  • 灰色:自身被标记完成,成员变量未被标记
  • 白色:未被标记的对象(垃圾)

漏标情况,会将本来不是垃圾得对象,当作垃圾回收了。在运行态中,1、黑色对象指向了白色对象,2、并且同时,灰色对象取消了白色对象的引用。就会产生白色对象漏标的情况。因为黑色对象已经标记完毕,不会再去扫描,并且也没有灰色对象的指向,所以漏标了。

漏标的应对,打破这两种条件之一即可

CMS对漏标的处理方式:增量更新(Incremental update),关注引用的增加,如果发现黑色指向了白色,把黑色重新标记为白色,remark过程将重新扫描属性。但是会造成重复扫描已扫描过的属性。
G1对漏标的处理方式:SATB(snapshot at the begining),关注引用的删除,当灰色对象对白色对象的引用删除后,将这个引用推到GC的堆栈中,保证白色对象还是能够被扫描到。在扫描时拿到这个引用,由于有Rset存在,不需要扫描整个堆去查找指向白色的引用,效率比较高。SATB配合Rset,使得整个效率提高。

3.4 YoungGC完整流程

YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数-XX:MaxGCPauseMills设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数-XX:MaxGCPauseMills设定的值,那么就会触发Young GC

3.5 MixedGC完整流程

不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC

3.6 FullGC

停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。

四、G1分析

4.1 参数分析

1、什么时候触发标记阶段global concurrent marking

-XX:InitiatingHeapOccupancyPercent=45:设置触发全局扫描的堆占用百分比,默认值45%。For changing the marking threshold.

Sets the Java heap occupancy threshold that triggers a marking cycle. The default occupancy is 45 percent of the entire Java heap.

2、什么时候触发Mixed GC

-XX:G1HeapWastePercent=10:设置触发Mixed GC的空间浪费阈值,默认值10%,空间浪费百分比大于10%才触发Mixed GC

Sets the percentage of heap that you are willing to waste. The Java HotSpot VM does not initiate the mixed garbage collection cycle when the reclaimable percentage is less than the heap waste percentage. The default is 10 percent.

3、哪些参数影响old region进入CSet

-XX:G1MixedGCLiveThresholdPercent=65:设置old region中存活对象百分比,只有在此百分比以下的region才能进入Cset,默认值65%
-XX:G1OldCSetRegionThresholdPercent=10:设置在一次mixed gcold regions大小总和占堆比例上限,默认值10%,即一次mixed gc中回收的old regions大小总和不能超过堆大小的10%

4、Young GCMixed GC都是STW,为什么G1还可以被称为低延迟的GC实现呢?

可以看到在这么多步骤里,G1只有两件事是并发执行的:(1) 全局并发标记;(2)logging write barrier的部分处理。而“拷贝对象”(evacuation)这个很耗时的动作却不是并发而是完全暂停的。那G1为何还可以叫做低延迟的GC实现呢?

重点就在于G1虽然会mark整个堆,但并不evacuate所有有活对象的region;通过只选择收益高的少量regionevacuate,这种暂停的开销就可以(在一定范围内)可控。每次evacuate的暂停时间应该跟一般GCyoung GC类似。所以G1把自己标榜为“软实时”(soft real-time)的GC

4.2 GC日志分析及最佳实践

1、疏散失败(Evacuation Failure)

当没有更多的空闲region被提升到老一代或者复制到幸存空间时,并且由于堆已经达到最大值,堆不能扩展,从而发生Evacuation Failure。对于G1 GC,它是非常耗时的。

a. 对于成功复制的对象,G1需要更新引用,并且该region被一直引用。
b. 对于未成功复制的对象,G1将自动转发它们,并保留这些region

解决方案:

①. 不要过度加一些jvm参数。比如-Xmn,这个参数会限制G1的参数的自动扩展。可以仅使用-Xms-Xmx和暂停时间目标-XX:MaxGCPauseMillis,删除任何额外的堆大小,例如-Xmn,-XX:NewSize,-XX:MaxNewSize,-XX:SurvivorRatio等。
②. 如果问题仍然存在,则增加JVM堆大小(即-Xmx)。
③. 如果无法增加堆大小,并且您注意到marking cycle没有足够早地开始回收老一代,那么请减少-XX:InitiatingHeapOccupancyPercent。默认值是45%。减小该值将提前开始marking cycle。另一方面,如果marking cycle提前开始并且未收回,请将-XX:InitiatingHeapOccupancyPercent阈值增加到默认值以上。
④. 如果并发marking cycle准时开始,但需要很长时间才能完成,那么使用属性-XX:ConcGCThreads增加并发标记线程数的数量。默认是GC Workers: 1,单线程执行。
⑤. 如果有大量“空间耗尽(to-space exhausted)”或“空间溢出(to-space overflow)”GC事件,则增加-XX:G1ReservePercent。默认值是Java堆的10%。注意:G1 GC将此值限制在50%以内。

java的官方文档,里面最佳实践里提到:不要设置年轻代大小
设置-Xmn会干预G1收集器的默认行为显式设置年轻代的大小。

①. G1将不再遵守集合的暂停时间目标。所以本质上,设置年轻代大小会禁用暂停时间目标。
②. G1不再能够根据需要扩展和收缩年轻代空间。由于大小是固定的,因此不能对大小进行更改。

2、巨型对象分配(Humongous Allocation)

它是由G1 Humongous Allocation造成的。大型对象(Humongous)是大于G1region大小50%的对象。频繁大型对象分配会导致性能问题。如果region里面包含大量的大型对象,则该region中最后一个具有巨型对象的区域与区域末端之间的空间将不会使用。如果有多个这样的大型对象,这个未使用的空间可能导致堆碎片化。直到jdk1.8u40之前,这些巨型对象的回收只在full GC期间完成。在较新的JVM中,对这些对象的清理放在了清理阶段。

解决方案:

①. 增加region的大小,设置-XX:G1HeapRegionSize=n,但是这个参数需要设置为2的幂次方,最小值是1M,最大值是32M
②. 增加JVM堆大小(即-Xmx -Xms)。

3、Pause Prediction Model(停顿预测模型)

通过用户设定的GC停顿时间(参数-XX:MaxGCPauseMillis),G1以衰减平均值为理论基础,计算需要回收的Region数量从而进行满足。

4.3 参数设置

-XX:+UseG1GC:使用G1收集器

-XX:ParallelGCThreads:指定GC工作的线程数量

-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区

-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)

-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)

-XX:G1MaxNewSizePercent:新生代内存最大空间

-XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批
        对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,
        此时就会把年龄n(含)以上的对象都放入老年代

-XX:MaxTenuringThreshold:最大年龄阈值(默认15)

-XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),
        则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,
        如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了

-XX:G1MixedGCLiveThresholdPercent(默认85%):region中的存活对象低于这个值时才会回收该region,
        如果超过这个值,存活对象过多,回收的的意义不大。

-XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,
        然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。

-XX:G1HeapWastePercent(默认5%):gc过程中空出来的region是否充足阈值,在混合回收的时候,
        对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,
        然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,
        一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。

五、总结

为什么G1能够让用户设置应用的暂停时间?

根据Garbage First的原则,G1优先处理回收垃圾最大量的区间(Region),G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。

由于内存被分成了很多小块,又带来了另外好处,由于内存块比较小,进行内存压缩整理的代价都比较小,相比其它GC算法,可以有效的规避内存碎片的问题。

缺点:如果应用的内存非常吃紧,对内存进行部分回收根本不够,始终要进行整个Heap的回收,那么G1要做的工作量就一点也不会比其它垃圾回收器少,而且因为本身算法复杂了一点,可能比其它回收器还要差。G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW,主要是因为G1未能解决转移过程中准确定位对象地址的问题。因此G1比较适合内存稍大一点的应用(一般来说至少4G以上),小内存的应用还是用传统的垃圾回收器比如CMS比较合适。

六、拓展

6.1 CMS和G1收集器的选择

G1需要暂停来拷贝对象,而CMS在暂停中只需要扫描(mark)对象,那算法上G1的暂停时间会比CMS短么?其实CMS在较小的堆、合适的workload的条件下暂停时间可以很轻松的短于G1

在2011年的时候堆大小的分水岭大概在10GB~15GB左右:以下的-Xmx更适合CMS,以上的才适合试用G1。现在到了2014年,G1的实现经过一定调优,大概在6GB~8GB也可以跟CMS有一比,我之前见过有在-Xmx4g的环境里G1CMS的暂停时间更短的案例。

合适的workloadCMS最严重的暂停通常发生在remark阶段,因为它要扫描整个根集合,其中包括整个young gen。如果在CMS的并发标记阶段,mutator仍然在高速分配内存使得young gen里有很多对象的话,那remark阶段就可能会有很长时间的暂停。Young gen越大,CMS remark暂停时间就有可能越长。所以这是不适合CMSworkload。相反,如果mutator的分配速率比较温和,然后给足时间让并发的precleaning做好remark的前期工作,这样CMS就只需要较短的remark暂停,这种条件下G1的暂停时间很难低于CMS

参考文章

posted @ 2022-04-25 17:26  夏尔_717  阅读(759)  评论(0编辑  收藏  举报