3、JVM--垃圾回收期和内存分配策略(2)
3.5、垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java
虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚
拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应
用特点和要求组合出各个年代所使用的收集器。这里讨论的收集器基于JDK 1.7 Update 14之
后的HotSpot虚拟机(在这个版本中正式提供了商用的G1收集器,之前G1仍处于实验状
态)
展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们
可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。
虽然我们是在对各个收集
器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的收集器
出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器。这点不需
要多加解释就能证明:如果有一种放之四海皆准、任何场景下都适用的完美收集器存在,那
HotSpot虚拟机就没必要实现那么多不同的收集器了。
3.5.1、Serial收集器
Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机
新生代收集的唯一选择。这个收集器是一个单线程的收集器,但它
的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,
更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。“Stop
The World”这个名字也许听起来很酷,但这项工作实际上是由虚拟机在后台自动发起和自动
完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说都是难
以接受的。
对于“Stop The World”带给用户的不良体验,虚拟机的设计者们表示完全理解,但也表
示非常委屈:“你妈妈在给你打扫房间的时候,肯定也会让你老老实实地在椅子上或者房间
外待着,如果她一边打扫,你一边乱扔纸屑,这房间还能打扫完?”这确实是一个合情合理
的矛盾,虽然垃圾收集这项工作听起来和打扫房间属于一个性质的,但实际上肯定还要比打
扫房间复杂得多啊!
从JDK 1.3开始,一直到JDK 1.7,HotSpot虚拟机开发团队为消除或者减少工
作线程因内存回收而导致停顿的努力一直在进行着,从Serial收集器到Parallel收集器,再到
Concurrent Mark Sweep(CMS)乃至GC收集器的最前沿成果Garbage First(G1)收集器,我
们看到了一个个越来越优秀(也越来越复杂)的收集器的出现,用户线程的停顿时间在不断
缩短,但是仍然没有办法完全消除(这里暂不包括RTSJ中的收集器)。寻找更优秀的垃圾收
集器的工作仍在继续!
它依然是虚拟机运行在Client模式下的默认新生代收集器。
它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个
CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最
高的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很
大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再
大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点
停顿是可以接受的。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的
选择。
3.5.2、ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之
外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:
PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对
象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相
当多的代码。
ParNew收集器除了多线程收集之外,其他与Serial收集器相比并没有太多创新之处,但
它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但
很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。在JDK 1.5时
期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器——CMS收
集器(Concurrent Mark Sweep),这款收集器是HotSpot虚
拟机中第一款真正意义上的并发(Concurrent)收集器,它第一次实现了让垃圾收集线程与
用户线程(基本上)同时工作。
不幸的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器
Parallel Scavenge配合工作,所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选
择ParNew或者Serial收集器中的一个。ParNew收集器也是使用-XX:+UseConcMarkSweepGC
选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在
线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保
证可以超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源
的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多
(譬如32个,现在CPU动辄就4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的
环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
注意 从ParNew收集器开始,后面还会接触到几款并发和并行的收集器。在大家可能
产生疑惑之前,有必要先解释两个名词:并发和并行。这两个名词都是并发编程中的概念,
在谈论垃圾收集器的上下文语境中,它们可以解释如下。
●并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
●并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能
会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
3.5.3、Paeallel Scaven收集器
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行
的多线程收集器
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点
是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到
一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总
消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚
拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高
吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不
需要太多交互的任务。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集
停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参
数。
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回
收花费的时间不超过设定值。不过大家不要认为如果把这个参数的值设置得稍小一点就能使
得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取
的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾
收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每
次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了
GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时
间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总
时间的5%(即1/(1+19)),默认值为99,就是允许最大1%(即1/(1+99))的垃圾收集
时间。
由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。除上
述两个参数之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得关
注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、
Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:
PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信
息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC
自适应的调节策略(GC Ergonomics)。如果读者对于收集器运作原来不太了解,手工优化
存在困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务
交给虚拟机去完成将是一个不错的选择。只需要把基本的内存数据设置好(如-Xmx设置最大
堆),然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio(更关注吞
吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。自
适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
3.5.4、Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整
理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式
下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge
收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent
Mode Failure时使用。
需要说明一下,Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代
收集,并非直接使用了Serial Old收集器,但是这个PS MarkSweep收集器与Serial Old的实现
非常接近,所以在官方的许多资料中都是直接以Serial Old代替PS MarkSweep进行讲解
3.5.5、Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直
处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了
Serial Old(PS MarkSweep)收集器外别无选择(还记得上面说过Parallel Scavenge收集器无
法与CMS收集器配合工作吗?)。由于老年代Serial Old收集器在服务端应用性能上的“拖
累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于
单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较
高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。
直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组
合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old
收集器。
3.5.6、CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集
器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重
视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常
符合这类应用的需求。
从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记—清除”算法实现
的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:
1、初始标记(CMS initial mark)
2、并发标记(CMS concurrent mark)
3、重新标记(CMS remark)
4、并发清除(CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是
标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC RootsTracing
的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变
动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远
比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起
工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停
顿,Sun公司的一些官方文档中也称之为并发低停顿收集器(Concurrent Low Pause
Collector)。但是CMS还远达不到完美的程度,它有以下3个明显的缺点:
CMS收集器对CPU资源非常敏感。其实,面向并发设计的程序都对CPU资源比较敏感。
在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资
源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量
+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且
随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就
可能变得很大,如果本来CPU负载就比较大,还分出一半的运算能力去执行收集器线程,就
可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。为了应付这种情况,
虚拟机提供了一种称为“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)的
CMS收集器变种,所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思
想一样,就是在并发标记、清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的
独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些,
也就是速度下降没有那么明显。实践证明,增量时的CMS收集器效果很一般,在目前版本
中,i-CMS已经被声明为“deprecated”,即不再提倡用户使用。
CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode
Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴
随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法
在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃
圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间
给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进
行收集,需要预留一部分空间提供并发收集时的程序运作使用。在JDK 1.5的默认设置
下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在
应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来
提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK 1.6中,CMS收集器
的启动阈值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一
次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来
重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CM
SInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能
反而降低。
还有最后一个缺点,CMS是一款基于“标记—清除”算法实现的收集
器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着收集结束时会有大量
空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有
很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full
GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开
关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并
整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于
设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full
GC时都进行碎片整理)
3.5.7、GI收集器
G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一,早在JDK 1.7刚刚
确立项目目标,Sun公司给出的JDK 1.7 RoadMap里面,它就被视为JDK 1.7中HotSpot虚拟机
的一个重要进化特征。从JDK 6u14中开始就有Early Access版本的G1收集器供开发人员实
验、试用,由此开始G1收集器的“Experimental”状态持续了数年时间,直至JDK 7u4,Sun公
司才认为它达到足够成熟的商用程度,移除了“Experimental”的标识。
G1是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长
期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他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把内存“化整为零”的思路,理解起来似乎很容易,但其中的实现细节却远远没有想象
中那样简单,否则也不会从2004年Sun实验室发表第一篇G1的论文开始直到今天(将近10年
时间)才开发出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,时间是用户可控
制的,而且停顿用户线程将大幅提高收集效率。通过图3-11可以比较清楚地看到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时间占用执行时间的百分比。
对于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的平均结果如图3-12所示。在
SPECjbb的应用下,各种配置下的G1收集器表现出了一致的行为,吞吐量看起来只与允许最
大GC时间成正比关系,而在telco的应用中,不同配置对吞吐量的影响则显得很微弱。与
CMS收集器的吞吐量对比可以看到,在SPECjbb测试中,在堆容量超过768MB时,CMS收集
器有5%~10%的优势,而在telco测试中,CMS的优势则要小一些,只有3%~4%左右。
3.5.8、理解GC日志
阅读GC日志是处理Java虚拟机内存问题的基础技能,它只是一些人为确定的规则,没有
太多技术含量。
每一种收集器的日志形式都是由它们自身的实现所决定的,换而言之,每个收集器的日
志格式都可以不一样。但虚拟机设计者为了方便用户阅读,将各个收集器的日志都维持一定
的共性
GC日志开头的“[GC”和“[Full GC”说明了这次垃圾收集的停顿类型,而不是用来区分新
生代GC还是老年代GC的。如果有“Full”,说明这次GC是发生了Stop-The-World的。
如果是调用System.gc()方法所触发的收集,那么在这里将
显示“[Full GC(System)”。
接下来的“[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,这里显示的区域名称与
使用的GC收集器是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default
New Generation”,所以显示的是“[DefNew”。如果是ParNew收集器,新生代名称就会变
为“[ParNew”,意为“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套
的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。
后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量->
GC后该内存区域已使用容量(该内存区域总容量)”。而在方括号之外的“3324K->
152K(11904K)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容
量)”。
再往后,“0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。有的收集器会
给出更具体的时间数据,如“[Times:user=0.01 sys=0.00,real=0.02 secs]”,这里面的user、
sys和real与Linux的time命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核
态消耗的CPU事件和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。CPU时间与
墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻
塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些
CPU时间,所以读者看到user或sys时间超过real时间是完全正常的。
3.5.9、垃圾收集器参数总结
JDK 1.7中的各种垃圾收集器到此已全部介绍完毕,在描述过程中提到了很多虚拟机非
稳定的运行参数,在表3-2中:
3.6、内存分配与回收策略
Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对
象分配内存以及回收分配给对象的内存。
对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标
量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分
配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的
规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟
机中与内存相关的参数的设置。
3.6.1、对象有限在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟
机将发起一次Minor GC。
虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集
行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。在实际
应用中,内存回收日志一般是打印到文件后通过日志工具进行分析
注意 Minor GC和Full GC有什么不一样吗?
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝
生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴
随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行
Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
3.6.2、大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长
的字符串以及数组(byte[]数组就是典型的大对象)。大对象对虚拟机
的内存分配来说就是一个坏消息(替Java虚拟机抱怨一句,比遇到一个大对象更加坏的消息
就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易
导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。
虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老
年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制
3.6.3、长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象
应放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对
象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被
Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中
每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就
将会被晋升到老年代中。
对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
3.6.4、动态对象年龄判定
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到
了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总
和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等
到MaxTenuringThreshold中要求的年龄。
3.6.5、控件分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有
对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机
会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代
最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行
一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置
不允许冒险,那这时也要改为进行一次Full GC。
下面解释一下“冒险”是冒了什么风险,前面提到过,新生代使用复制收集算法,但为了
内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor
GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要
老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类
似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多
少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋
升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进
行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活
后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。
如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保
失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避
免Full GC过于频繁,参见代码清单3-9,请读者在JDK 6 Update 24之前的版本中运行测试。
3、JVM--垃圾回收期和内存分配策略(2)的更多相关文章
- [jvm]垃圾回收与内存分配策略
一.垃圾回收算法 概述 JVM中,当创建的对象不再被使用的时候,此时我们认为他是无用的“垃圾”:在现代主流的商用jvm中,都是通过可达性分析来判断对象是否存活的.这个算法的基本思想是通过一系列“GCR ...
- jvm垃圾回收器与内存分配策略
一.判断对象存活的算法 1.引用计数算法 (1)概念:给对象中添加一个引用计数器每当有一个地方引用它时,计数器值加1:当引用失效时,计数器就减1:任何时刻计数器为0的对象就是不可能再被使用的. (2) ...
- 【java虚拟机序列】java中的垃圾回收与内存分配策略
在[java虚拟机系列]java虚拟机系列之JVM总述中我们已经详细讲解过java中的内存模型,了解了关于JVM中内存管理的基本知识,接下来本博客将带领大家了解java中的垃圾回收与内存分配策略. 垃 ...
- JVM学习总结四——内存分配策略
之前几篇我们介绍了jvm的内存模型以及垃圾回收机制,而本篇我们将介绍几个JVM中对象在分配内存是应该遵循的策略.毕竟,想要去优化程序,不仅要考虑垃圾回收的过程,还要从对象内存分配的角度减少gc的代价. ...
- 深入理解JVM(5)——垃圾收集和内存分配策略
1.垃圾收集对象 垃圾收集主要是针对堆和方法区进行. 程序计数器.虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收. 哪 ...
- Java自动内存管理机制学习(二):垃圾回收器与内存分配策略
备注:本文引自<深入理解Java虚拟机第二版>仅供参考 图片来自:http://csdn.net/WSYW126 垃圾收集器与内存分配策略 概述 GC要完成3件事: 哪些内存需要回收? 什 ...
- JVM系列四(内存分配策略).
一.概要 前面的文章介绍了对象的创建过程,其中第三步 -- 分配内存,只是简单的介绍了分配的方式 -- 指针碰撞.空闲列表,其实内存在堆上分配还大有文章嘞. 对象的内存分配,往大方向上讲,就是在堆上分 ...
- JVM学习第二天(垃圾回收器和内存分配策略)大章
说道垃圾回收器大家应该都会有所了解,GC白,当然说道具体的可能就不是很清楚了,今天我们就来玩一玩; GC要做的事情: 第一步:确定堆中需要回收的对象; 第二步:什么时候回收; 第三步:怎样回收 为什么 ...
- JVM垃圾回收器、内存分配与回收策略
新生代垃圾收集器 1. Serial收集器 serial收集器即串行收集器,是一个单线程收集器. 串行收集器在进行垃圾回收时只使用一个CPU或一条收集线程去完成垃圾回收工作,并且会暂停其他的工作线程( ...
- JVM(3) 垃圾回收器与内存分配策略
文章内容摘自:深入理解java虚拟机 第三章 对象已死? 1. 引用计数算法: 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器为0 ...
随机推荐
- Java代理(三)
前面说到了java的动态代理,但是动态代理依赖于接口,这次来看看cglib来实现的代理... 假设有如下方法,这回没有说接口哦~ package proxy.cglibProxy; public cl ...
- Graphviz 的命令行参数说明
所有的Graphviz程序都具有相似的命令行调用方式 cmd [ flags] [input files] 如果没有指定输入文件路径程序从标准输入stdin中读取. 参数设置 -Gname ...
- Java虚拟机基础知识你知道多少?
http://www.cnblogs.com/qlky/p/7401841.html java虚拟机结构 http://liuwangshu.cn/java/jvm/1-runtime-data-ar ...
- 视区相关单位vw, vh..简介以及可实际应用场景——张鑫旭
一.N多的唠哩唠叨 CSS3中一些新的单位早在去年春暖花开的时候就介绍了,参见:CSS长度值及时间.频率.角度单位.显然,其中就提到了本文要感叹的单位vw, vh,见下图: 不过“我看见你”和“我触碰 ...
- Android下用程序的方法为ListView设置分割线Divider样式
使用XML的时候可以使用android:divider属性为ListView设置分割线的样式(颜色或者资源文件),而在Java代码中默认提供的方法 listView.setDivider() 却只支持 ...
- Yii 之components
当我们创建一个module的时候,对应的path alias就已经创建.比如我们定义了一个module: www 1 2 3 4 5 'modules'=>array( 'www'=&g ...
- substr与substring的区别
在js中字符截取函数有常用的三个slice().substring().substr()了,下面我来给大家介绍slice().substring().substr()函数在字符截取时的一些用法与区别吧 ...
- CentOS安装Oracle 11g R2
官方的安装链接: https://docs.oracle.com/cd/E11882_01/install.112/e24326/toc.htm#BHCGJCEA 检查硬件需求 1. 内存需求 物理内 ...
- exim CVE-2017-16943 uaf漏洞分析
前言 本文由 本人 首发于 先知安全技术社区: https://xianzhi.aliyun.com/forum/user/5274 这是最近爆出来的 exim 的一个 uaf 漏洞,可以进行远程代码 ...
- vue router 配合transition 切换动画
把<router-view>嵌套在<transition>里,路由变化的时候,vue会为包裹页面的div增加动画样式,我们要做的就是监听路由变化.定义这些动画样式,以规定页面到 ...