关键描述

       G1是一种服务器端的垃圾收集器,应用在多处理器和大内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求,全堆操作(例如全局标记)与应用程序线程并行执行。这样可以防止与堆或活动数据大小成比例的中断。
       G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色: 
* G1是一个有整理内存过程的垃圾收集器,在回收垃圾的时候会压缩存活对象。不会产生很多内存碎片。
* G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

G1中几个重要术语

1,Region 常翻译成分区/区域

         传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代/PermGen,引入了元空间Metaspace),这种划分的特点是各代的存储地址(逻辑地址)是连续的。
如下图所示:

       而G1也是分代管理内存的,他的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。Java堆空间被G1 GC分成2048个大小一样的region,(2048个region是可以改的,使用G1 GC的jvm配置参数 -XX:G1HeapRegionSize=2M,设置单个region的大小,一个2g的堆就会被分成1024个region)   之前熟悉的内存分区名称 Eden,Survivor和Old是n个region的逻辑集合,并不物理相邻。
        在实际查看使用G1 GC的程序的内存使用情况的时候,使用命令jmap -heap pid 就会看到E区的regions size是x个,S区的regions size是Y个 ,O区的regions size是Z个,x+y+z < 2048,正常情况下还有好多个region是free的,未被使用的。
       还有就是这几个分区的region个数也是一直在变化的,不像之前的其它gc,分区定了之后,各区大小很少变化。
       -XX:G1HeapRegionSize=n,设置the size of a G1 region的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。看文章末尾的截图吧。
如下图所示:

2,巨型区域

       对于 G1 GC,任何超过区域一半大小的对象都被视为“巨型对象”。此类对象直接被分配到老年代中的“巨型区域” Humongous。这些巨型区域是一个连续的区域集(n个地址连续的region,为的是可以拿出来连续的地址给大对象分配空间)。StartsHumongous 标记该连续集的开始,ContinuesHumongous 标记它的延续。上图的H区就是说的这个。H区的gc频率就会低点儿。
H-obj有如下几个特征: 
* H-obj直接分配到了old gen,防止了反复拷贝移动。 
* H-obj在global concurrent marking阶段的cleanup 和 full GC阶段回收。 
* 在分配H-obj之前先检查是否超过 initiating heap occupancy percent和the marking threshold, 如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 full GC。
       如果一个巨型对象跨越两个分区,开始的那个分区被称为“开始巨型”,后面的分区被称为“连续巨型”,这样最后一个分区的一部分空间是被浪费掉的,如果有很多巨型对象都刚好比分区大小多一点,就会造成很多空间的浪费,从而导致堆的碎片化。如果你发现有很多由于巨型对象分配引起的连续的并发周期,并且堆已经碎片化(明明空间够,但是触发了FULL GC),可以考虑调整-XX:G1HeapRegionSize参数,减少或消除巨型对象的分配。
       关于巨型对象的回收:在JDK8u40之前,巨型对象的回收只能在并发收集周期的清除阶段或FULL GC过程中过程中被回收,在JDK8u40(包括这个版本)之后,一旦没有任何其他对象引用巨型对象,那么巨型对象也可以在年轻代收集中被回收。

3,CSet (collection sets)

        一组可被回收的region的集合。在CSet中存活的数据会以增量、并行的方式复制到不同的一个或者n个新region来实现压缩,从而减少堆碎片(类似young区的复制算法),CSet中的region可以来自Eden空间、Survivor空间、或者Old。CSet会占用不到整个堆空间的1%大小。目标是从可回收空间最多的区域开始,尽可能回收更多的堆空间,同时尽可能不超出暂停时间目标。

4,RSet(Remembered Set)

全称是Remembered Set,是辅助GC过程的一种结构,典型的空间换时间工具。在GC的时候,对于old->young和old->old的跨代跨region对象引用,只要扫描对应的CSet中的RSet即可。 逻辑上说每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。G1的RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可。

       如上图所示,三个Region,每个Region被分成了多个Card,在不同Region中的Card会相互引用,Region1中的Card中的对象引用了Region2中的Card中的对象,蓝色实线表示的就是points-out的关系,而在Region2的RSet中,记录了Region1的Card,即红色虚线表示的关系,这就是points-into。 而维系RSet中的引用关系靠post-write barrier和Concurrent refinement threads来维护。
       RSet究竟是怎么辅助GC的呢?在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。 而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量。

5,停顿预测模型

        G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.G1 GC是一个响应时间优先的GC收集器,它与CMS最大的不同是,用户可以设定整个GC过程的期望停顿时间,参数-XX:MaxGCPauseMillis指定一个G1收集过程目标停顿时间,默认值200ms,不过它不是硬性条件,只是期望值。那么G1怎么满足用户的期望呢?就需要这个停顿预测模型了。G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。 

6,G1 GC中的几个关键动作

1,young gc

    g1gc也是分代收集垃圾的,

  • Eden区耗尽的时候就会触发新生代收集,新生代垃圾收集会对整个新生代(E + S)进行回收
  • 新生代垃圾收集期间,整个应用STW
  • 新生代垃圾收集是由多线程并发执行的
  • 通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
  • 新生代收集结束后依然存活的对象,会被疏散evacuation到n(n>=1)个新的Survivor分区,或者是老年代。

2,全堆并发标记

  • 初始标记(initial-mark),在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) young垃圾回收密切相关。在这个阶段,应用会经历STW,通常初始标记阶段会跟一次新生代收集一起进行,换句话说——既然这两个阶段都需要暂停应用,G1 GC就重用了新生代收集来完成初始标记的工作。在新生代垃圾收集中进行初始标记的工作,会让停顿时间稍微长一点,并且会增加CPU的开销。初始标记做的工作是设置两个TAMS变量(NTAMS和PTAMS)的值,所有在TAMS之上的对象在这个并发周期内会被识别为隐式存活对象;
  • 根分区扫描(root-region-scan),G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。这个过程不需要暂停应用,在初始标记或新生代收集中被拷贝到survivor分区的对象,都需要被看做是根,这个阶段G1开始扫描survivor分区,所有被survivor分区所引用的对象都会被扫描到并将被标记。survivor分区就是根分区,正因为这个,该阶段不能发生新生代收集,如果扫描根分区时,新生代的空间恰好用尽,新生代垃圾收集必须等待根分区扫描结束才能完成。如果在日志中发现根分区扫描和新生代收集的日志交替出现,就说明当前应用需要调优。
  • 并发标记阶段(concurrent-mark),G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断。并发标记阶段是多线程的,我们可以通过-XX:ConcGCThreads来设置并发线程数,默认情况下,G1垃圾收集器会将这个线程总数设置为并行垃圾线程数(-XX:ParallelGCThreads)的四分之一;并发标记会利用trace算法找到所有活着的对象,并记录在一个bitmap中,因为在TAMS之上的对象都被视为隐式存活,因此我们只需要遍历那些在TAMS之下的;记录在标记的时候发生的引用改变,SATB的思路是在开始的时候设置一个快照,然后假定这个快照不改变,根据这个快照去进行trace,这时候如果某个对象的引用发生变化,就需要通过pre-write barrier logs将该对象的旧的值记录在一个SATB缓冲区中,如果这个缓冲区满了,就把它加到一个全局的列表中——G1会有并发标记的线程定期去处理这个全局列表。
  • 重新标记阶段(remarking),重新标记阶段是最后一个标记阶段,需要暂停整个应用,G1垃圾收集器会处理掉剩下的SATB日志缓冲区和所有更新的引用,同时G1垃圾收集器还会找出所有未被标记的存活对象。这个阶段还会负责引用处理等工作。
  • 清理阶段(cleanup),在这个最后阶段,G1 GC 执行统计和 RSet 净化的操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。清理阶段真正回收的内存很小,截止到这个阶段,G1垃圾收集器主要是标记处哪些老年代分区可以回收,将老年代按照它们的存活度(liveness)从小到大排列。这个过程还会做几个事情:识别出所有空闲的分区、RSet梳理、将不用的类从metaspace中卸载、回收巨型对象等等。识别出每个分区里存活的对象有个好处是在遇到一个完全空闲的分区时,它的RSet可以立即被清理,同时这个分区可以立刻被回收并释放到空闲队列中,而不需要再放入CSet等待混合收集阶段回收;梳理RSet有助于发现无用的引用。

        G1设计了一个标记阈值,它描述的是总体Java堆大小的百分比,默认值是45,这个值可以通过命令-XX:InitiatingHeapOccupancyPercent来调整,一旦达到这个阈值就会触发一次并发收集周期。注意:这里的百分比是针对整个堆大小的百分比
        在并发收集周期中,至少有一次(很可能是多次)新生代垃圾收集,上面说了并发标记和young gc一起发生,共同使用stw时间,这是因为他们可以复用root scan操作,所以可以说global concurrent marking是伴随Young GC而发生的;
        标记周期找出的包含最多垃圾的分区(注意:它们内部仍然保留着数据);
        老年代的空间占用在标记周期结束后变得更多,这是因为在标记周期期间,新生代的垃圾收集会晋升对象到老年代,而且标记周期中并不会回收老年代的任何对象。
        第四阶段Cleanup只是回收了没有存活对象的Region。

3,mixed gc  != full gc

         Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。
         Mixed GC不是full GC,它只能回收部分老年代的Region,如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。所以我们可以知道,G1是不提供full GC的。这个serial old GC full gc是单线程的(在Java 8中)并且非常慢,因此应避免使用。
         什么时候发生Mixed GC呢?
         其实是由一些参数控制着的,另外也控制着哪些老年代Region会被选入CSet。 
         * G1HeapWastePercent:在global concurrent marking结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。 
         * G1MixedGCLiveThresholdPercent:old generation region中的存活对象的占比,只有在此参数之下,才会被选入CSet。 
         * G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数。 
         * G1OldCSetRegionThresholdPercent:一次Mixed GC中能被选入CSet的最多old generation region数量。

4,full gc 全堆gc

如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。G1是不提供full GC的。这个serial old GC full gc是单线程的(在Java 8中)并且非常慢,因此应避免在G1 gc的时候出现这个full gc 。不能觉得用了G1gc收集器之后,Java heap里面的gc不是young gc 就mixed gc,还有这个full gc呢。有必要强调一下。

7,G 1 GC支持的配置参数以及默认值

-XX:G1HeapRegionSize=n 设置the size of a G1 region的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。
-XX:MaxGCPauseMillis=200 设置最长暂停时间目标值。默认值是 200 毫秒。
-XX:G1NewSizePercent=5 设置年轻代最小值所占总堆的百分比。默认值是堆的 5%。
-XX:G1MaxNewSizePercent=60 设置年轻代最大值所占总堆的百分比。默认值是 Java 堆的 60%。
-XX:ParallelGCThreads=n

设置 STW 并行gc工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多为 8。

如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的 5/8 左右。这适用于大多数情况,除非是较大的 SPARC 系统,其中 n 的值可以是逻辑处理器数的 5/16 左右。

-XX:ConcGCThreads=n 并发标记阶段,并发执行的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。
-XX:InitiatingHeapOccupancyPercent=45 设置触发全局并发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。
-XX:G1MixedGCLiveThresholdPercent=65 old generation region中的存活对象的占比,只有占比小于此参数的old region,才会被选入CSet。数越大,活的对象越多,这个region可回收的就越少,gc效果就不明显。就优先不gc这些region
-XX:G1HeapWastePercent=10

设置您愿意浪费的堆百分比。如果可回收百分比小于堆废物百分比,Java HotSpot VM 不会启动混合垃圾周期。默认值是 10%。

在global concurrent marking结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC

-XX:G1MixedGCCountTarget=8 一次global concurrent marking之后,最多执行Mixed GC的次数。
-XX:G1OldCSetRegionThresholdPercent=10 一次Mixed GC中能被选入CSet的最多old generation region数量。默认值是 Java 堆的 10%
-XX:G1ReservePercent=10 设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险。默认值是 10%。增加或减少百分比时,请确保对总的 Java 堆调整相同的liang

8,实际看看程序在G1 GC收集器情况下的一些信息

1,jmap -heap pid 查看G1gc收集器下的堆内存情况

可以看到整个堆的大小8g多,G1 heap region size,也就是单个region的大小是4M,总的G1 heap分成2048个region,g1 heap的容量上8192M = 8g多,New size 500M,E区分了108个regions,s区分了4个regions,Old区分了399个regions,ESO区的容量使用空闲以及使用率都有展示,还带单位的。ESO的各个的总region的个数,是一直在变的,不同的时候,看到的个数是不一样的。

2,jstat -gc pid  1s 10

上图看程序的gc情况,这个程序的jvm的各种设置信息 jinfo -flags pid

和之前的gc收集器不同的是,他的s区,是只有1个在用的,另一个一直空的,之前还说为啥young区要2个s区呢,现在这个gc就只使用一个s区就OK了呢?这也和他分n个region的设计有关系,s区就是n个不同的region,其实已经隐式的分成了2个s区了,也可以实现copy算法的垃圾收集。上面这个截图中young gc的频率还是很高的,代码里面是几个线程一直在消费数据。一直在产生废对象。

因为在jvm的设置里面显示的设置了young区的大小512m,然后,看到gc的频率是刷刷刷的,一秒很多次(看YGC那列的2行的差),因为新对象一般都会出生在young区的E区,设置了young区的大小,那么程序一直在飞速的运转,那么young区是有限的,除了已经被old占领的地方之外,还有大片的地方是free的,暂时是浪费的,然后young gc一直在干活,为了回收那一丢丢的空间,下面是把这个-Xmn的设置给去掉之后,的效果。

丢到young区大小的设置之后,E区就宽敞很多了,他直接2.5个多g,在new 对象的时候就宽敞多了,因为程序里面的对象大多都在young区使用完之后,很少部分会被晋级到old区,e区基本都是复制+整理算法在回收地盘。

下面是去掉young区大小的设置之后的gc信息

发现这个gc信息中young gc的频率明显降低,gc时间也少了,可以留更多的时间给cpu处理我们代码里面的事儿,每秒处理的数据量就稍微上来了一丢丢,下面的日志打印也稍微能说明点问题,前面2个是之前有-Xmn设置的时候处理量,最后一个是刚刚去掉这个设置之后的处理量。

9,G1 调优

        G1的调优目标主要是在避免FULL GC和疏散失败的前提下,尽量实现较短的停顿时间和较高的吞吐量。关于G1 GC的调优,需要记住以下几点:
        不要显式的设置新生代的大小(用Xmn或-XX:NewRatio参数),如果显式设置新生代的大小,会导致目标时间这个参数失效。
        由于G1收集器自身已经有一套预测和调整机制了,因此我们首先的选择是相信它,即调整-XX:MaxGCPauseMillis=N参数,这也符合G1的目的——让GC调优尽量简单,这里有个取舍:如果减小这个参数的值,就意味着会调小新生代的大小,也会导致新生代GC发生得更频繁,同时,还会导致混合收集周期中回收的老年代分区减少,从而增加FULL GC的风险。这个时间设置得越短,应用的吞吐量也会受到影响。
        针对混合垃圾收集的调优。如果调整这期望的最大暂停时间这个参数还是无法解决问题,即在日志中仍然可以看到FULL GC的现象,那么就需要自己手动做一些调整,可以做的调整包括:
        调整G1垃圾收集的后台线程数,通过设置-XX:ConcGCThreads=n这个参数,可以增加后台标记线程的数量,帮G1赢得这场你追我赶的游戏;
        调整G1垃圾收集器并发周期的频率,如果让G1更早得启动垃圾收集,也可以帮助G1赢得这场比赛,那么可以通过设置-XX:InitiatingHeapOccupancyPercent这个参数来实现这个目标,如果将这个参数调小,G1就会更早得触发并发垃圾收集周期。这个值需要谨慎设置:如果这个参数设置得太高,会导致FULL GC出现得频繁;如果这个值设置得过小,又会导致G1频繁得进行并发收集,白白浪费CPU资源。通过GC日志可以通过一个点来判断GC是否正常——在一轮并发周期结束后,需要确保堆剩下的空间小于InitiatingHeapOccupancyPercent的值。
        调整G1垃圾收集器的混合收集的工作量,即在一次混合垃圾收集中尽量多处理一些分区,可以从另外一方面提高混合垃圾收集的频率。在一次混合收集中可以回收多少分区,取决于三个因素:
       (1)有多少个分区被认定为垃圾分区,-XX:G1MixedGCLiveThresholdPercent=n这个参数表示如果一个分区中的存活对象比例超过n,就不会被挑选为垃圾分区,因此可以通过这个参数控制每次混合收集的分区个数,这个参数的值越大,某个分区越容易被当做是垃圾分区;
       (2)G1在一个并发周期中,最多经历几次混合收集周期,这个可以通过-XX:G1MixedGCCountTarget=n设置,默认是8,如果减小这个值,可以增加每次混合收集收集的分区数,但是可能会导致停顿时间过长;
        3)期望的GC停顿的最大值,由MaxGCPauseMillis参数确定,默认值是200ms,在混合收集周期内的停顿时间是向上规整的,如果实际运行时间比这个参数小,那么G1就能收集更多的分区。

10,常见问题

1,Young GC、Mixed GC和Full GC的区别? 
        答:Young GC的CSet中只包括年轻代的region,Mixed GC的CSet中除了包括young的region,还包括n个Old的region;Full GC会暂停整个引用,同时对新生代和老年代进行收集和压缩。
2,ParallelGCThreads和ConcGCThreads的区别? 
        答:ParallelGCThreads指得是在STW阶段,并行执行垃圾收集动作的线程数,
ParallelGCThreads的值一般等于逻辑CPU核数,如果CPU核数大于8,则设置为5/8 * cpus,在SPARC等大型机上这个系数是5/16。;ConcGCThreads指的是在并发标记阶段,并发执行标记的线程数,一般设置为ParallelGCThreads的四分之一。
3,为什么G1GC在较大的堆中会更好地工作?
        在开始时JVM启动→分配了堆→堆被标记为S,E或O→应用程序启动→创建了对象→E空间已满→当所有E空间都被填满时,将发生年轻的GC→年轻GC把E区所有live对象,根据年龄和资格将它们复制到S区和O区→要进行此复制,应该有足够的free区域(O区和S区)来容纳来自young gc之后的live对象→如果堆空间小,则来自E区的对象将没有空间可容纳和压缩。因此,G1GC适合大堆。可以理解为复制算法和标记整理算法共同作用,复制算法就是需要大的内存。


发布评论
IT序号网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

concurrent 和 parallel; 并发和并行的区别知识解答
你是第一个吃螃蟹的人
发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。