GarbageFirst(G1)
G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:
- 并行与并发 G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
- 分代收集 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
- 空间整合 G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
- 可预测的停顿 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
内存划分Region
G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。
在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
PS:在java 8中,持久代也移动到了普通的堆内存空间中,改为元空间。
对象分配策略
说起大对象的分配,我们不得不谈谈对象的分配策略。它分为3个阶段:
- TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区
- Eden区中分配
- Humongous区分配
TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。
对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。
建立可预测的时间模型
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
避免全堆扫描——Remembered Set
G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。
为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
- 初始标记(Initial Marking) 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。
- 并发标记(Concurrent Marking) 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
- 最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
- 筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
GC模式
G1 Young GC
Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
这时,我们需要考虑一个问题,如果仅仅GC 新生代对象,我们如何找到所有的根对象呢? 老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。
在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。
但在G1中,并没有使用point-out,这是由于一个分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。于是G1中使用point-in来解决。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。
需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
Young GC 阶段:
- 阶段1:根扫描
静态和本地对象被扫描 - 阶段2:更新RS
处理dirty card队列更新RS - 阶段3:处理RS
检测从年轻代指向年老代的对象 - 阶段4:对象拷贝
拷贝存活的对象到survivor/old区域 - 阶段5:处理引用队列
软引用,弱引用,虚引用处理
G1 Mix GC
Mix GC是伴随着Young GC一起发生的。
Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。
它的GC步骤分2步:
- 全局并发标记(global concurrent marking)
- 拷贝存活对象(evacuation)
在进行Mix GC之前,会先进行global concurrent marking(全局并发标记)。 global concurrent marking的执行过程是怎样的呢?
在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:
Phase | Description |
---|---|
(1) Initial Mark(Stop the World Event) 初始标记 |
This is a stop the world event. With G1, it is piggybacked on a normal young GC. Mark survivor regions (root regions) which may have references to objects in old generation. 是STW的事件,并且依赖于young GC的发生。标记survivor regions作为root regions,因为它可能有指向老年代的引用。 |
(2) Root Region Scanning root region 扫描 |
Scan survivor regions for references into the old generation. This happens while the application continues to run. The phase must be completed before a young GC can occur. 扫描survivor regions 中指向 old generation 的引用。同时,应用还在继续执行。这个阶段必须在young GC发生之前完成。 |
(3) Concurrent Marking 并发标记 |
Find live objects over the entire heap. This happens while the application is running. This phase can be interrupted by young generation garbage collections. 找到整个heap 中存活的对象。同时,应用程序继续执行。这个阶段可以被young GC中断。 |
(4) Remark(Stop the World Event) 最终标记 |
Completes the marking of live object in the heap. Uses an algorithm called snapshot-at-the-beginning (SATB) which is much faster than what was used in the CMS collector. 使用SATB算法完成heap中活对象的标记。 |
(5) Cleanup(Stop the World Event and Concurrent) | - Performs accounting on live objects and completely free regions. (Stop the world) - Scrubs the Remembered Sets. (Stop the world) - Reset the empty regions and return them to the free list. (Concurrent) - 计算存活对象和完全空闲的regions。 - 重置remember set。 - 重置空闲regions,并放入空闲列表中。 |
() Copying(Stop the World Event)* | These are the stop the world pauses to evacuate or copy live objects to new unused regions. This can be done with young generation regions which are logged as [GC pause (young)] . Or both young and old generation regions which are logged as [GC Pause (mixed)] .计算并拷贝存活对象到新的regions中。这个阶段可能发生在yong GC 和mixed GC 中。 |
三色标记算法
提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。 首先,我们将对象分成三种类型的。
- 黑色:根对象,或者该对象与它的子对象都被扫描
- 灰色:对象本身被扫描,但还没扫描完该对象中的子对象
- 白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象
当GC开始扫描对象时,按照如下图步骤进行对象的扫描:
根对象被置为黑色,子对象被置为灰色。
继续由灰色遍历,将已扫描了子对象的对象置为黑色。
遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理。
这看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失问题
我们看下面一种情况,当垃圾收集器扫描到下面情况时:
这时候应用程序执行了以下操作:
A.c=C
B.c=null
这样,对象的状态图变成如下情形:
这时候垃圾收集器再标记扫描的时候就会下图成这样:
很显然,此时C是白色,被认为是垃圾需要清理掉,显然这是不合理的。那么我们如何保证应用程序在运行的时候,GC标记的对象不丢失呢?有如下2中可行的方式:
- 在插入的时候记录对象
- 在删除的时候记录对象
刚好这对应CMS和G1的2种不同实现方式:
在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。
在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:
1,在开始标记的时候生成一个快照图标记存活对象
2,在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)
3,可能存在游离的垃圾,将在下次被收集
这样,G1到现在可以知道哪些老的分区可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。混合式垃圾收集如下图:
混合式GC也是采用的复制的清理策略,当GC完成后,会重新释放空间。
至此,混合式GC告一段落了。下一小节我们讲进入调优实践。
G1 Full GC
如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc.
关键技术
Remember Set和Card Table
RS(Remember Set)是一种抽象概念,用于记录从非收集部分指向收集部分的指针的集合。
在传统的分代垃圾回收算法里面,RS(Remember Set)被用来记录分代之间的指针。在G1回收器里面,RS被用来记录从其他Region指向一个Region的指针情况。因此,一个Region就会有一个RS。这种记录可以带来一个极大的好处:在回收一个Region的时候不需要执行全堆扫描,只需要检查它的RS就可以找到外部引用,而这些引用就是initial mark的根之一。
那么,如果一个线程修改了Region内部的引用,就必须要去通知RS,更改其中的记录。为了达到这种目的,G1回收器引入了一种新的结构,CT(Card Table)——卡表。每一个Region,又被分成了固定大小的若干张卡(Card)。每一张卡,都用一个Byte来记录是否修改过。卡表即这些byte的集合。实际上,如果把RS理解成一个概念模型,那么CT就可以说是RS的一种实现方式。
从第一感觉,或者出于直觉的考虑,使用一个bit来记录一张卡是否被修改过,就已经足够了。而使用一个byte会造成更多的空间开销。但是实际上,使用一个byte来记录一张卡是否被修改过,会比使用一个bit来记录效率更高。更多细节参阅资料3。
在RS的修改上也会遇到并发的问题。因为一个Region可能有多个线程在并发修改,因此它们也会并发修改RS。为了避免这样一种冲突,G1垃圾回收器进一步把RS划分成了多个哈希表。每一个线程都在各自的哈希表里面修改。最终,从逻辑上来说,RS就是这些哈希表的集合。哈希表是实现RS的一种通常的方式之一。它有一个极大的好处就是能够去除重复。这意味着,RS的大小将和修改的指针数量相当。而在不去重的情况下,RS的数量和写操作的数量相当。
整个关系如下:
Remember Set
图中RS的虚线表名的是,RS并不是一个和Card Table独立的,不同的数据结构,而是指RS是一个概念模型。实际上,Card Table是RS的一种实现方式。
Remember Set的写屏障
写屏障是指,在改变特定内存的值(实际上也就是写入内存)的时候额外执行的一些动作。在大多数的垃圾回收算法中,都利用到了写屏障。写屏障通常用于在运行时探测并记录回收相关指针(interesting pointer),在回收器只回收堆中部分区域的时候,任何来自该区域外的指针都需要被写屏障捕获,这些指针将会在垃圾回收的时候作为标记开始的根。JAVA使用的其余的分代的垃圾回收器,都有写屏障。举例来说,每一次将一个老年代对象的引用修改为指向年轻代对象,都会被写屏障捕获,并且记录下来。因此在年轻代回收的时候,就可以避免扫描整个老年代来查找根。
G1垃圾回收器的写屏障和RS是相辅相成的,也就是记录Region内部的指针。这种记录发生在写操作之后。对于一个写屏障来说,过滤掉不必要的写操作是十分有必要的。这种过滤既能加快赋值器的速度,也能减轻回收器的负担。G1垃圾回收器采用的双重过滤
- 过滤掉同一个Region内部引用;
- 过滤掉空引用;
过滤掉这两个部分之后,可以使RS的大小大大减小。
G1的垃圾回收器的写屏障使用一种两级的log buffer结构:
- global set of filled buffer:所有线程共享的一个全局的,存放填满了的log buffer的集合;
- thread log buffer:每个线程自己的log buffer。所有的线程都会把写屏障的记录先放进去自己的log buffer中,装满了之后,就会把log buffer放到 global set of filled buffer中,而后再申请一个log buffer;
Collect Set
Collect Set(CSet)是指,在Evacuation阶段,由G1垃圾回收器选择的待回收的Region集合。G1垃圾回收器的软实时的特性就是通过CSet的选择来实现的。对应于算法的两种模式fully-young generational mode和partially-young mode,CSet的选择可以分成两种:
- 在fully-young generational mode下:顾名思义,该模式下CSet将只包含young的Region。G1将调整young的Region的数量来匹配软实时的目标;
- 在partially-young mode下:该模式会选择所有的young region,并且选择一部分的old region。old region的选择将依据在Marking cycle phase中对存活对象的计数。G1选择存活对象最少的Region进行回收。
SATB(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的误差。
SATB的log buffer如RS的写屏障使用的log buffer一样,都是两级结构,作用机制也是一样的。
细节可以参阅资料2,6
Marking bitmaps和TAMS
Marking bitmap是一种数据结构,其中的每一个bit代表的是一个可用于分配给对象的起始地址。举例来说:
其中addrN代表的是一个对象的起始地址。绿色的块代表的是在该起始地址处的对象是存活对象,而其余白色的块则代表了垃圾对象。
G1使用了两个bitmap,一个叫做previous bitmap,另外一个叫做next bitmap。previous bitmap记录的是上一次的标记阶段完成之后的构造的bitmap;next bitmap则是当前正在标记阶段正在构造的bitmap。在当前标记阶段结束之后,当前标记的next bitmap就变成了下一次标记阶段的previous bitmap。
TAMS(top at mark start)变量,是一对用于区分在标记阶段新分配对象的变量,分别被称为previous TAMS和next TAMS。在previous TAMS和next TAMS之间的对象则是本次标记阶段时候新分配的对象。如图:
白色region代表的是空闲空间,绿色region代表是存活对象,橙色region代表的在此次标记阶段新分配的对象。注意的是,在橙色区域的对象,并不能确保它们都事实上是存活的。
算法详解
整个算法可以分成两大部分:
- Marking cycle phase:标记阶段,该阶段是不断循环进行的;
- Evacuation phase:该阶段是负责把一部分region的活对象拷贝到空Region里面去,然后回收原本的Region空间,该阶段是STW(stop-the-world)的;
而算法也可以分成两种模式:
- fully-young generational mode:有时候也会被称为young GC,该模式只会回收young region,算法是通过调整young region的数量来达到软实时目标的;
- partially-young mode:也被称为Mixed GC,该阶段会回收young region和old region,算法通过调整old region的数量来达到软实时目标;
有趣的地方是不论处在何种模式之下,yong region都在被回收的范围内。而old region只能期望于Mixed GC。但是,如同在CMS垃圾回收器中遇到的困境一样,Mixed GC可能来不及回收old region。也就说,在需要分配老年代的对象的时候,并没有足够的空间。这个时候就只能触发一次full GC。
算法会自动在young GC和mixed GC之间切换,并且定期触发Marking cycle phase。HotSpot的G1实现允许指定一个参数InitiatingHeapOccupancyPercent,在达到该参数的情况下,就会执行marking cycle phase。
算法并不使用在对象头增加字段来标记该对象,而是采用bitmap的方式来记录一个对象被标记的情况。这种记录方法的好处就是在使用这些标记信息的时候,仅仅需要扫描bitmap而已。G1统计一个region的存活的对象,就是依赖于bitmap的标记。
调优实践
MaxGCPauseMillis调优
前面介绍过使用GC的最基本的参数:
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
前面2个参数都好理解,后面这个MaxGCPauseMillis参数该怎么配置呢?这个参数从字面的意思上看,就是允许的GC最大的暂停时间。G1尽量确保每次GC暂停的时间都在设置的MaxGCPauseMillis范围内。 那G1是如何做到最大暂停时间的呢?这涉及到另一个概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的区域集合。
- Young GC:选定所有新生代里的region。通过控制新生代的region个数来控制young GC的开销。
- Mixed GC:选定所有新生代里的region,外加根据global concurrent marking统计得出收集收益高的若干老年代region。在用户指定的开销目标范围内尽可能选择收益高的老年代region。
在理解了这些后,我们再设置最大暂停时间就好办了。 首先,我们能容忍的最大暂停时间是有一个限度的,我们需要在这个限度范围内设置。但是应该设置的值是多少呢?我们需要在吞吐量跟MaxGCPauseMillis之间做一个平衡。如果MaxGCPauseMillis设置的过小,那么GC就会频繁,吞吐量就会下降。如果MaxGCPauseMillis设置的过大,应用程序暂停时间就会变长。G1的默认暂停时间是200毫秒,我们可以从这里入手,调整合适的时间。
其他调优参数
-XX:G1HeapRegionSize=n
设置的 G1 区域的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。
-XX:ParallelGCThreads=n
设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多为 8。
如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的 5/8 左右。这适用于大多数情况,除非是较大的 SPARC 系统,其中 n 的值可以是逻辑处理器数的 5/16 左右。
-XX:ConcGCThreads=n
设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。
-XX:InitiatingHeapOccupancyPercent=45
设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。
避免使用以下参数:
避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。
触发Full GC
在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求,我们的程序当然不希望看到这些。那么发生Full GC的情况有哪些呢?
- 并发模式失败
G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。
- 晋升失败或者疏散失败
G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:
a,增加 -XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。
b,通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。
c,也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。
- 巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。
由于篇幅有限,G1还有很多调优实践,在此就不一一列出了,大家在平常的实践中可以慢慢探索。最后,期待java 9能正式发布,默认使用G1为垃圾收集器的java性能会不会又提高呢?
使用场景
The first focus of G1 is to provide a solution for users running applications that require large heaps with limited GC latency. This means heap sizes of around 6GB or larger, and stable and predictable pause time below 0.5 seconds.
Applications running today with either the CMS or the ParallelOldGC garbage collector would benefit switching to G1 if the application has one or more of the following traits.
- Full GC durations are too long or too frequent.
- The rate of object allocation rate or promotion varies significantly.
- Undesired long garbage collection or compaction pauses (longer than 0.5 to 1 second)
总结
- regions 划分:Eden、Survivor、Old、Humongous
- young gc 和 mixed gc
- remember set、card table、satb(snapshot-at-the-beginning)
参考
https://crowhawk.github.io/2017/08/15/jvm_3/
http://blog.jobbole.com/109170/
https://www.jianshu.com/p/8bd15969a641
http://www.importnew.com/27793.html
https://tech.meituan.com/g1.html
http://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
https://blog.csdn.net/renfufei/article/details/41897113
https://www.jianshu.com/p/870abddaba41