注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

断尘居

温柔的男人像海洋。

 
 
 
 
 

日志

 
 

解析JDK 7的Garbage-First收集器  

2011-11-14 01:55:34|  分类: JVM/ HotSpot |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
Garbage-First(后文简称G1)收集器是当今收集器技术发展的最前沿成果,在Sun公司给出的JDK RoadMap里面,它被视作JDK 7的HotSpot VM 的一项重要进化特征。从JDK 6u14中开始就有Early Access版本的G1收集器供开发人员实验、试用,虽然在JDK 7正式版发布时,G1收集器仍然没有摆脱“Experimental”的标签,但是相信不久后将会有一个成熟的商用版本跟随某个JDK 7的更新包发布出来。
  因版面篇幅限制,笔者行文过程中假设读者对HotSpot其他收集器(例如CMS)及相关JVM内存模型已有基本的了解,涉及到基础概念时,没有再延伸介绍,读者可参考相关资料。

G1收集器的特点
  G1是一款面向服务端应用的垃圾收集器,Sun(Oracle)赋予它的使命是(在比较长期的)未来可以替换掉JDK 5中发布的CMS(Concurrent Mark Sweep)收集器,与其他GC收集器相比,G1具备如下特点:
  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合:与CMS的“标记-清理”算法不同,G1从整体看来是基于“标记-整理”算法实现的收集器,从局部(两个Region之间) 上看是基于“复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运 行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对于CMS的另外一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预 测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的 垃圾收集器特征了。

实现思路
  在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收集器的运作大致可划分为以下几个步骤:
  • 初始标记(Initial Marking)
  • 并发标记(Concurrent Marking)
  • 最终标记(Final Marking)
  • 筛选回收(Live Data Counting and Evacuation)
  对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收集器的运作步骤中并发和需要停顿的阶段。

解析JDK 7的Garbage-First收集器 - 断尘伤痕 - 断尘居
图1 G1收集器运行示意图

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% 分别代表的含义为:
  • V%:表示测试过程中,软实时目标失败的概率,软实时目标失败即某个时间片段中实际GC时间超过了允许的最大GC时间。
  • avgV%:表示在所有实际GC时间超标的时间片段里,实际GC时间超过最大GC时间的平均百分比(实际GC时间减去允许最大GC时间,再除以总时间片段)。
  • wV%:表示在测试结果最差的时间片段里,实际GC时间占用执行时间的百分比。
 测试结果如下表所示:
  表1:软实时目标测试结果
解析JDK 7的Garbage-First收集器 - 断尘伤痕 - 断尘居

  从上面结果可见,对于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 %左右。

解析JDK 7的Garbage-First收集器 - 断尘伤痕 - 断尘居
图2:吞吐量测试结果

  在更大规模的生产环境下,笔者引用一段在StackOverflow.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给出一个自己的建议:直到现在为止还没有一款“最好的”收集器出现,更加没有“万能的” 收集器,所以我们选择的只是对具体应用最合适的收集器。对于不同的硬件环境、不同的软件应用、不同的参数配置、不同的调优目标都会对调优时的收集器选择产 生影响,选择适合的收集器,除了理论和别人的数据经验作为指导外,最终还是应当建立在自己应用的实际测试之上,别人的测试,大可抱着“至于你信不信,反正 我自己没测之前是不信的”的态度。

参考资料
  本文撰写时主要参考了以下资料:
声明
  本文已经首发于InfoQ中文站,版权所有,原文为《解析JDK 7的Garbage-First收集器》,如需转载,请务必附带本声明,谢谢。
  InfoQ中文站是一个面向中高端技术人员的在线独立社区,为Java、.NET、Ruby、SOA、敏捷、架构等领域提供及时而有深度的资讯、高端技术大会如QCon 、线下技术交流活动QClub、免费迷你书下载如《架构师》等。


-----------------------------------分割线---------------------------------

垃圾回收的瓶颈

    传统分代垃圾回收方式,已经在一定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量推到了一个极限。但是他无法解决的一个问题,就是Full GC所带来的应用暂停。在一些对实时性要求很高的应用场景下,GC暂停所带来的请求堆积和请求失败是无法接受的。这类应用可能要求请求的返回时间在几百甚 至几十毫秒以内,如果分代垃圾回收方式要达到这个指标,只能把最大堆的设置限制在一个相对较小范围内,但是这样有限制了应用本身的处理能力,同样也是不可 接收的。

    分代垃圾回收方式确实也考虑了实时性要求而提供了并发回收器,支持最大暂停时间的设置,但是受限于分代垃圾回收的内存划分模型,其效果也不是很理想。

    为了达到实时性的要求(其实Java语言最初的设计也是在嵌入式系统上的),一种新垃圾回收方式呼之欲出,它既支持短的暂停时间,又支持大的内存空间分配。可以很好的解决传统分代方式带来的问题。

增量收集的演进

    增量收集的方式在理论上可以解决传统分代方式带来的问题。增量收集把对堆空间划分成一系列内存块,使用时,先使用其中一部分(不会全部用完),垃圾收集时 把之前用掉的部分中的存活对象再放到后面没有用的空间中,这样可以实现一直边使用边收集的效果,避免了传统分代方式整个使用完了再暂停的回收的情况。

    当然,传统分代收集方式也提供了并发收集,但是他有一个很致命的地方,就是把整个堆做为一个内存块,这样一方面会造成碎片(无法压缩),另一方面他的每次 收集都是对整个堆的收集,无法进行选择,在暂停时间的控制上还是很弱。而增量方式,通过内存空间的分块,恰恰可以解决上面问题。

 

Garbage Firest(G1)

这部分的内容主要参考这里,这篇文章算是对G1算法论文的解读。我也没加什么东西了。


目标

从设计目标看G1完全是为了大型应用而准备的。

支持很大的堆

高吞吐量

  --支持多CPU和垃圾回收线程

  --在主线程暂停的情况下,使用并行收集

  --在主线程运行的情况下,使用并发收集

实时目标:可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收

当然G1要达到实时性的要求,相对传统的分代回收算法,在性能上会有一些损失。

 

算法详解

    G1可谓博采众家之长,力求到达一种完美。他吸取了增量收集优点,把整个堆划分为一个一个等大小的区域(region)。内存的回收和划分都以 region为单位;同时,他也吸取了CMS的特点,把这个垃圾回收过程分为几个阶段,分散一个垃圾回收过程;而且,G1也认同分代垃圾回收的思想,认为 不同对象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。为了达到对回收时间的可预计性,G1在扫描了region以后,对其中的 活跃对象的大小进行排序,首先会收集那些活跃对象小的region,以便快速回收空间(要复制的活跃对象少了),因为活跃对象小,里面可以认为多数都是垃 圾,所以这种方式被称为Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收。


回收步骤:

初始标记(Initial Marking)

    G1对于每个region都保存了两个标识用的bitmap,一个为previous marking bitmap,一个为next marking bitmap,bitmap中包含了一个bit的地址信息来指向对象的起始点。

    开始Initial Marking之前,首先并发的清空next marking bitmap,然后停止所有应用线程,并扫描标识出每个region中root可直接访问到的对象,将region中top的值放入next top at mark start(TAMS)中,之后恢复所有应用线程。

    触发这个步骤执行的条件为:

    G1定义了一个JVM Heap大小的百分比的阀值,称为h,另外还有一个H,H的值为(1-h)*Heap Size,目前这个h的值是固定的,后续G1也许会将其改为动态的,根据jvm的运行情况来动态的调整,在分代方式下,G1还定义了一个u以及soft limit,soft limit的值为H-u*Heap Size,当Heap中使用的内存超过了soft limit值时,就会在一次clean up执行完毕后在应用允许的GC暂停时间范围内尽快的执行此步骤;

    在pure方式下,G1将marking与clean up组成一个环,以便clean up能充分的使用marking的信息,当clean up开始回收时,首先回收能够带来最多内存空间的regions,当经过多次的clean up,回收到没多少空间的regions时,G1重新初始化一个新的marking与clean up构成的环。


并发标记(Concurrent Marking)

    按照之前Initial Marking扫描到的对象进行遍历,以识别这些对象的下层对象的活跃状态,对于在此期间应用线程并发修改的对象的以来关系则记录到remembered set logs中,新创建的对象则放入比top值更高的地址区间中,这些新创建的对象默认状态即为活跃的,同时修改top值。


最终标记暂停(Final Marking Pause)

    当应用线程的remembered set logs未满时,是不会放入filled RS buffers中的,在这样的情况下,这些remebered set logs中记录的card的修改就会被更新了,因此需要这一步,这一步要做的就是把应用线程中存在的remembered set logs的内容进行处理,并相应的修改remembered sets,这一步需要暂停应用,并行的运行。

 

存活对象计算及清除(Live Data Counting and Cleanup)

    值得注意的是,在G1中,并不是说Final Marking Pause执行完了,就肯定执行Cleanup这步的,由于这步需要暂停应用,G1为了能够达到准实时的要求,需要根据用户指定的最大的GC造成的暂停时 间来合理的规划什么时候执行Cleanup,另外还有几种情况也是会触发这个步骤的执行的:

    G1采用的是复制方法来进行收集,必须保证每次的”to space”的空间都是够的,因此G1采取的策略是当已经使用的内存空间达到了H时,就执行Cleanup这个步骤;

    对于full-young和partially-young的分代模式的G1而言,则还有情况会触发Cleanup的执行,full-young模式 下,G1根据应用可接受的暂停时间、回收young regions需要消耗的时间来估算出一个yound regions的数量值,当JVM中分配对象的young regions的数量达到此值时,Cleanup就会执行;partially-young模式下,则会尽量频繁的在应用可接受的暂停时间范围内执行 Cleanup,并最大限度的去执行non-young regions的Cleanup。

展望

    以后JVM的调优或许跟多需要针对G1算法进行调优了。

  评论这张
 
阅读(1403)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017