【Java 性能伴侣】第一章 G1概览

【前言】在2016年为了了解G1找了一本不错的书,Java performance companion,这本书发表于16年5月,市面上没有中文版。我打算把全书翻译一下,通过翻译来彻底学习一下本书的内容,如有翻译上的疑问可以留言,相互探讨。在此感谢您的点击阅读。



第一章 G1概览

本章介绍了垃圾优先(Gabage First)G1的垃圾回收策略(GC),以及HotSpot虚拟机垃圾回收方面的历史,还有HotSpot采用G1背后原因。本章假定读者属性垃圾回收的概念,如青年代,老年代,压缩。可以从《Java Performance》的第三章”JVM概览"学到更多关于这些概念的知识[1]。

       串行GC是HotSpot在1999年引入第一种垃圾回收器,它是作为JDK1.3.1的一部分出现。平行收集器和并发标记擦除收集器在2002以JDK1.4.2的一部分加入。这三种收集器对应着三种重要的GC应用阶段(phase):“最小化内存的占用与并发开销”,“最大化应用吞吐量”,和“最小化GC暂停时间”。有人会问,“我们为什么需要一个新的回收器,比如G1?”。在回答这个问题之前,我们来明确一些在对比各垃圾回收器时用到的术语。之后我们简要概览HotSpot的四个垃圾回收器,包括G1,并指出G1与其他回收器的不同之处

 

术语

在本节中,我们定义术语平行(parallel),停止世界(stop-the-world)和并发(concurrent)。术语平行意味着多线程的垃圾回收策略。当一个GC活动事件被描述成平行事件的时候,实际是采用了多线程完成该事件。当垃圾收集器被描述成平行的时候,垃圾收集器是采用多线程进行垃圾回收的。在HotSpot垃圾收集器中,几乎所有多线程的GC选项都由JVM内部线程来处理。而G1收集器是一个主要的例外,它会使用应用线程在后台完成垃圾回收的工作。更多详情在第二章“深入垃圾优先垃圾回收器”,和第三章“理解垃圾优先垃圾回收器性能”。

       术语停止世界意味着在GC事件当中,所有的Java应用线程都被停止。一个停止世界的垃圾回收器是一个在它进行垃圾回收操作时停止所有Java应用线程的回收器。一个GC阶段或者事件被描述成停止世界的时候,意味着在这个GC阶段或GC事件下,所有的Java应用线程都被停止了。

       术语并发意味着垃圾回收活动在Java应用线程活动的情况下也会发生。一个并发GC阶段或事件意味着GC阶段或事件和Java应用同时发生。

       一个垃圾回收器或许可以用这三个术语中任意一个或组合的多个术语来描述。例如,一个平行并发收集器,是多线程(平行部分)的执行期和应用程序相同的(并发)垃圾回收器。

 

平行GC

平行GC是一个平行的停止世界收集器,这意味着当GC发送的时候,它会停止所有的应用线程并用多个线程实现GC。这样GC的工作不会被打断从而非常有效的进行。这种方式通常是最有效的方法在应用工作的时候最小化GC工作的总时间开销。然而,由GC引入的每个单独的Java应用程序的暂停可能会相当长(注:这里是说应用频繁GC?)

       年轻代和老年代在平行GC中是平行收集的并且都会停止世界。老年代收集器还会实施压缩操作。压缩会移动对象到更紧凑的位置来消除对象之间的空间浪费,用以优化堆布局。然而压缩会花费相当可观的一段时间,这是通常是以Java堆尺寸、老年代中存活对象的数量、存活对象的尺寸为参数的函数。

       在平行GC引入HotSpot的时候,只有年轻代使用平行停止世界收集器。老年代收集器使用一个单线程停止世界的收集器。那时,在第一次引入平行GC的时候,在配置中用以开启平行GC功能的HotSpot命令行选项是-XX:+UseParallelGC

       在平行GC引入的时候,虚拟机服务中最常见的应用模式是吞吐量优化,因而平行GC变成了HotSpot默认收集器。而且还有,那时候大部分Java堆的尺寸在512MB到2GB之间,使得平行GC的停止世界的时间相对短,即便是单线程停止世界的收集器。并且在那个时候,延迟请求的要求比今天要宽松。对于Web应用来说,可以忍受由GC引入的1秒延迟,甚至可以到3、5秒。

       随着Java堆尺寸和老年代存活对象的增长,收集老年代对象的时间开销越来越长。同时,硬件进化使得可用的硬件线程增多。结果,平行GC通过增加多线程的老年代收集器结合多线程年轻代收集器的使用得到了增强。这种增强的平行GC收集器降低了收集和压缩堆的时间开销。

       增强平行GC在Java6更新中发布。它用新的命令行选项打开-XX:+UseParallelOldGC。当-XX:+UseParallelOldGC打开的时候,平行年轻代收集器也被打开了。这就是我们今天在HotSpot中看到的平行GC收集器,一个多线程停止世界年轻代收集器伴随着多线程停止世界老年代收集器。

要点
在Java7更新版release4(也被称作Java 7u4,或者JDK 7u4),-XX:+UseParallelOldGC变成了默认的GC选项并且成为平行GC的通用选项。在Java 7u4中,指定-XX:+UseParallelGC通用会打开-XX:+UseParallelOldGC选项,同时打开-XX:+UseParallelOldGC也会打开-XX:+UseParallelGC(注:这两个选项是一个功能)

平行GC在以下阶段中是一个不错的选择

1.       应用吞吐量比系统延迟重要的多。一个批处理应用是一个很好的例子,因为它不需要交互。当你启动一个批处理业务时,你期望他尽快的完成。

2.       如果最坏情况的应用延迟要求可以满足,平行GC会提供最好的吞吐量。最坏的延迟要求包括最坏的暂停时间长度和暂停频度。举个例子,应用可能会要求“每两小时中超过500毫秒延迟的暂停不能超过一次,所有的暂停不能超过3秒”。一个有足够小的存活数据尺寸的交互应用,如果其平行GC的全GC事件能够满足或超出应用的最坏GC引入延迟要求,则对这个应用来说,使用平行GC是一个很好的例子。然而,既然存活对象数量和Java堆尺寸非常相关,能够采用这种GC策略的的应用非常有限。


图1.1使用平行GC时,Java应用线程如何被GC线程中断

平行GC在满足上述条件的应用上工作的很好。对于不满足这些要求的应用,暂停时间会变得极其的长,因为全GC必须标记整个Java堆并且压缩整个老年代空间。结果导致,暂停时间随着Java堆的增长而变得更久。

       图1.1画出了Java应用线程(灰色箭头)如何被停止,以及GC线程(黑色箭头)替代应用线程进行垃圾回收的工作。在这个图里面有8个平行的GC线程和8个Java应用线程,然而在大部分应用中应用线程经常超出GC线程的个数,特别是应用线程有可能处于空闲态。当发生了GC,所有的应用线程被停止,在整个GC期间,会启用多个GC线程实施GC的工作。

 

串行GC

串行GC和平行GC非常相似,除了它是在一个线程中完成所有的事情。这个单线程方案令简化的GC实现成为可能,它几乎不需要外部运行时的数据结构。它是内存占用(memory footprint)最低的HotSpot收集器。串行GC所面临的挑战同平行GC一样。暂停时间可能很长,并且会随着Java堆尺寸、存活数据个数的增长而近乎线性地增长。并且,在串行GC中更容易出现长时间GC暂停,因为它是在单线程中进行GC。

图1.2使用串行GC时Java应用线程如何被GC单线程中断

       因为低内存占用,串行GC是HotSpot客户端虚拟机的默认GC策略。它同时还定位于嵌入式设备的应用。串行GC可以通过-XX:+UseSerialGC的HotSpot命令行选项明确指定使用串行GC。图1.2画出了在一个8个应用线程的应用上,Java应用线程(灰色箭头)如何被停止,GC线程(黑色箭头)如何接替运行权来进行垃圾回收工作。由于它是一个单线程,串行GC在大部分情况下会产生比平行GC更长的GC事件执行时间,因为平行GC可以把GC工作分摊到多个线程上。


并发标记擦除(CMS)GC

CMS GC的开发是为了满足比串行GC、平行GC更少的最坏暂停时间,并且能够牺牲一部分吞吐量消除或极大降低GC暂停的长度这种需求。

       在CMS GC中,年轻代垃圾回收和平行GC相似。它们是平行停止世界,意味着Java应用线程在年轻代垃圾回收的时候被暂停,垃圾回收工作由多线程来完成。记住,你可以将CMS配置成使用单线程进行年轻代垃圾回收,然而这个选项在Java8中已经不推荐,并且将在Java9中删除。

       平行GC和CMS GC的主要不同在于老年代回收。对于CMS老年代回收尝试避免长时间暂停应用线程。为了达到这个目的,CMS老年代收集器在应用线程执行期完成它的大部分工作,只有少数相应短时的GC同步暂停。CMS经常被称为尽力并发,因为在老年代回收的阶段下会有一些停止应用线程的情况。举个初始标记和重新标记阶段的例子。在CMS一开始的实现中,初始标记和重新标记阶段都是单线程的,但是之后它们都被增强为多线程的了。指出多线程的初始标记和重新标记的HotSpot命令行选项分别是-XX:+CMSParallelInitialMarkEnable-XX:+CMSParallelRemarkEnable。它们在配置了CMS GC的时候会自动配置上,开启CMS GC的命令行选项是 -XX:+UseConcurrentMarkSweepGC

       有可能,并且非常可能,在老年代回收的时候发生年轻代回收。当发生这种阶段的时候,老年代并发收集会被年轻代收集中断掉,并且在年轻代收集完成后被立即唤醒。CMS GC年轻代收集器通常被称为ParNew。

       图1.3展示了Java应用线程(灰色箭头)被年轻代GC线程(黑色箭头)停止而对CMS初始标记和重新标记,以及进行老年代GC停止世界阶段(同样黑色箭头)。CMS中一个老年代收集起始于停止世界初始标记阶段。一旦初始标记完成,在Java应用线程被允许执行的时候会并发地使用CMS并发线程开始标记的阶段。在图1.3中,并发标记线程是在“Marking/Pre-cleaning”标签下的两个黑色箭头,一个在另一个的上方。一旦并发标记结束,CMS线程将执行并发预清除,如图中“Marking/Pre-cleaning”标签下的两个更短的黑箭头。记住,如果有足够可用的硬件线程,CMS线程执行的开销不会像Java应用线程那样有效。然而如果硬件线程已经饱和或者被高度使用,CMS线程将同Java应用线程竞争CPU循环周期。一旦并发预清理完成,停止世界重标记阶段开始。重新标记阶段标记的对象在初始化标记、并发标记和并发预清除阶段之后可能会消失。在重新标记阶段完成之后,并发擦除开始,它会清除所有的死对象占用的空间。

       CMS GC面临的一个挑战是为它调优来使得Java应用程序耗尽Java堆空间之前能够完成并发工作。因此,CMS采用的一个小把戏是找出正确的时间点开始并发工作。CMS采用的一个普通并发方法是申请比平行GC多百分之10到20的Java堆空间来处理同一个应用。这多出来的空间是为缩短GC中断时间付出的代价。

图1.3使用CMS GC时GC线程如何影响到Java应用线程

CMS GC面临的另一个挑战是它如何处理老年代的碎片化。当老年代对象之间的空间变得足够小,或者没有任何一个年轻代传出的对象可以填充到老年代对象间的任意空间的时候,此时会产生碎片。CMS并发收集周期不会进行压缩,即使是递增或部分压缩。当寻找可用的洞(注:老年代对象之间的空间)失败的时候,CMS会调用串行GC进行全收集,这将导致很长的暂停时间。另一个不幸的挑战伴随着CMS碎片是碎片的不可预测性。许多应用运行中根本没有发生过因为老年代碎片导致的全GC,而其他很多应用却经常经历全GC。

       对CMS GC的调优(tuning)能够延迟碎片化,比如让应用程序避免分配大对象。调优是一个需要大量经验的非凡任务。对应用做出改变来避免碎片化同样具有挑战性。

 

收集器的摘要

到目前为止描述的所有的收集器有相同的特征。一个是老年代收集器为了应对常用操作如标记、擦除、压缩,必须扫描整个老年代。这意味着进行GC操作的时间消耗随着Java堆大小线性的变化。另一个是收集器必须决定年轻代和老年代应该被放置在虚拟地址空间的哪个位置,因为年轻代和老年代是分别在连续内存块上存储的。

 

垃圾优先(G1)GC

G1垃圾回收器通过另外的途径解决了平行、并发、CMS GC中存在的许多缺点。相似的,老年代也是一片区域。在JVM启动的时候不再需要指定哪些区域是年轻代、哪些区域是老年代。实际上,G1的普通运行状态是随着时间推移,映射到G1区域的虚拟内存在各个代(注:年轻代、老年代、其他代)之间来回移动。一个G1区域在年轻代收集之后可以被指定为年轻代和later,变成可以用于各处的状态,因为年轻代区域是被完全回收到未用区域的。

       在本章剩下的篇幅里,属于可用区域用来标记未使用并且可用于G1的区域。一个可用区域可以被用于或者指定为年轻代或老年代区域。有可能在年轻代收集之后,被回收的年轻代区域在之后的某个时间被用于老年代区域。相似的,老年代收集之后,老年代区域可以用做年轻代区域。

       G1年轻代收集器是平行停止世界收集器。如之前提到的,平行停止世界收集器在垃圾回收器线程执行时暂停所有的Java应用线程,并且GC工作在多个线程中进行。同其他HotSpot垃圾回收器一样,当年轻代回收发生之后,整个年轻代将被回收。

       G1老年代收集器和其他的HotSpot回收器明显不同。G1老年代收集器不要求收集整个老年代来释放所有的老年代空间。相应的,一次只回收老年代区域的一个子集。另外还有,这个老年代的子集是和年轻代回收一起进行回收的。

要点
用于描述老年代区域子集同年轻代一同回收的行为被称作合成GC(mixed GC)。因此,一个合成GC里,所有的年轻代区域都被回收,同时回收老年代区域的一个子集。也就是说,一个合成GC是一个年轻代和老年代区域回收的混合体。
       

       同CMS GC相似,有一个失败保险来收集并压缩整个的老年代,用于例如老年代空间耗尽这样一个可怕的场景。

       一个G1老年代收集过程,不考虑收集的失败保险类型,是一个阶段的集合,这些阶段当中有些事平行停止世界而有一些是平行并发。这就是说,一些阶段是多线程且停止所有应用线程,而其他的是多线程且和应用线程同时执行。第二章和第三章将提供这些阶段的详细内容。

       G1发起老年代收集会在Java堆占用率超过门限时发生。有一点非常重要请记住,G1的Java堆占用门限是用老年代占用量比上整个堆容量。熟悉CMS GC的读者应该记得CMS GC发起一个老年代回收所使用的占用量门限只考虑老年代空间。在G1中,一旦达到或超过堆占用门限,平行停止世界初始标记阶段就被调度去执行。

       初始标记阶段同下一次年轻代GC一起执行。一旦初始化标记阶段完成,一个并发多线程标记阶段被触发,它将标记所有老年代中的存活对象。当并发标记阶段完成,一个平行停止世界重标记阶段被调度去标记一些对象,这些对象是在并发标记阶段中由应用程序并行执行所产生的。在重标记结束时,G1对老年代完成了全标记。如何此时碰巧老年代中没有任何存活对象,它们被重新回收而不用在下一个并发循环中进行更多的GC工作,这就是清除阶段。

       同样在重新标记结束的时候,G1可以识别到可以回收的最佳老年代集合。

要点
在垃圾回收中被收集的区域集合被称为收集集合(CSet)

被选中到收集集合中的区域,基于有多少空间可以被释放以及G1暂停时间的目标。在收集集合被识别出来后,G1在之后的一些年轻代收集过程中调度出一些收集集合中的GC收集区域。这就是说,经过之后的一些年轻代GC,一部分老年代被收集用作年轻代。这就是前面提到的混合GC类型的垃圾回收。

       在G1中,每个被垃圾回收的区域,不考虑年轻待或老年代,都把它自己的存活对象疏散到了可用区域。一旦存活对象被疏散,被回收的年轻代或老年代区域变成了可用区域。

       从老年代疏散存活对象到可用区域的一个魅力点在于,疏散的对象在虚拟内存空间中作为要终结的对象紧邻着排列。这样在对象之间就没有空白空间碎片。G1有效地使用部分压缩来压缩老年代。对比CMS GC,平行GC和串行GC都需要做全GC来压缩老年代而这个压缩需要扫描整个老年代空间。

       既然G1在每个区域上实施GC操作,它就很适合大的Java堆。GC工作的数量可以被限制到一个小的区域集合即便Java堆的尺寸非常大。

       G1对暂停时间的最大贡献在于年轻代和混合回收,因而它的一个设计目标是允许用户设置一个GC暂停时间。G1通过调整Java堆尺寸来尝试满足预设的GC停止时间。G1会基于预设暂停时间来自动调整年轻代和总的堆尺寸。暂停预设时间越低,年轻代越小同时Java堆越大,使得老年代越大。

       一个G1设计目标是限制定标--预设最大Java堆尺寸和指定GC暂停时间目标。另一方面,G1被设计成通过内部启发式的决策实现自动定标。在写本书的过程中,G1启发算法是HotSpot GC开发中最活跃的部分。同样在写本书的时候,G1在某些情况下或许需要附加的定标,不过建立好的启发算法的先决条件已经出现并且看起来很有希望。了解更多关于如何定标G1,参考第三章。

       综上,G1通过把Java堆分割成区域,使得它在大Java堆应用中比其他垃圾回收策略的弹性更强。G1借助部分压缩处理Java堆碎片,并且几乎都是在多线程下进行操作。

       在写本书的时候,G1主要定位于大的Java堆和合理的低暂停时间,以及正在使用CMS GC的应用。当前有计划使用G1实现吞吐量场景,但是对于高吞吐量高中断忍受度的应用,平行GC是当前更好的选择。


G1设计

如之前提到的,G1把Java堆划分成区域。区域的尺寸可以可以依据堆大小有所不同,不过必须是2的幂函数并且至少为1MB最大32MB。或许区域尺寸可以是1,2,4,8,16,32MB。所有的区域有相同的尺寸,并且它们的尺寸在JVM执行期间不会变化。区域尺寸的技术基于初始和最大Java堆尺寸的均值,这样就有将近2000个这样尺寸的区域。举个例子,16GB的Java堆命令行参数是 -Xmx16g -Xms16g(程序可用堆16GB,程序初始化堆16G),G1将会选择16GB/2000 = 8MB的区域。

       如果初始和最大Java堆尺寸相差很大,或者堆尺寸非常大,他可用拥有多于2000个的区域。同样如果堆尺寸非常小,可用使用小于2000个的区域。

       每个区域都有伴随的记忆集合(一个包含指向区域的指针的位置集合,简称RSet)。总的RSet尺寸有限但是也是可观的,因此区域的数量对HotSpot内存占用量有明显的影响。RSet的总尺寸严重依赖于应用行为。在低点,RSet开销大概有百分之1,在高点有百分之20的堆尺寸。

       一个特点区域在一定时间点只用于一个目的,但是当区域被回收,区域将被完全分散并释放来作为可用区域。

       G1中有一些区域的类型。可用区域是当前未用的区域。伊甸区域(Eden regions)组成了年轻代伊甸空间,同时幸存(survivor)区域组成了年轻代幸存空间。所有伊甸和幸存区域的集合组成年轻代。伊甸或幸存区域的数量在一个GC到下一个GC之间可用发生变化,这个GC可以是年轻代、混合、全GC。老年代区域组成大部分的老年代。最终,极大的区域被看做老年代的一部分,并且这些区域包含了百分之50甚至更多的区域。直到JDK8u40大量的区域被回收到老年代,但是在JDK8u40中,特定大量区域被收集为年轻代。这些在本章之后会对大量区域进行详细介绍。

       事实上区域可以被任意使用,这意味着不需要将堆划分成连续的年轻代和老年代的段。替代方案是,G1启发估计年轻代会包含有多少个区域和估计有多少区域在给定GC暂停时间内依然被回收。随着应用开始分配对象,G1选择可用的区域,将其分配为伊甸区,开始为Java应用线程分发伊甸区中的内存块。一旦区域被填满,另一个空区域被分配为伊甸区。这个过程一直保持进行直到达到伊甸区最大数,此时会触发年轻代GC。

       在年轻代GC中,所有的年轻代区域,伊甸区和幸存区,都被回收。所有这些区域中的存活对象要么被分发到新的幸存区要么被分发到老年区。在当前分发目标区域被填满的时候,这些可用的区域会被标记成幸存区或者如果需要的话被标记成老年区。

       在GC之后,如果老年区占用空间达到或超过初始的堆占用阈值,G1开始老年代回收。堆占用阈值通过命令行配置-XX:InitiatingHeapOccupancyPercent,默认值是百分之45的堆空间。

       G1可用回收早些时候回收老年代区域,只要标记阶段显示老年代区域没有存活对象了。这些区域被添加到可用集中。包含存活对象的老区域被调度到将被混合的集合里。

       G1使用多并发标记线程。标记线程会在很急促的情况下尝试尽量少的偷取应用程序线程的CPU时间片来工作。标记线程尽力在给定的时隙中做足够多的事情,然后停止工作一段时间使得Java应用线程继续工作。


极大对象

G1专门针对大对象分配进行处理,大对象也被G1称作极大对象(humongous objects)。如之前提到的,极大对象是指超过区域百分之50及以上尺寸的对象。这个尺寸包括Java对象头(注:虚拟机中Java对象的头)。对象头的尺寸在32位和64位HotSpot虚拟机中是不一样的。指定虚拟机中的指定对象的头尺寸可以通过Java对象布局工具(Java Object Layout Tool)获得,即JOL。写本书的时候,JOL可以在互联网上找到[2]。

       当分配极大对象的时候,G1定位到一个连续可用区域集合,这个集合的总容量应该能够容纳下该极大对象。集合中的第一个区域被标记为“极大开始”(humongous start)区域,而其他区域被标记成“极大连续”(humongouscontinues)区域。如果没有足够的连续区域,G1将开始执行全GC来压缩Java堆。

       极大区域被认为是老年代,不过它们(一起)只包含一个对象。这个属性允许G1在并发标记阶段探测到极大对象不存活时倾向于回收极大区域。发生这个回收的时候,所有包含同一个极大对象的区域一起被回收。

       对G1潜在的挑战是,短期存活的极大对象可能不会被回收,直到对这个对象没有任何引用之后,还要过一段时间才被回收。JDK8u40实现了一个方法来,在某种情况下,在年轻代回收过程中回收极大对象。在使用G1的时候,避免频繁的分配极大对象可以显著达到应用性能设计目标。在JDK8u40中的增强实现对含有大量短期存活的极大对象的应用可能会有帮助,但它并不能成为解决方案。


全垃圾回收

在G1中的全GC使用了和串行GC相同的算法实现垃圾回收。当发生全GC,Java堆将被全部压缩。这保证了向系统提供最大限度的可用内存空间。有一点非常重要需要记住,G1的全GC是单线程的,它会导致及其长的暂停时间。同样G1是设计成不必进行全GC的。G1被设计成满足不需要全GC就能满足应用性能的设计目标的,它可用通过调优来实现根本不需要全GC。


并发循环

一个G1的并发循环包含多个活动阶段:初始标记,并发根区域扫描,并发标记,重新标记,和清除。并发循环的开始阶段是初始标记,终止阶段是清除。除过清除阶段,所有阶段都可以被认为是“标记存活对象图表”的一部分。

       初始标记的目的是扫描收集所有的GC根。根是对象图表的开始,从应用线程收集根,需要停止应用线程;这样初始标记阶段是一个停止世界的。在G1中,初始标记是年轻代GC暂停的一部分,因为毕竟年轻代GC需要收集所有的根。

       标记操作必须同样扫描跟踪幸存区域中的对象的所有引用。这是并发根区域扫描阶段所做的工作。在这个阶段里,允许Java线程运行,所以不会有线程暂停发送。唯一的限制是扫描必须在下一次GC开始之前完成。这是因为新的GC会收集到新的幸存区域集合,这些集合与初始标记的幸存区域集合是不同的。

       大部分标记工作在并发标记阶段完成。由多个线程协作标记存活对象图表。所有的Java线程会和并发标记线程同时运行,这就不会带来Java应用线程的暂停但是会导致应用吞吐量的减弱。

       在并发标记完成之后,需要另一个停止世界的操作来收尾所有的标记工作。这个阶段叫做“重新标记”阶段并且是一个非常短暂的停止世界过程。

       并发标记的最终阶段是清除阶段。在这个阶段里,被发现不含任何存活对象的区域被回收利用。这些区域不被包含在年轻代或混合GC中,因为它们不包含任何存活对象。它们被加入到了可用区域列表之中。

       这些阶段必须完成来找出哪些对象是存活的,这样可用为决定哪些区域应该包含在混合GC中提供信息。因为混合GC是G1中释放内存的主要机制,所以在G1用光可用区域之前完成标记阶段非常重要。如果标记阶段在可用区域耗尽之前没有完成,G1会回滚到全GC来释放内存。这很可靠但非常耗时。确保标记及时完成从而避免全GC是需要进行调优的,在第三章将设计这方面内容。


设计堆尺寸

在G1中Java堆尺寸是区域尺寸的倍数。处过这个限制(注:指整数倍的限制),G1可以让Java堆的尺寸在-Xms和-Xmx之间动态的摆动,这同其他HotSpot的GC一样。

       G1会增加Java堆尺寸是出于以下理由:

1.       尺寸的增加可以在基于全GC时对堆计算的时候发生。

2.       当年轻代或混合GC发生的时候,G1计算实施GC的时间开销和Java应用执行时间开销的比例。如果按照命令行设置的-XX:GCTimeRatio花费了太多时间,就会增加Java堆的尺寸。此时增加Java堆背后的逻辑是允许GC降低发生的频度使得GC耗时相比Java应用程序执行时间变短。G1中默认的-XX:GCTimeRatio为9。所有的其他HotSpot垃圾回收器这个值是99。GCTimeRatio的值越大,Java堆增长的越激烈。其他的HotSpot垃圾回收器增长堆的程度因此更为激烈,并且在默认情况下定位于花费较少时间在GC上面,相比Java应用程序执行时间来说。

3.       如果一个对象收集失败,即便GC完成,相比于回滚做全GC,G1会尝试增加Java堆来满足对象分配。

4.       如果极大对象无法找到足够的连续自由区域导致分配失败,G1会尝试扩展Java堆来获取更多的可用区域而不是做全GC。

5.       当一个GC请求新的区域向其中疏散对象,G1倾向于增加Java堆尺寸来获取新的区域而不是让GC失败并回滚到全GC来获得可用区域。


参考

[1] CharlieHunt and Binu John. Java Performance. Addison-Wesley, Upper Saddle River, NJ,2012. ISBN 978-0-13-714252-1.

[2]"Code Tools:jol." OpenJDK, circa 2014. http://openjdk.java.net/projects/code-tools/jol/.


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值