G1:Java性能调优指南

并发收集器 Parallel Grabage Collector: 垃圾收回线程工作的同时,应用程序也正常工作,从而系统响应性较好,但垃圾回收过程需要更多的时间。

并行收集器 Concurrent Garbage Collector: 指垃圾回收会启用多进线程,并行执行,速度快,但会使应用程序的线程暂停Stop-The-World。因此具有较高的吞吐量,是HotSpot虚拟机默认的收集器。

相关命令:-XX:+UseParallelOldGC  -XX:+UseParallelGC

随着Java堆的大小以及老年代存活对象的数量和大小的不断增长,老年代的垃圾收集时间越来越长。同时硬件性能也在不断提高,因此通过增加一个多线程的老年代收集器与多线程的新生代收集器同时使用的方式,并行垃圾收集器得到了增强。这使并行垃圾收集器降低了收集和压缩堆的时间开销。-XX:+UseParallelOldGC也就默认开启了新生代的并行垃圾收集。这是Java7u4的缺少垃圾收集器。并且不论启用并行新生代,还是并行老年代都会激活对方。

在以下情景下,优先选择并行垃圾收集器:

1. 对应用吞吐量的要求 远高于延迟的要求。典型应用:批处理应用。

2. 在满足最差延迟要求的前提下,并行垃圾收集器可以提供最佳吞吐量。最差延迟包括:最差延迟时间、中断发生的频率。

在Java Hotspot Client模式下,默认使用串行的垃圾收集器,同时也被广泛应用于大师嵌入式场景的需求。-XX:UserSerialGC。

 

CMS : Concurrent Mark-and-Sweep 垃圾收集器

比并行收集器,拥有更短的中断时间,响应性较高。通过 牺牲一次的吞吐量来消除或减少漫长的GC中断数量也是可以接受的。

在CMS垃圾收集器中,年轻代的垃圾收集和 并行垃圾收集器很类似,都是并行的,并且会Stop-The-World。在Java8,9中已经不再支持显式设置一个串行化的新生代垃圾收集器。

CMS与Parallel收集器的区别:老年代的垃圾收集方式不同。CMS老年代收集活动试图避免应用程序长时间中断。只有在初始标记阶段、重新标记阶段会暂停应用线程,其余阶段都是和应用程序同时工作的。

默认情况下初始标记和重新标记都是单线程的,可以激活多线程的。-XX:+UseConcurrentMarkSweepGC.

CMS GC的缺少新生代收集器被称为ParNew收集器

G1 Garbage First垃圾收集器

在G1中,年轻代就是一系列的内存分区,这意味着不再要求新生代是一个连续的内存块。

老年代同样也是由一系列的分区组成。因此就不再需要在JVM运行时考虑哪些分区是老年代还是新生代。

G1通常的运行状态是映射G1分区的虚拟内存随着时间的推移在不同的代之间前后切换。

一个G1分区最初始被指定为新生代,经过一次年轻代的回收之后,整个年轻代分区都被划入到未被使用的分区中。

术语:可用分区。用来定义那些未被使用且可以被G1使用的分区。一个可用分区能被用于或指定为年轻代或老年代分区。可能在完成一个年轻代收集之后,一个年轻代的分区在未来某个时刻被用于老年代分区。同样的老年代分区完成之后事,它也成为了可用分区,在未来某个时候作为一个年轻代分区使用。

G1年轻代的收集方式是并行Stop-The-World。(与Parallel GC, CMS在年轻代,垃圾收集是一致的)

G1老年代的收集与其它HotSpot收集器有很大的不同。G1老年代的收集不会为了释放老年代的空间就要求对整个老年代做回收。相反,在任一时刻只有一部分老年代分区会被回收。并且这部分老年代分区将与一次年轻代收集一起被回收。

G1老年代的收集是由一系列阶段组成,某些是并行的stop-thep-world,某些是并行并发的。也就是说,某些阶段是多线程的同时会暂停所有应用线程,而其它阶段是多线程的,但可以与应用应用程序同行运行。

当超过Java堆的占用阈值,G1就会启动一次老年代收集。这是根据老年代的占用空间与整个Java堆空间相比较得出的。而在CMS中,CMS触发老年代收集所用的占用阈值只是相对于老年代空间本身而言的。在G1中,一旦达到或超过内存堆的占用阈值,一次并发STOP-THE-WORLD方式的初始标记阶段就会被安排执行。

初始标记阶段会跟着下一次的年轻代收集同时进行。一旦初始标记阶段结束,就会触发一个并发多线程的标记阶段,标记老年代中所有的存活对象。当并发标记结束之后,并行stop-the-world的重新标记阶段就被启动,标记那些因为在标记阶段同时执行的应用线程导致产生错对的对象。到重新标记结束,G1就拥有了老年代分区的完整信息。如果老年代分区里没有一个存活对象,那么在下一阶段---清除阶段,不用做额外的垃圾收集工作就可以被回收再利用。

在重新标记阶段结束,G1能够识别出最适合回收的老年代分区集合。

选择哪些分区可以被包含在一个CSet,是基于有多少空间可以被释放以及G1暂集时间目标。在完成CSet识别之后,G1就在新下来的几次年轻代垃圾收集过程中对CSet中的分区进行回收。也就是说,在接下来的几个年轻代垃圾收集中,除了年轻代分区,还有一部分老年代分区也将被回收。这就是前面提到的混合GC类型。

G1设计

分区的大小可以依据堆的大小而改变,但必是2的幂。同时最小为1MB,最大为32MB。

例如一个16G的JAVA堆使用-Xmx16,-Xms16g命令行选项,G1会选择采用16GB/2000=8MB的分区尺寸。

如果Java堆内存初始值和最大值相关很远,或者这个堆内存的尺寸非常大,很有可能会产生超过2000个的分区。若堆分区很小,则分区数量会远小于2000。

并发周期

一个G1并发周期包含了几个阶段的活动:初始标记,并发根分区扫描,并发标记,重新标记、清除。一个并发周期从初始标记开始,到清除阶段结束。除了清除阶段,所有这些阶段都是“标记存活对象图”的组成部分。

初始标记阶段的目的:收集所有的GC根。根是对象图的起点。为了从应用线程中收集根引用,必须先暂停这些应用线程,所有初始标记阶段都是stop-the-world方式。在G1里,完成初始标记是年轻代GC暂停的一个组成部分,因为无论如何年轻代GC都必须收集所有根。

标记操作的同时还必须扫描和跟踪survivor分区里所有对象的引用。这也是并发根分区扫描所要做的事。这个阶段,所有Java线程都允许执行,不会发生应用暂停。唯一的限制是在下一次GC启动之前必须先完成扫描。原因是一次新的GC会产生一个新的存活对象集合,与初始标记的存活对象是有区别的。

大部分标记工作是在并发标记阶段完成的。多个线程协同标示存活对象图。所有 JAVA线程都可以与并发标记线程同时运行,应用不存在暂停,但吞吐量会受影响。

完成并发标记之后就需要另一个stop-the-world方式的阶段来完成最后所有的标记工作。称为重新标记阶段。

并发标记的最后阶段是清除阶段,找出来的那些没有任何存活对象的分区将被回收。因为没有任何存活对象,这些分区将不会包含在年轻代或混合GC中,它们可以被添加到可用分区的队列中。

完成标记阶段之后,就可以找出哪些对象是存活的,进而确定哪些分区要被包含在混合GC里。混合GC是G1释放内存的基本手段,那么在G1用光可用分区之前完成标记阶段是至关重要的的。如果做不到,G1只能退回去发起一次full GC来释放内存,这虽然可靠,但很慢。确保标记阶段及时完成以避免fullGC,需要进行调优。

堆空间调整

 G1里的Java堆尺寸通常是分区的整数倍。除去这个限制,G1与其它HotSpot垃圾收集器将一样,可以在-Xms和-Xmx之间动态地扩大或缩小堆大小。

基于以下理由,G1可能会增加堆尺寸:

1. 在一次fullGC中,基于堆尺寸的计算结果会调整堆的空间

2.当发生年轻代收集或混合收集,G1会计算执行GC所花费的时间以及执行JAVA应用所花费的时间。根据命令行配置-XX:GCTimeRatio,如果将太多时间用在垃圾收集上,JAVA堆尺寸就会增加。这个情况下增加JAVA堆尺寸,背后的思想是允许GC减少发生频度,这样与花在应用上的时间相比,花在GC上的时间也可以随之降低。

G1中的-XX:GCTimeRatio的缺省值为9,其它的HotSpot垃圾收集器都缺省使用99.GCTimeRatio值越大,JAVA堆尺寸的增长就会更加积极。其它的HOTSPOT堆尺寸的增加策略更加激进,因为它们的目标是:相对于执行应用的开销,用于GC的时间越少越好。

如果一个对象分配失败了(基至是做了一次GC之后),G1会尝试增加堆尺寸来满足对象分配,而不是马上退回去做一次FULLGC

如果一个巨型对象分配无法找到足够的连续分区来容纳这个对象,G1会尝试扩展JAVA堆来获取更多的可用分区,而不是做一次FULLGC。

当GC需要一个新的分区来转移对象时,G1更倾向于通过增加JAVA堆空间来获得一个新的分区,而不是通过返回GC失败并开始做一次FULLGC来找到一个可用分区。

第二章:深入Garbage First垃圾收集器

G1 GC的基于原则:首先尽可能多的收集垃圾,因此被命名为Garbage First GC。

G1有增量并行的stop-the-world方式的暂停,通过考贝的方式来实现压缩,同时还有并行的多级并发标记,这有助于将标记、重新标记,以及因清除导致的暂停减少到最小程度。

在G1中,将传统的各个代必须相信的堆布局方式,改变为由多个不相邻分区组合而成的方式。由多个分区汇集起来组成一个逻辑上的代,这个代也符合过去HotSpot垃圾收集器中对代空间概念(年轻代、老年代)的常规定义。

年轻代

G1垃圾收集器是一种分代垃圾收集器,由老年代和年轻代组成。

除了一些例化情况,如巨型对象本身,还有许多虽然小于巨型对象,但依然很大,以至于无法放入TLAB(线程本地分配缓冲)。JAVA线程本地分配缓冲区,不会产生锁竞争,分配速度更快。

TLAB大小默认采用年轻代初始化尺寸、最大尺寸、以及应用的暂停时间目标(-XX:MaxGCPauseMillis)来自动 计算当前空间的大小。在JDK8u45中,年轻代初始化空间缺少是整个JAVA堆的5%(-XX:G1NewSizePrecent),年轻代最大缺省空间是整个JAVA堆的60%(-XX:G1MaxNewSizePercent)。

-XX:MaxGCPauseMillis  默认为200ms暂停时间目标。

一旦设定的年轻代空间被占满,新的空闲分区就会被添加到年代代中。

堆分区的大小必须是2的幂次,同时最小为1M,最大为32MB。 

JVM划分了大约2048个分区,可自定义分区个数-XX:G1HeapRegionSize=n

年轻代收集暂停:

如果eden中的对象无法被考贝到survivor中,则会promote到老年代Tenuring。

同时晋升的时候,内存的分配是在晋升本地分配缓冲区中(promotion thread local allcation buffer)进行的PLAB,每个线程都有一个PLAB。

在年轻代收集暂停过程中,G1 GC根据本次收集花费的时间总和来计算以下几个内容:

当前年轻代空间需要扩展或者缩小的空间大小(即G1决定增加或移除的空闲分区数)

已记忆集合的空间尺寸。

当前、最大、最小年轻代空间容量

暂停时间

因此,在垃圾收集的最后,年轻代空间就会做相应的调整。

通过-XX:PrintGCDetails输出的结果,我们可以观察计算年轻代前一次以及后续的空间变化 。

 

对象老化与老年代:

G1 GC需要维护每个对象的年龄字段,存活对象所经历过的垃圾收集总次数,称为对象的“年龄”。G1 GC将那么晋升对象的尺寸总和与它们的年龄信息一起维护到年龄表中。

结合年龄表、survivor尺寸,survivor填充容量(-XX:TargetSurvivorRatio选项决定,缺省为50),以及命令行选项-XX:MaxTenuringThreshold(缺省为15),JVM将给所有的存活对象设置一个恰当的任期阈值。一旦对象年龄超过这个任期阈值,将会被晋升到老年代。当老年代的中对象死亡时,它们的空间会在一次混合MIX GC中被回收,空间也可能会在一次清除过程中释放 ,或者在full GC中释放。

巨型分区

对G1来说,收集是一个分区为单位的,因此堆分区尺寸-XX:G1HeapRegionSize是一个非常重要的参数,因为它决定了什么样尺寸的对象可以放进一个分区。堆分区尺寸同样也决定了哪些对象可以被称之为巨型对象。它们至少需要占用一个分区50%的空间。这样的对象不会使用通常的快速分配方式(TLAB),而是在老年代的巨型分直接分配的。

之所有直接分配在老年代里,是因为通常巨型对象的存活时间较长,符合老年代对象的特点,因此直接分配。减少不断老化的复制、考贝过程。

JDK8U40对巨型对象的回收做了一个重要的修改,一时没有任何外部引用指向巨型分区的对象,那么巨型分区就可以在年轻代收集中就可以被回收,而不必等到多级并发收集的最后清除阶段再回收。

一次fullGc也可以回收完全释放的巨型分区。

混合收集:

混合收集包括收集新生代垃圾老年代垃圾。 为了识别出垃圾最多的老年代分区

-XX:InitiatingHeapOccupancyPercent来设置触发老年代的垃圾收集。默认为45%。

百分比是指占用整个堆空间的百分比,而在CMS GC中,百分比是占用老年代分区的比例。

并发标记结束之后,G1垃圾收集器会计算每个老年代分区的存活对象数。

在清除阶段G1垃圾收集器会根据老年代分区的“GC效率”定出它们的等级。这样 ,混合垃圾收集就可以开始了。

混合收集周期只能在达到IHOP并完成并发标记周期之后才能启动。

-XX:G1MixedGCCountTarget和 -XX:G1HeapWastePercent两个参数用于决定在一个混合收集周期中包含混合收集的总数。

XX:G1MixedGCCountTarget缺省值为8,是混合GC数量的目标选项,意义是给标记周期结束之后所能启动混合收集的数目设置一个物理限制。

垃圾收集器根据 混合GC数量目标值,对可被收集的候选老年代分区总数进行平均拆分,并将结果设置为每次混合收集所要回收老年代分区的最小数量

每次混合收集的老年代CSet最伺候数量 = 混合收集周期将回收的候选老年代分区总数/ G1MixedGCCountTarget

-XX:G1HeapWastePercent,在JDK8u45中缺省为Java堆总大小的5%,这个参数对于控制在一次混合收集周期中回收的老年代分区数有重要作用。

对于每次混合收集暂停,G1垃圾收集器根据那些能被回收的死亡对象的空间计算可被 回收空间的大小。一旦G1垃圾收集器达到堆废物阈值百分比,G1垃圾收集器就不会再启动新的混合收集,同时混合收集周期也将结束。设置堆废物百分比本质是你愿意浪费一定数量的堆空间,通过综们可以有效提升 混合收集周期的效率 。

因此每个混合收集周期中所包含的混合收集数可以通过两方面来控制:一个是每次混合收集暂停的老年代CSet最小数量,和堆废物百分比。

 

收集集合及其重要性

在任一垃圾收集暂停中,CSet里所有的分区都会被释放。CSet就是一系列的分区的集合,也是在垃圾收集暂停过程中被回收的目标。这些候选分区里的所有存活对象,在收集过程中都会被转移,然后分区会被释放回空闲分区队列中。

另外混合收集,不光会把所有的年轻代分区添加到它的CSet中,还会添加一部分老年代候选分区,基于它们的GC效率。

-XX:G1MixedGCLiveThresholdPercent:低于这个活跃度阈值的老年代分区都会被包含在混合收集的CSet中。默认值为85%

-XX:G1OldCSetRegionThresholdPercent:默认为10%, 这个参数设置了每个混合收集暂停所能收集的老年代分区数量的上限。这个值依赖于JVM进程可用的JAVA堆的总大小,同时也被描述为JAVA堆总尺寸的百分比。

 

已记忆集合及其重要性

       为了更好地使用独立收集,许多垃圾收集器为它们各个代维护了已记忆集合RSet。RSet是一个数据结构,它维护并跟踪外部对收集拥有单元的引用,于是就没有必要通过扫描整个堆来获取此类信息。当G1垃圾收集器执行一个stop-the-world式的收集,它会扫描包含在CSet中的分区的RSet。一旦分区中的存活对象被移动,则对这些对象的引用也要更新。

       对于G1垃圾收集器来说,在单独年轻代收集或是混合收集过程中,年轻代通常是整体回收,这样就无需再跟踪那些指向的对存活在年轻代中的引用。

  • 老年代对年轻化的引用。G1垃圾收集器维护了老年代分区指向了年轻代分区的指针。这个年轻代分区被描述为拥有RSet,因此这个分区可以被称为RSet拥有分区。
  • 老年代对老年代的引用。老年代不同分区的指针将被维护在老年拥有Rest分区中。

不同的分区的受欢迎程度是不同的。

RSet的密度:稀少、细粒度、粗粒度。“受欢迎分区”在容纳指针时可能会采用粗粒度方式。这将影响到这些分区的RSet扫描时间。这3种粒度水平都有一个PRT (per-regin-table),提供给所有特殊RSet的抽象容器。

G1垃圾收集器的分区内部会拆成多个块,对G1垃圾收集器来说,堆内存块最小可用粒度是512字节,也可以称为一个”卡片“。

全局卡片表Global Card Table维护着所有的卡片。

当一个指针产生对RSet的拥有分区的引用,包含这个指针的卡片会被记录在PRT中。一个稀少PRT本质上是这些卡片索引的哈希表。这种简单的实现方式使得垃圾收集有更快的扫描时间。

另外细粒度PRT和粗粒度位图采用不同的处理方式。

 

Chapter3: Garbage First垃圾收集器性能优化

年轻代收集暂停的串行阶段,可以是多线程的。可用-XX:ParallelGCThreads选项的值来确定垃圾收集器工作线程数目。