Garbage-First(后文简称G1)收集器是当今收集器技术发展的最前沿成果,在Sun公司给出的JDK RoadMap里面,它被视做JDK 7的HotSpot VM 的一项重要进化特征。从JDK 6u14中开始就有Early Access版本的G1收集器供开发人员实验、试用,虽然在JDK 7正式版发布时,G1收集器仍然没有摆脱“Experimental”的标签,可是相信不久后将会有一个成熟的商用版本跟随某个JDK 7的更新包发布出来。 html
因版面篇幅限制,笔者行文过程当中假设读者对HotSpot其余收集器(例如CMS)及相关JVM内存模型已有基本的了解,涉及到基础概念时,没有再延伸介绍,读者可参考相关资料。 java
G1是一款面向服务端应用的垃圾收集器,Sun(Oracle)赋予它的使命是(在比较长期的)将来能够替换掉JDK 5中发布的CMS(Concurrent Mark Sweep)收集器,与其余GC收集器相比,G1具有以下特色: 算法
在G1以前的其余收集器进行收集的范围都是整个新生代或者老年代,而G1再也不是这样。使用G1收集器时,Java堆的内存布局与就与其余收集器有很大差异,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,它们都是一部分Region(不须要连续)的集合。 服务器
G1收集器之因此能创建可预测的停顿时间模型,是由于它能够有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所得到的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据容许的收集时间,优先回价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划份内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内获能够获取尽量高的收集效率。 网络
G1把内存“化整为零”的思路,理解起来彷佛很容易理解,但其中的实现细节却远远没有现象中简单,不然也不会从04年Sun实验室发表第一篇G1的论文拖至今将近8年时间都尚未开发出G1的商用版。笔者举个一个细节为例:把Java堆分为多个Region后,垃圾收集是否就真的能以Region为单位进行了?听起来瓜熟蒂落,再仔细想一想就很容易发现问题所在:Region不多是孤立的。一个对象分配在某个Region中,它并不是只能被本Region中的其余对象引用,而是能够与整个Java堆任意的对象发生引用关系。那在作可达性断定肯定对象是否存活的时候,岂不是还得扫描整个Java堆才能保障准确性?这个问题其实并不是在G1中才有,只是在G1中更加突出了而已。在之前的分代收集中,新生代的规模通常都比老年代要小许多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象也面临过相同的问题,若是回收新生代时也不得不一样时扫描老年代的话,Minor GC的效率可能降低很多。。 架构
在G1收集器中Region之间的对象引用以及其余收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每一个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操做时,会产生一个Write Barrier暂时中断写操做,检查Reference引用的对象是否处于不一样的Region之中(在分代的例子中就是检查引是否老年代中的对象引用了新生代中的对象),若是是,便经过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,GC根节点的枚举范围中加入Remembered Set便可保证不对全堆扫描也不会有遗漏。 并发
若是不计算维护Remembered Set的操做,G1收集器的运做大体可划分为如下几个步骤: oracle
对CMS收集器运做过程熟悉的读者,必定已经发现G1的前几个步骤的运做过程和CMS有不少类似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,而且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中建立新对象,这阶段须要停顿线程,但耗时很短。并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。而最终标记阶段则是为了修正并发标记期间,因用户程序继续运做而致使标记产生变更的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段须要把Remembered Set Logs的数据合并到Remembered Set中,这阶段须要停顿线程,可是可并行执行。最后筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所指望的GC停顿时间来制定回收计划,从Sun透露出来的信息来看,这个阶段其实也能够作到与用户程序一块儿并发执行,可是由于只回收一部分Region,时间是用户可控制的,并且停顿用户线程将大幅提升收集效率。经过图1能够比较清楚地看到G1收集器的运做步骤中并发和须要停顿的阶段。 框架
图1 G1收集器运行示意图
因为目前尚未成熟的版本,G1收集器几乎能够说尚未通过实际应用的考验,网上关于G1收集器的性能测试很是贫乏,笔者没有Google到有关的生产环境下的性能测试报告。强调“生产环境下的测试报告”是由于对于垃圾收集器来讲,仅仅经过简单的Java代码写个Microbenchmark程序来建立、移除Java对象,再用-XX:+PrintGCDetails等参数来查看GC日志是很难作到准衡量其性能的(为什么Microbenchmark的测试结果不许确可参见笔者这篇博客:http://icyfenix.iteye.com/blog/1110279)。所以关于G1收集器的性能部分,笔者引用了Sun实验室的论文《Garbage-First Garbage Collection》其中一段测试数据,以及一段在StackOverfall.com上同行们对G1在真实生产环境下的性能分享讨论。
Sun给出的Benchmark的执行硬件为Sun V880服务器(8×750MHz UltraSPARC III CPU、32G内存、Solaris 10操做系统)。执行软件有两个,分别为SPECjbb(模拟商业数据库应用,堆中存活对象约为165MB,结果反映吐量和最长事务处理时间)和telco(模拟电话应答服务应用,堆中存活对象约为100MB,结果反映系统能支持的最大吞吐量)。为了便于对比,还收集了一组使用ParNew+CMS收集器的测试数据。全部测试都配置为与CPU数量相同的8条GC线程。
在反应停顿时间的软实时目标(Soft Real-Time Goal)测试中,横向是两个测试软件的时间片断配置,单位是毫秒,以(X/Y)的形式表示,表明在Y毫秒内最大容许GC时间为X毫秒(对于CMS收集器,没法直接指定这个目标,经过调整分代大小的方式大体模拟)。纵向是两个软件在对应配置和不一样的Java堆容量下的测试结果,V%、avgV%和wV%分别表明的含义为:
测试结果以下表所示:
表1:软实时目标测试结果
Benchmark / confguration |
Soft real-time goal compliance statistics by Heap Size |
|||||||||
V% |
avgV% |
wV% |
V% |
avgV% |
wV% |
V% |
avgV% |
wV% |
||
SPECjbb |
512M |
640M |
768M |
|||||||
G1 |
(100/200) |
4.29% |
36.40% |
100.00% |
1.73% |
12.83% |
63.31% |
1.68% |
10.94% |
69.67% |
G1 |
(150/300) |
1.20% |
5.95% |
15.29% |
1.51% |
4.01% |
20.80% |
1.78% |
3.38% |
8.96% |
G1 |
(150/450) |
1.63% |
4.40% |
14.32% |
3.14% |
2.34% |
6.53% |
1.23% |
1.53% |
3.28% |
G1 |
(150/600) |
2.63% |
2.90% |
5.38% |
3.66% |
2.45% |
8.39% |
2.09% |
2.54% |
8.65% |
G1 |
(200/800) |
0.00% |
0.00% |
0.00% |
0.34% |
0.72% |
0.72% |
0.00% |
0.00% |
0.00% |
CMS |
(150/450) |
23.93% |
82.14% |
100.00% |
13.44% |
67.72% |
100.00% |
5.72% |
28.19% |
100.00% |
Telco |
384M |
512M |
640M |
|||||||
G1 |
(50/100) |
0.34% |
8.92% |
35.48% |
0.16% |
9.09% |
48.08% |
0.11% |
12.10% |
38.57% |
G1 |
(75/150) |
0.08% |
11.90% |
19.99% |
0.08% |
5.60% |
7.47% |
0.19% |
3.81% |
9.15% |
G1 |
(75/225) |
0.44% |
2.90% |
10.45% |
0.15% |
3.31% |
3.74% |
0.50% |
1.04% |
2.07% |
G1 |
(75/300) |
0.65% |
2.55% |
8.76% |
0.42% |
0.57% |
1.07% |
0.63% |
1.07% |
2.91% |
G1 |
(100/400) |
0.57% |
1.79% |
6.04% |
0.29% |
0.37% |
0.54% |
0.44% |
1.52% |
2.73% |
CMS |
(75/225) |
0.78% |
35.05% |
100.00% |
0.54% |
32.83% |
100.00% |
0.60% |
26.39% |
100.00% |
从上面结果可见,对于telco来讲,软实时目标失败的几率控制在0.5%~0.7%之间,SPECjbb就要差一些,但也控制在2%~5%之间,几率随着(X/Y)的比值减少而增长。另外一方面,失败时超出容许GC时间的比值随着总时间片断增长而变小(分母变大了嘛),在(100/200)、512MB的配置下,G1收集器出现了某些时间片断下100%时间在进行GC的最坏状况。而相比之下,CMS收集器的测试结果对比之下就要差不少,3种Java堆容量下都出现了100%时间进行GC的状况,
在吞吐量测试中,测试数据取3次SPECjbb和15次telco的平均结果。在SPECjbb的应用下,各类配置下的G1收集器表现出了一致的行为,吞吐量看起来只与容许最大GC时间成正比关系,而在telco的应用中,不一样配置对吞吐量的影响则显得很微弱。与CMS收集器的吞吐量对比能够看到,在SPECjbb测试中,在堆容量超过768M时,CMS收集器有5%~10%的优点,而在telco测试中CMS的优点则要小一些,只有3%~4%左右。
图2:吞吐量测试结果
在更大规模的生产环境下,笔者引用一段在StackOverfall.com上看到的经验分享:“我在一个真实的、较大规模的应用程序中使用过G1:大约分配有60~70GB内存,存活对象大约在20~50GB之间。服务器运行Linux操做系统,JDK版本为6u22。G1与PS/PS Old相比,最大的好处是停顿时间更加可控、可预测,若是我在PS中设置一个很低的最大容许GC时间,譬如指望50毫秒内完成GC(-XX:MaxGCPauseMillis=50),但在65GB的Java堆下有可能获得的直接结果是一次长达30秒至2分钟的漫长的Stop-The-World过程;而G1与CMS相比,它们都立足于低停顿时间,CMS仍然是我如今的选择,可是随着Oracle对G1 的持续改进,我相信G1会是最终的胜利者。若是你如今采用的收集器没有出现问题,那就没有任何理由如今去选择G1,若是你的应用追求低停顿,那G1如今已经能够做为一个可尝试的选择,若是你的应用追求吞吐量,那G1并不会为你带来什么特别的好处。”
在这节笔者引了两段别人的测试结果、经验后,对于G1给出一个本身的建议:直到如今为止尚未一款“最好的”收集器出现,更加没有“万能的”收集器,因此咱们选择的只是对具体应用最合适的收集器。对于不一样的硬件环境、不一样的软件应用、不一样的参数配置、不一样的调优目标都会对调优时的收集器选择产生影响,选择适合的收集器,除了理论和别人的数据经验做为指导外,最终仍是应当创建在本身应用的实际测试之上,别人的测试,大可抱着“至于你信不信,反正我本身没测以前是不信的”的态度。
感谢张凯峰对本文的审校。