1
.简介
The Java
TM 2 Platform Standard Edition (J2SE
TM platform)用途非常广泛,从桌面上的Applets小程序到服务器级别的大型Web应用都有它的身影。在J2SE platform 1.4.2版本中,有四种garbage collector,如果用户没有明确指定,则serial garbage collector为默认选择。在5.0版本中,garbage collector的选择是由application在启动时,基于设备的级别来决定的。
这种garbage collector的“智能选择”通常工作的很好,但也并非一贯如此。如果用户想对garbage collector做出自己的选择,那么本文将为用户提供非常有用的信息,包括垃圾收集的一般特性,以及如何从这些特性的调整项中获得最大收益。然后通过实例来阐述serial collector和 stop-the-world collector的背景。其他collector的具体特征则将在它们被选用时,根据应该考虑的因素再来讨论。
对garbage collector的选择何时才与用户有关呢?对多数application而言,两者并不会发生必然联系。也就是说,由garbage collector所导致的中断频率和中断持续时间能保持在一个适度范围内,从而不影响application的正常执行。举一个非实际的例子(当serial collector被使用时),当一个具有庞大规模的application需要在大量线程、处理器、sockets和内存资源之间谋求某种规模平衡时,那么这种情况就使得用户和garbage collector的选择相关了。
Amdahl观察到,大多数工作负载(workloads)不能很好的并行展开;另外,某些工作总是按序进行,从而无法从并行机制中获得好处。J2SE平台也会出现这情况,特别是在没有并行garbage collection的虚拟机中,比如1.3.1及以前版本的Java
TM platform,所以在多处理器系统上,garbage collection的作用增长与否,和其他并发执行的application是息息相关的。
下图勾画了一个理想中系统模型,它除了garbage collection外,能够进行完美的伸缩。红线是指application在单处理器环境下,garbage collection仅花费了1%的时间(红线左侧)。而转到32个处理器的环境下后,吞吐量则下降了20%(红线右侧)。如果garbage collection花费10%的时间(不考虑garbage collection在单处理器环境下所产生的骇人耗时),那么按比放到32个处理器时,就会损失75%的吞吐量。
这反映出,在小型系统环境下,如果开发过程中忽视了速度问题,那么当迁移到大型环境后,这些问题就会成为最主要的性能瓶颈。不管怎样,对瓶颈的微小改良,都能获得性能大幅提升。就大系统而言,正确选择garbage collector并对其按需调整,这是非常值得的。
Serial collector能适合大部分application。而其他collector都有一些额外开销和复杂性,这是运行其特有行为所需付出的代价。如果application不需要这些特有行为,请使用serial collector。举例来说,当一个大型application运行于大内存和多处理器的硬件设备上,且application还有着繁重线程工作时,那么在这种情况下,serial collector就不是预期中最好的选择。对付这类application,我们现在可以选择throughput collector。
本文中使用J2SE Platform 1.5版,以Solaris
TM Operating System (SPARC
(R) Platform Edition)为基础平台,因为这套平台能为J2SE Platform提供最大的软硬件可伸缩性。但文章中的内容也同样适合其他平台,包括Linux、Microsoft Windows和Solaris Operating System (x86 Platform Edition),就可伸缩硬件而言它们都是可以用的。虽然命令行选项是跨平台的,但某些平台的默认值或许会与下文描述有所不同。
2
.
Ergonomics
(工效学)
J2SE Platform version 1.5在此涉及的新特性就如同Ergonomics。Ergonomics的目的是,以最少的命令行来为JVM提供最好的性能调整。Ergonomics试图为application协调好以下部分:
* Garbage collector
* Heap 的容量
* Runtime 编译器
这部分假设application所处设备的级别是根据application的规格参数来暗示的(即,大型application运行在大型设备上)。另外,以上部分是调整garbage collection的简便途径。如果用户以application的最大中断时间和吞吐量为目标,那么便可使用throughput collector。为获得更好性能,我们必须设定heap的容量大小。尤其对大型application而言,使用大容量heap能有效提升性能。更多常用的ergonomics将在“Ergonomics in the 1.5 Java Virtual Machine”小节中描述。本文在介绍ergonomics前,将用更详细的操控方法来做解释。
本文随后讨论的throughput collector都属于ergonomics的特性,这些特性提供了新自适应容量的部分策略。其中包含了新的可用选项和附加选项,它们目标是提升与调优garbage collection的性能。
3
.
Generations
(辈分)
J2SE平台的一项重要优势是使开发人员从繁琐的内存分配和garbage collection中解脱出来。但是garbage collection可能会成为系统最主要的瓶颈,所以我们需要明白一些garbage collection实现中隐含的概念。Garbage collector会对各种application使用对象的情况做出一些假设,并从可调整地参数中反映出来,这些参数能在不牺牲抽象能力(power of the abstraction)的前提下提升性能。
当某个对象不再能被运行中的程序指针所触及时,这个对象就可视为垃圾。最直接的垃圾收集算法是简单地迭代每个可达对象。当某对象离开迭代时,则被视为垃圾。这方法所用的时间需要与存活对象的数量保持均衡,以禁止大型applications维持大量存活状态的数据。
从J2SE Platform 1.2版本开始,虚拟机便融入了许多不同的垃圾收集算法,它们相互联合,并用在generational collection中。当naive garbage collection去heap中检验每个存活对象时,利用对大多数applications属性的观察经验,generational collection会尽量避免产生额外的工作。
被观察的属性中,最重要的是infant mortality(新生儿死亡率)。下图蓝色区域是对象存活时间的一种典型分布结果。X轴是对象的存活时间,以字节的分配(bytes allocated)来测量。Y轴上的字节计数是对象的总字节数,与存活时间相对应。左侧顶点区域代表在完成分配后可以被很快回收的对象(也就是说,对象已经死了)。例如,循环中的迭代对象,这种对象常存活在单个循环内。
某些对象会存活较长时间,所以它们会分布在图中右侧的延展区域。例如,某些对象会从程序开始时一直存活到程序结束。而在这两个极端之间的对象,它们的存活时间则趋于适中,在上图中则表现为infant mortality高峰期右边近似长方形的区块。虽然某些application的对象分布会有与上图截然不同,但大多数application则与之惊人的相似。通过着力于对“die young”对象的监控,从而使高效率的垃圾收集成为可能。
对于这种情况,优化方案是在generation中运用内存管理,或使不同内存池持有不同存活时长的对象。每当generation 的可用内存空间被填满时,Garbage collection就会在发生。对象创建后会被分配到一个专用于存放新生对象的generation中或称young generation,由于infant mortality的原因,大多数对象都会在此死亡。当young generation被填满时,就会触发一个minor collection。假设在高infant mortality率的前提下,Minor collections就能被优化。垃圾收集过程是有开销的,需要平衡存活对象和被收集对象之间的数量。如果一个young generation被死亡的对象填满,那么它的收集过程便会很迅速。而一些幸存下来的对象则被迁移到tenured generation中。当tenured generation需要进行收集时,major collection就会被触发,但由于它涉及到所有存活对象,所以收集过程通常比较缓慢。
如果触发minor collection的时间间隔足够长,那么就能让大多数对象在两次minor collection之间死亡。这在概念上具有良好意义,因为如果young generation足够大(外加两次minor collection的间隔够长),那么minor collection就能够从高infant mortality率中获得效益。但这种情况也会由于application对象存活周期的特殊分布,或狭小的generation空间而被搅乱,致使收集行为发生在对象死去之前。
就如第二部分ergonomics中阐述的那样,对garbage collector的不同选择,是为了使各种application能获得良好的性能。serial garbage collector趋于为小型application提供服务,其默认参数是为多数小型application的有效运行而设计的。throughput garbage collector则趋于为大型application提供服务。根据ergonomics来设定Heap的大小,并配合适当的内存使用策略,throughput garbage collector就能为服务器级别的application提供良好的性能。以上选择方案能在多数application下很好的工作,但也并非总是如此。这就引出了本文的主要原则:
如果garbage collector成为了瓶颈,你也许会希望自定义generation的空间大小。那么请先检视garbage collector输出的详细信息,并从中探究那些与你特有性能指标相关的敏感部分,然后再对garbage collector的参数进行设定。
默认的generations排列如下图所示(这种排列除了throughput collector外适合于所有garbage collector)。
在初始化时,最大地址空间(maximum address space)实际上是被预留的,除非需要使用,否则不会给它分配物理内存。而完整地址空间(complete address space)则分成young generation和 tenured generation两部分,准备用于存储对象。
Young generation由Eden加两个survivor space组成。对象初始化时,将被分配到Eden区域。两个survivor space的其中一个在任意时刻都是空闲的,它将充当下一次拷贝Eden和另一个survivor space中存活对象的目的地。对象就以这种方式在survivor space之间来回拷贝,直到它们存活足够长的时间,以使它们被转移到tenured generation去后,这种拷贝才算结束。
其他虚拟机,包括为Solaris Operating System提供的J2SE Platform 1.2版本的专用虚拟机,都使用两个相等的survivor space拷贝,而不是一个大Eden加两个小survivor space。这意味着对于young generation的大小选择是不能直接比对的。参见Performance FAQ中的例子。
上图中紧挨着tenured generation的第三个区域是permanent generation。permanent generation有其特殊性,原因在于它持有是虚拟机需要使用的数据,这些数据用于描述对象,这种对象和Java语言层面的对象是不同的。例如,这些被储存于permanent generation中的对象是描述了各种类和方法。
3.1
性能考虑
对于garbage collection的性能,这里有两个主要测量指标。第一,Throughput(吞吐量)是指与总用时相比,没有花在garbage collection上的用时比例,这是对garbage collection长用时周期(long periods of time)的考虑。Throughput也包括在分配上所花费的时间(通常对分配速度作调整是不必要的)。第二,Pauses(中断)是指当一个application由于garbage collection的进行,导致application出现未响应状态所耽搁的时间。
用户对garbage collection会有不同的要求。例如,某些用户会为自己的web服务做出更多throughput的考量,所以用户也许可以忍受在garbage collection期间产生的多次pause,或者简单的以网络延迟来掩盖由pause导致的时间损耗。但无论如何,在一个图形化交互式程序中,即使短暂的pause都会影响用户的体现而不可被接受。
另一些用户则会对其他考量比较敏感。比如Footprint(印迹),它是一个工作集的处理过程(the working set of a process),可在页面和缓存行里进行测量。当系统受到物理内存的限制,或有许多处理工作要做,footprint也许会指定可测量性。再如Promptness,它是指从对象变为死亡,到死亡对象所占内存变为可用的这段时间,它对分布式系统尤为重要,包括远程方法调用(RMI)。
通常,某个特定generation的大小调整是在平衡这些考量后做出选择的。例如,一个庞大的young generation也许能使throughput最大限度的提高,但这些都是以footprint、 promptness,和pause的时间为代价的。而通过使用小型的young generation可以使其产生的各种pause降至最低幅度,但却是以损失throughput为代价的。对于第一次的近似值,一个generation的大小调节不会影响另一个generation的收集频率和pause的时间。
我们并没有一个绝对正确的方法来调节generations的大小。最好的方法是同时根据application的内存使用和用户需求来选择。由于这个原因,所以虚拟机对garbage collector的选择也并非总是最优的,也许还需用户通过命令行选项进行重设,见后文描述。
3.2
测量
Throughput和footprint是最好的测量指标,常用于衡量特定的application。例如,使用客户端负载生成器(client load generator)来对Web服务器的throughput进行测试,而服务器端的footprint则可能通过在Solaris Operating System上的pmap命令来测量。另一方面,garbage collection期间出现的pauses则能方便的通过检查虚拟机自身输出的诊断信息来评估。
在命令行中加入参数-verbose:gc,会打印出每次垃圾收集的信息。注意-verbose:gc输出的格式取决于不同的J2SE platform。例如,以下来自大型服务器application的输出:
[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]
在此,我们看到两个minor collections和一个major collections。第一行中,箭头前后的数字
325407K->83000K ( in the first line )
表明在garbage collection执行前后,存活对象所占空间的大小。在minor collections之后,总数中也包含那些不必要存活但没有得到回收的对象,这种情况的出现,是因为某些对象刚被创建出来,或者它们被在tenured generation里中的其他对象所引用。第一行中,括号内的数字
(776768K)( in the first line )
是总可用空间,不过没有把permanent generation的空间计算进来,这个空间其实是总heap空间减去一块survivor space。minor collection大约耗时1/4秒。
0.2300771 secs (in the first line)
类似的,第三行是major collection的输出格式。参数-XX:+PrintGCDetails能够打印出收集过程的附加信息。这些附加信息会随着不同版本的虚拟机而变化。-XX:+PrintGCDetails的附加输出尤其会随着Java虚拟机的开发需要而改变,我们来看一下J2SE Platform 1.5版本中使用serial garbage collector时所输出的信息:
[GC [DefNew: 64575K->959K(64576K), 0.0457646 secs] 196016K->133633K(261184K), 0.0459067 secs]]
显示出minor collection回收了大约98%的young generation,
DefNew: 64575K->959K(64576K)
大约用时46毫秒。
0.0457646 secs
整个heap的使用量下降了大约51%
196016K->133633K(261184K)
另外还有一些收集过程的额外信息(young generation的收集过程),比如最终时间:
0.0459067 secs
参数-XX:+PrintGCTimeStamps会将每次收集过程的启动时间作为附加信息一并输出。
111.042: [GC 111.042: [DefNew: 8128K->8128K(8128K), 0.0000505 secs]111.042: [Tenured: 18154K->2311K(24576K), 0.1290354 secs] 26282K->2311K(32704K), 0.1293306 secs]
收集过程大约在application执行的111秒时开始。minor collection是启动的时间于此相同。另外,Tenured中显示的是major collection的信息。tenured generation的使用量下降了大约10%,
18154K->2311K(24576K)
并用时约0.13秒。
4
.调整各
Generation
的容量
有许多参数能影响generation的容量。下图说明了heap中committed space和virtual space的区别。在虚拟机初始化时,给heap提供的全部空间都是处于预留状态的(没有占有实际物理内存)。预留空间能由选项-Xmx来指定。如果参数-Xms的值小于-Xmx的值,那么虚拟机并不会立即占有全部预留空间。而未占有的空间则称为“virtual”。Heap中,不同部分的容量(
permanent generation、
tenured generation和
young generation)都能被增加,直至受到virtual space限制为止。某些参数指定了heap中一部分与另一部分的比例。如参数NewRatio,它表示
tenured generation和
young generation之间的容量比例。这些参数会在随后讨论。
对heap空间伸缩概念的讨论并不适用于throughput collector。与throughput collector相关的heap伸缩概念会在5.2.2小节中讲述。而控制heap总量和generations容量的参数则适用于throughput collector。
4.1 Heap
总量
由于garbage collection会在generations被填满时发生,所以系统吞吐量会与可用内存总量成反比。所以总可用内存量是影响garbage collection性能的最重要因素。
在默认情况下,虚拟机会在每次collection时对heap进行伸缩,并根据特定比例,尝试为存活下来的对象保持一定大小的可用空间。这个特定比例可以通过对参数-XX:MinHeapFreeRatio=<minimum> 和 -XX:MaxHeapFreeRatio=<maximum>各设定一个百分比来实现,而heap总量则可通过下限-Xms和上限-Xmx来设定。32-bit Solaris Operating System (SPARC Platform Edition)的默认参数如下表所示:
-XX:MinHeapFreeRatio=
|
40
|
-XX:MaxHeapFreeRatio=
|
70
|
-Xms
|
3670k
|
-Xmx
|
64m
|
在64位系统上,heap大小参数的默认值已按比例增加近30%。这个增幅是用于补偿对象处于64位系统时,其尺寸变大所导致的额外内存消耗。
上表中的第一个参数表示:如果一个Generation的可用空间比例降到40%以下,那么虚拟机就会为了保有这40%的可用空间,并假设Generation尚未到达它的容量上限,而使Generation伸展。第二个参数则表示:如果可用空间超过70%,只要generation收缩后的容量不低于第一个参数的设定值,那么generation就会为了仅使用70%的可用空间而收缩。
服务器级别的大型application常会因这些默认值而经历两种不同情况。一是启动缓慢,原因在于初始heap的容量较小,虚拟机必须对它反复进行伸缩调整,同时还伴随着大量的
major collection。另一个更为紧迫的情况是,对于大多数服务器级别的application,heap的默认最大值却小的不合理。这里有几条规则可用于服务器级别的application:
除非你碰到了各种中断问题,否则请尝试给予虚拟机尽可能多的内存。64M的默认值通常太小了。将-Xms和-Xmx设置为相同值,避免虚拟机对影响性能最严重的容量调整做出自行判断,从而增加系统的可预测性。但另一方面,这也会使你失去虚拟机为拙劣设定而提供的自动补偿。确保增加内存的同时,增加可用处理器的数量,以便使得分配工作能够并发进行。
更多有关虚假机参数的设定见:
第二个最具影响力的要素是young generation在heap中所占的比例。较大的young generation能够降低minor collections的发生次数。然而由于heap的空间有限,所以如果有一个较大的young generation,那就意味着会有一个相对较小的
tenured generation,从而使得major collections的频率增加。因此最佳选择取决于application分配对象的存活时间和分布状况。
在默认情况下,
young generation的容量是由参数NewRatio控制的。比如,设定-XX:NewRatio=3,意为
young generation和
tenured generation之间的比例为1比3。换言之,Eden和survivor spaces的组合空间将是heap总量的四分之一。
参数NewSize和MaxNewSize限定了
young generation容量的上下边界。对这两参数的设定就等于固定了
young generation的容量,就像用-Xms和-Xmx的设定固定heap总量一样。 这种用NewRatio的整倍数来调整
young generation的方式为我们提供了更好的、更有用的粒度控制。
在理想情况下,minor collection中的存活对象将从
young generation的某一部分(Eden space加上第一个survivor space)被拷贝到
young generation的另一部分(第二个survivor space)。然而,这里并不能保证所有存活对象都能顺利融入第二个survivor space中。即使所有对象都为存活状态,也要确保minor collection能够完成,所以在
tenured generation中必须预留足够的可用空间以便容纳所有这里存活对象。在最坏情况下,预留空间的容量等于Eden space加上非空survivor space中对象已占的容量。当
tenured generation中没有足够空间应对这种最坏情况时,一次major collection就会被触发,以取代这种拷贝行为。小型applications在这种策略下并无大碍,因为
tenured generation中的预留空间通常仅是虚拟申请,而非实际使用。若不是application本身需要,去设定一个尽可能庞大的heap空间其实是无效的,比如Eden区域大过heap虚拟申请空间的一半,这只会使major collections被触发。注意
young generation的保证协议仅适用于serial collector。throughput collector和concurrent collector将继续为
young generation做收集工作,另外,如果
tenured generation不能容纳所有来自
young generation的晋升对象,那么两个generation都会触发垃圾收集。
如果想要调整survivor spaces的容量,则可使用参数SurvivorRatio,但这对性能而言通常并不重要。比如-XX:SurvivorRatio=6,设置每个survivor space和Eden之间的比例为1:6。换句话说,每个survivor space将是
young generation的八分之一(不是七分之一,因为有两个survivor space)。
如果survivor spaces太小,那么因收集而拷贝的对象就会直接流向
tenured generation。如果survivor spaces太大,则它们会无用地空闲在那里。在每次garbage collection的过程中,虚拟机会选择一个threshold次数(a threshold number of times),使一个对象能在它变老(tenured)之前被拷贝。Threshold的选择将以保持半满的survivors为参照。命令行选项-XX:+PrintTenuringDistribution能用于显示这个threshold,以及在new generation中的对象年龄。这对观察application的寿命分布(lifetime distribution)十分有用。
下面是32-bit Solaris Operating System (SPARC Platform Edition)的默认值:
NewRatio
|
2
( client JVM: 8)
|
NewSize
|
2228k
|
MaxNewSize
|
Not limited
|
SurvivorRatio
|
32
|
Young generation的最大容量将会根据heap的最大容量和NewRatio计算而来。MaxNewSize的默认值为“无限制(not limited)”,这意味着除非在命令行中为MaxNewSize指定一个值,否则前者的计算值(young generation的最大容量)不会受到它限制。
这里有几条规则可用于服务器级别的application:
最初决定给予虚拟机的内存总量应是你能负担起的。然后针对 young generation容量,绘制出你自己的性能衡量标准,并找出最佳设定。除非你发现由于过度major collection和pause次数而出现问题,否则请给予 young generation大量内存。在只有在 young generation达到heap总量的一半或接近的情况下,增加 young generation才会适得其反(无论何时 young generation的保证协议不能被满足)(这句翻译不一定准确,原文为:Increasing the young generation becomes counterproductive at half the total heap or less (whenever the young generation guarantee cannot be met).)。确保在增加 young generation同时,增加处理器的数量,以便分配能并非进行。
5
.
Collector
的类型
讨论至此,我们已经了解了serial collector。而在J2SE Platform 1.5版本中还提供了另外三种Collector。每个collector的实现侧重点各有不同,有的强调Application的吞吐量,而有的则关心Garbage collection的低中断耗时。
1.
throughput collector:它在
young generation中使用了一个collector的并行版本(parallel version)。你可以通过命令行选项-XX:+UseParallelGC来使用它。
tenured generation的collector还是与serial collector一样。
2.
concurrent low pause collector:如果你要使用这个collector,则可通过命令行-Xincgc ™ 或-XX:+UseConcMarkSweepGC来激活。
tenured generation使用Concurrent collector进行收集工作,而收集工作几乎完全能和application同时执行。Application仅会在收集期间被短暂的中断。
young generation copying collector的并行版本用的就是concurrent collector。
3.
incremental (有时也称为
train) low pause collector:如果要使用它,则可通过命令行-XX:+UseTrainGC来激活。这个collector从J2SE Platform 1.4.2版本之后就不再改变了,同时也停止了开发活动。未来的版本中它将不再被支持。请见
1.4.2 GC Tuning Document获得更多信息。
注意,使用了-XX:+UseParallelGC后,就不能再使用-XX:+UseConcMarkSweepGC了。J2SE Platform从1.4.2版本之后,会对为garbage collectors在命令行选项中设定的参数组合进行合法性验证,但早期版本并没有这项功能,所以非法参数组合会导致出现不可预期的结果。
首先,在你为application明确地选择其他collector之前,请先尝试JVM选择的collector。其次,为你的application调整heap容量,然后考虑application有什么样的要求没有得到满足。最后,在后者的基础上,考虑是否使用其他collectors。
5.1
何时使用
Throughput Collector
当你想提升application的性能,而硬件又能提供许多处理器时,那么使用throughput collector会是个很好的选择。在serial collector中,垃圾收集工作由单个线程负责,因此收集工作所耗费的时间将计入application的执行时间中,从而使执行时间变长。throughput collector则使用多线程来执行minor collection,所以能有效降低application的执行时间。典型的情况是application有大量的线程来分配对象。所以在这样的application中常常需要有一个大容量的young generation。
Throughput collector同serial collector一样,都是一种generational collector,不同之处在于它使用多线程来进行minor collection。major collections本质上等同于serial collector。在硬件设备拥有N个CPU的情况下,throughput collector默认将在minor collection中启用N个garbage collector threads。启用线程的数量可有命令行进行控制(见下文)。在硬件设备只有一个CPU的情况下,throughput collector运行表现会差于serial collector,因为它要为额外的并发算法付出性能开销(比如用于同步的开销)。在有两个CPU的硬件设备上,throughput collector通常同serial garbage collector的表现一样,而期望通过throughput collector降低minor garbage collector中断时间的预期,则需要硬件设备拥有2个以上的CPU。
通过命令行-XX:+UseParallelGC可以激活throughput collector。garbage collector线程的数量能通过命令行选项ParallelGCThreads进行控制(-XX:ParallelGCThreads=<期望的数量>)。如果通过命令行标记显式调整heap,那么throughput collector正常执行所需要的heap空间是同serial collector一样的(If explicit tuning of the heap is being done with command line flags the size of the heap needed for good performance with the throughput collector is to first order the same as needed with the serial collector.)。打开throughput collector能使minor collection的中断时间减少。因为它有多个线程参与到minor collection的工作中,不过在young generation到tenured generation的对象晋升过程中,它可能会出现碎片(fragmentation),但出现几率很小。每个garbage collection thread会在tenured generation中预留了一部分空间,用于对象的晋升,以及可用空间的区域,这些“用于对象晋升的缓存”会导致碎片效果。减少garbage collection thread的数量将会降低碎片效果影响,同样也将增加了tenured generation的容量。
就如先前所提及的,generations 的排列在throughput collector中是不同的,如下图所示。
在服务器级别的设备上, J2SE Platform 1.5版本将选择throughput collector作为garbage collector。
Ergonomics in the 5 Java Virtual Machine这篇文档将会讨论这个主题。一个新的调节方法已被加入到throughput collector中,该方法是基于application对于garbage collection期望的行为而设计的。下列命令行能用于指定这种期望行为,它的目的是为application设定最大中断时间和吞吐量。
通过命令行指定maximum pause time goals
-XX:MaxGCPauseMillis=<nnn>
这说明了throughput collector的最大中断时间为小等于<nnn> 毫秒。默认情况下,并没有maximum pause time goal(最大中断时间定额)的限制。throughput collector将调整Java heap的大小,同时调整与其他garbage collection的相关参数,来试图使garbage collection产生的中断时间小于<nnn> 毫秒。这些调整也许会导致garbage collector降低application的吞吐量。但在某些情况下,这个对中断时间的期望并不总能被满足。
就garbage collection的内外耗时而言(外部耗时是指application所用的时间),throughput goal(吞吐量定额)是可以被测量的。这个 goal可以通过命令行来设置:
-XX:GCTimeRatio=<nnn>
这个garbage collection和application的耗时比例为
1 / (1 + <nnn>)
比如-XX:GCTimeRatio=19,则表示goal被设为总耗时中的5%可用于garbage collection。默认值为1%。
另外,像隐式的goal一样,throughput collector将尽其所能地尝试访问那些处于最小heap中的其他各个goal。
各种goal的访问顺序如下:
-
- Maximum pause time goal
- Throughput goal
- Minimum footprint goal
Maximum pause time goal最先被访问。只有它被访问后,throughput goal才会被访问。同样的,仅当前两者已被访问后,footprint goal才会被考虑。
Collector维持着一些统计数据(如:平均中断耗时),并在它收集工作结束时更新这些统计数据。这些测试将根据已被访问的各个goal来决定是否对generation容量做出必要的调整。唯一的例外是,显式的garbage collections(比如调用System.gc())会根据维持的统计数据和对generations做出的容量调整而被忽略。
Generation容量的伸缩是由增额幅度设定来实现的,这个幅度是Generation容量的一个固定百分比。一个generation会朝着它所期望的容量进行伸缩调整,但伸缩之间会有不同的比例。默认情况下,一个generation的伸展幅度为20%,收缩幅度为5%。伸展幅度的百分比可由-XX:YoungGenerationSizeIncrement = <nnn>和-XX:TenuredGenerationSizeIncrement = <nnn>命令行来设定,它们分别针对young generation和tenured generation。而收缩幅度的百分则可由-XX: AdaptiveSizeDecrementScaleFactor=<nnn >来设定。如果容量的伸展幅度为百分比数值XXX,那么容量的收缩幅度则为XXX/nnn。
如果collector在启动时决定对generation容量进行伸展,那么会有一个增补的百分比数值被加入到增额幅度中。这个增补值会随着收集次数的增加而衰变,并且最终不再产生影响。增补值意图在于提高collector启动时的性能。收缩是没有增补值的。
如果maximum pause time goal没有访问到,那么每一次中,仅有一个generation的容量可被收缩。如果两个generation的中断耗时都高于goal的设定,那么中断耗时较长的generation的容量会先被收缩。
如果throughput goal没有被访问到,那么两个generation的容量都会增加。它们会增长,直至均衡达到各自对总garbage collection时间的贡献比例为止。例如,如果young generation 的garbage collection时间为总collection时间的25%,而young generation的全部增幅仅为20%,那么young generation将再被增加5%。
如果没有在命令行进行设置,那么Heap的初始容量和最大容量将基于物理内存的大小来计算。如果phys_mem表示平台中物理内存的容量,那么Heap的初始容量将等于phys_mem / DefaultInitialRAMFraction。DefaultInitialRAMFraction是一个命令行选项,其默认值为64。同样的,Heap的最大容量将等于phys_mem / DefaultMaxRAMFraction。DefaultMaxRAMFraction的默认值为4。
如果throughput collector在garbage collection上花费太多时间,那么它就会抛出out-of-memory的异常。举例来说,如果JVM花了98%以上的总时间来进行garbage collection,而却只回收了2%的Heap,那么就会抛出一个out-of-memory异常。此特性的实现机制已在1.5版本中被改变,主体策略是相同的,不过新的实现机制也许更为轻量。
garbage collector的详细输出信息也可用于throughput collector,就同serial collector一样。
5.3
何时使用
Concurrent Low Pause Collector
如果你的application想要以下好处,那么就可使用Concurrent Low Pause Collector。
1) 想要垃圾收集器在执行过程中,以较短的停顿间隔(即对application的中断)来运行;
2) 想要使application运行过程中,让垃圾收集器能共享处理器资源。
通常而言,这类application会有一个长寿命的数据集合,且集合的规模相对较大(需要一个大tenured generation来支持),另外这类application通常运行在拥有两个或两个以上处理器的硬件设备中,以此来获得Concurrent Low Pause Collector所带来的好处。但无论怎样,Concurrent Low Pause Collector是为有低停顿间隔需求的application而提供的。而对交互式application而言,观察到的最佳效果是,application以一个大小适中的tenured generations在单处理器上运行。
Concurrent Low Pause Collector类似于serial collector,是一个基于对象辈分(创建时获得的辈分)的收集器。
tenured generation的并行收集机制便是它。
Concurrent Low Pause Collector将试图降低
tenured generation在回收过程中所需的中断耗时。它使用独立的garbage collector thread去执行major collection的不同部分,同时还与application的相关线程并发运行。可以通过命令行-XX:+UseConcMarkSweepGC来激活它。
每次major collection要进行并发回收时,所有与application相关的线程都会被中断,这种短时中断有两处,分别位于回收过程开始和接近回收过程的中期。其中第二次中断会耗费较长时间,因为多线程回收工作将在此处开展。
回收工作的剩余部分由一个garbage collector thread来完成,而且它将与application一起并发运行。虽然minor collections采用多线程进行回收工作,但它在一定程度上仍类似于serial collector。可参见下文的"Parallel Minor Collection Options with the Concurrent Collector"章节以获取更多关于concurrent low pause collector使用多线程的信息。
有关并行收集器中使用的技术(用于tenured generation的回收)将在以下链接中详述:
Concurrent collector为major collection提供的更短的中断耗时优势,但这是以耗费处理器资源为代价的(这些资源本可用于application的其他部分)。Collector的并发部分其实是一个单一的garbage collection thread。当收集器的并发部分运行在一个多处理器设备上时,它将使用第一或第N个可用的处理器资源。不过在单处理器设备上,它也许能偶然提供的一些好处(具体参见
Incremental mode)。
Concurrent collector同样有一些额外的开销,这些开销会导致application吞吐量的下降,而且对某些application而言还有一些内在缺陷(如碎片等)。Concurrent collector在有两个处理器的设备上运行时,那么一个处理器将为application的线程提供服务,另一个则为收集器的并发部分提供服务,因此Concurrent collector的运行不会使application中断。这也许降低了中断导致的时间耗费,但同时也消耗了处理器为application所提供的可用资源,从而使其速度减慢。随着处理器数量的增加,并发垃圾回收线程在运行时所耗费的处理器资源将相对减少和变小,从而使得并发收集器的益处得以更充分的体现。
在J2SE Platform version 1.5之前,concurrent collector需要满足young generation的保证协议,就如同serial collector所做的一样。但自J2SE Platform version 1.5起就不再有效了。此后的concurrent collector便有了自动复原机制,即如果concurrent collector在young generation启动收集动作,而在tenured generation又遭遇空间不足以容纳这些需从young generation迁来的对象时,收集器会保留这些要迁移的对象。这类似于throughput collector。
Concurrent collector使用单一的垃圾回收线程,来与application的线程同步运行,这是为了tenured generation能在其被填满前进行完成垃圾回收工作。在通常的运行过程中,Concurrent collector几乎能在application线程不被中断的情况下完成它的工作,所以仅有短暂的中断被application线程所察觉。但如果Concurrent collector未能在tenured generation被填满前完成它的工作,那么作为一个后备方案,application将被迫中断,并且在垃圾回收工作全部完成前都不会恢复运行。这就是Full Collections,出现这种情况常意味着需要对Concurrent collector的运行参数做出调整。
Garbage collector的工作是从heap中寻找存活的对象。由于application的各种线程和garbage collector thread在major collection期间并发运行,所以现在被garbage collector thread找到的存活对象,可能在回收结束后不久就死亡了。这些对象就是所谓的floating Garbage。Floating garbage的数量取决于并发收集过程的长度(即application的各种线程经多久后会抛弃对象)和特定的application。一个粗略的方法是尝试增加20%的
tenured generation空间,以容纳这些floating Garbage。Floating garbage会在下一次收集时被回收。
Concurrent collector在并发回收的周期中,会中断application两次。第一次用于标记那些从根节点出发,直接可达的存活对象(例如:thread stack中的对象、静态对象等),和别处Heap中的对象(例如:
young
generation)。第一次中断可以理解为initial mark(初始标记)。第二次中断则从marking phase(标记阶段)结束后开始,用来找出那些由于application线程并发执行的缘故,而在标记阶段中错过的对象。第二次中断则可以理解remark(重新标记)。
Concurrent marking发生在initial mark和remark两个阶段之间。在Concurrent marking过程中,Concurrent garbage collector thread会开始执行,并占用本可为application提供服务的处理器资源(上图蓝色线条)。在remark阶段后,是concurrent sweep阶段,用于回收那些已死亡的对象。在此阶段,Concurrent garbage collector thread又将占用那些本可为application提供服务的处理器资源。在打扫完后,Concurrent collector将进入睡眠状态,直到开始下一次的major collection。
当tenured generation被填满时,采用serial collector的major collection才会被启动,并且停止所有与application相关的线程,直到回收工作完成。相比之下,Concurrent collection则会在tenured generation被填满前就启动,以完成它的回收工作。有几种不同的方式能启动concurrent collection。
Concurrent collector会反复统计和比对tenured generation在被填满前的可持续时间(T-until-full)与自身进行回收工作的所需时间 (T-collect)。当T-until-full接近T-collect时,并行回收工作就会被启动。为了及早且谨慎地开始回收工作,这种测试比对是适当地。
Concurrent collection同样会在tenured generation的占用量(occupancy)超过初始设定值(即在并行回收工作启动前,可占用当前堆空间的百分比)的时候启动。初始占用量的默认值为68%。它可以用参数CMSInitiatingOccupancyFraction来设定,命令行格式如下:
-XX:CMSInitiatingOccupancyFraction=<nn>
<nn>表示当前tenured generation空间的百分比。
在young generation和tenured generation中各自触发的中断是相互独立。虽然它们不能同时发生,但它们会被快速接连的触发,比如某个回收工作的中断被触发后,紧跟着另一个回收工作的中断又被触发,让我们以为它们是一个单次、较长的中断。为了避免上述情况, Concurrent collection引发的remark中断,可在young generation的前后两次中断的中途调度进行。另外,initial mark阶段所导致的中断,通常由于时间太短,而不值得这样做。
Concurrent collector可在某种特殊模式下运行,这模式以增量方式来完成并发阶段中的工作任务。回想一下,在这个并发阶段,garbage collector thread正在使用处理器资源。而incremental mode正是为了减少并发阶段消耗过长时间所带来的影响,所以通过周期性地暂定并发阶段,来让出处理器给application帮忙。这种模式(在此称为"I-CMS ")是由collector先把工作分割成可在小块时段内并发完成的子任务,然后将它们安排在两次young generation collections之间处理。当application运行于处理器数量较少(1或2个)的设备上,同时又需要concurrent collector来它提供低中断时,那么这种特征就显得非常有用。
concurrent collection的周期一般包含以下步骤:
- 中断所有application的线程运行;执行initial mark;恢复所有application的线程运行;
- 执行concurrent mark(使用一个处理器为并发工作提供服务);
- 执行concurrent pre-clean(并发预清理)(使用一个处理器为并发工作提供服务);
- 中断所有application的线程运行;执行remark;恢复所有application的线程运行;
- 执行concurrent sweep(使用一个处理器为并发工作提供服务);
- 执行concurrent reset(并发重置)(使用一个处理器为并发工作提供服务);
通常,Concurrent collector在整个concurrent mark阶段,仅用一个处理器为并发工作提供服务,并且不会自愿让出占用的处理器。类似的,在concurrent sweep阶段同样仅使用一个处理器,而且也不会让出占用的处理器。这时处理器会由于application与中断时间的约束而负担过重,这种情况在仅有一或两个处理器的设备上尤为明显。I-CMS模式可解决这个问题,它将并发阶段分割成可执行的小块任务,这些任务则可被安排在两次minor pause(次要中断)之间执行。
I-CMS使用一种名为“duty cycle(职责循环)”的机制,来控制concurrent collector在自愿放弃处理器之前被允许运行的数量。duty cycle其实是两次young generation收集过程中的时间比率,以表明concurrent collector允许运行的时间。I-CMS能够根据application的行为自动计算duty cycle的时间比率(推荐这种方式),或通过命令为duty cycle设一个固定值。
下列命令可用于设置I-CMS(见下文中推荐的初始值设定)
-XX:+CMSIncrementalMode 默认值:未激活(disabled)
这条命令能激活增量模式。注意,只有在并发收集器被激活后它才会生效。(使用命令行-XX:+UseConcMarkSweepGC来激活并发收集器)。
-XX:+CMSIncrementalPacing 默认值:未激活(disabled)
这条命令能使增量模式按JVM运行过程中的回收统计,自动调整duty cycle的值。
-XX:CMSIncrementalDutyCycle=<N> 默认值:50
一个百分比数值(0-100),设定在两次minor collection间并行收集器被允许运行的时间比率,即duty cycle的大小。只有当CMSIncrementalPacing被激活后,这条命令才有效。
-XX:CMSIncrementalDutyCycleMin=<N> 默认值:10
一个百分比数值(0-100),用于限定duty cycle的最小值,当CMSIncrementalPacing被激活时才有效。
-XX:CMSIncrementalSafetyFactor=<N> 默认值:10
一个百分比数值(0-100),用于在计算duty cycle时,加入保守因子(conservatism)
-XX:CMSIncrementalOffset=<N> 默认值:0
一个百分比数值(0-100),是指在minor collections中,incremental mode duty cycle可使用连续区域的大小比例。
-XX:CMSExpAvgFactor=<N> 默认值:25
一个百分比数值(0-100),当在计算并行收集统计的指数平均值时,它将对现行样本进行加权。
当你在尝试I-CMS时,我们建议按照以下命令行选项对其进行初始设定:
-XX:+UseConcMarkSweepGC \
-XX:+CMSIncrementalMode \
-XX:+CMSIncrementalPacing \
-XX:CMSIncrementalDutyCycleMin=0 \
-XX:+CMSIncrementalDutyCycle=10 \
-XX:+PrintGCDetails \
-XX:+PrintGCTimeStamps \
-XX:-TraceClassUnloading
前三个选项用于激活concurrent collector、I-CMS模式和I-CMS自动起搏机制。随后两项用于将duty cycle的最小值设为0、初始值设为10,默认值(10、50)对于大多数程序来说太大了。最后三项可使收集过程中的诊断信息输出到控制台或日志中,以便能随后能分析I-CMS的行为。
I-CMS的自动起搏机制使用运行中的统计聚合程序来计算duty cycle,所以会导致并发收集过程在完成前heap就被填满。不管怎样,用过去的行为预测将来并非好事,而且估算过程也未必足够精确,所以不能防止heap被填满。如果因此而促成full collections进行太多的情况,请按以下步骤逐尝试(一次进行一个):
增加安全因子
-XX:CMSIncrementalSafetyFactor=<N>
增加duty cycle的最小值
-XX:CMSIncrementalDutyCycleMin=<N>
禁止duty cycle的自动起搏,并固定它。
-XX:-CMSIncrementalPacing -XX:CMSIncrementalDutyCycle=<N>
通过命令行-verbose:gc联合-XX:+PrintGCDetails一起使用,可获得有用的输出信息(某些细节已被移除)。注意下文,可见并发收集器的输出信息是散布在minor collection输出信息中的。通常许多minor collections将在并发回收过程中被触发。CMS-initial-mark:表明concurrent collection cycle的启动,CMS-concurrent-mark:表明concurrent marking phase的结束,CMS-concurrent-sweep:标记concurrent sweeping phase的结束。CMS-concurrent-preclean:表明之前没有讨论的precleaning phase。Precleaning,它是一项为标记阶段CMS-remark提供的准备工作,而且能并发完成。最后阶段由CMS-concurrent-reset来定义,它是下一次concurrent collection的准备过程。
[GC [1 CMS-initial-mark: 13991K(20288K)] 14103K(22400K), 0.0023781 secs]
[GC [ DefNew: 2112K->64K(2112K), 0.0837052 secs] 16103K->15476K(22400K), 0.0838519 secs]
...
[GC [DefNew: 2077K->63K(2112K), 0.0126205 secs] 17552K->15855K(22400K), 0.0127482 secs]
[CMS-concurrent-mark: 0.267/0.374 secs]
[GC [DefNew: 2111K->64K(2112K), 0.0190851 secs] 17903K->16154K(22400K), 0.0191903 secs]
[CMS-concurrent-preclean: 0.044/0.064 secs]
[GC[1 CMS-remark: 16090K(20288K)] 17242K(22400K), 0.0210460 secs]
[GC [DefNew: 2112K->63K(2112K), 0.0716116 secs] 18177K->17382K(22400K), 0.0718204 secs]
[GC [DefNew: 2111K->63K(2112K), 0.0830392 secs] 19363K->18757K(22400K), 0.0832943 secs]
...
[GC [DefNew: 2111K->0K(2112K), 0.0035190 secs] 17527K->15479K(22400K), 0.0036052 secs]
[CMS-concurrent-sweep: 0.291/0.662 secs]
[GC [DefNew: 2048K->0K(2112K), 0.0013347 secs] 17527K->15479K(27912K), 0.0014231 secs]
[CMS-concurrent-reset: 0.016/0.016 secs]
[GC [DefNew: 2048K->1K(2112K), 0.0013936 secs] 17527K->15479K(27912K), 0.0014814 secs]
initial mark pause所用的时间相比minor collection pause来说通常很短。而concurrent phases的所用的时间(concurrent mark, concurrent precleaning, 和concurrent sweep)比起minor collection pause可能相对较长,但application不会在concurrent phases被中断。remark pause则被application的特殊细节(比如,高频度的修改对象便可使中断增加),以及自最后一次进行minor collection的时间所影响(换言之,
young generation中的对象越多就越可能使中断增加)。
6
.其他方面的考量
对多数application而言,permanent generation不会牵扯垃圾回收的效能。但无论如何,一些application会大量动态生成和装载许多类,例如某些JSP页面的执行。所以如果必要的话,permanent generation的最大空间可通过对命令项-XX:MaxPermSize的设定来增加。
某些application会通过使用finalization和weak/soft/phantom references来与garbage collection进行交互。这些特性能在现代Java编程语言的层面上创造出古老的编程行为(performance artifacts)。举例来讲,依赖finalization去关闭文件描述符,就是让一个外部资源(这里的描述符)的释放依赖于garbage collection的及时运行。garbage collection是用来管理内存的,而用它管理来外部资源的方法几乎总是差强人意的。
而另外一些application则通过如System.gc()的显式调用来与garbage collection进行交互,以避免full garbage collection的出现。这些调用是针对major collection的,并且抑制了在大型系统上的可测量性。因为explicit garbage collections(垃圾的显式回收)而受到影响的性能可通过命令行-XX:+DisableExplicitGC来测量,命令行-XX:+DisableExplicitGC用于禁止这种explicit garbage collections(显式的垃圾回收)。
使用explicit garbage collection的另一种最常见情况是发生在RMI的分布式垃圾回收过程(distributed garbage collection,DGC)中。application使用RMI来获取其他虚拟机中的对象。这些分布式application中的垃圾在没有occasional local collection(不经常的本地收集)的情况下就不能被收集,所以RMI就采用了定期的整体收集(periodic full collection)的策略。这种收集的频率可通过以下属性来控制。例如通过命令行:
java -Dsun.rmi.dgc.client.gcInterval=3600000
-Dsun.rmi.dgc.server.gcInterval=3600000 ...
-Dsun.rmi.dgc.server.gcInterval=3600000 ...
以上命令指定了显式回收的频率为每小时一次,而不是默认的每分钟一次。但这也会使某些对象在花费很长时间后才能得到回收。此属性可设为Long型的最大值,实际上会是使显示回收之间的时间间隔变为无穷大,如果你没有设定DGC活动间隔的上限值,则默认为Long型的最大值。
Solaris 8操作系统能支持另一种形式,名为libthread,它是一种直接把线程绑定到light-weight processes (LWPs)上的解决方案。某些application可从此方案获得大量好处。对所有多线程的application而言,这种方案都能为其提供一些潜在益处。你可以尝试在运行虚拟机之前,设置环境变量LD_LIBRARY_PATH,使它包含/usr/lib/lwp,以获得这些潜在益处。在Solaris 9上,此方案已为默认设置。
软引用(Soft references)通常易于清理,且***性弱,运行于服务器上的虚拟机比在运行客户端上的虚拟机更突出这点。清理频率能够通过设定参数SoftRefLRUPolicyMSPerMB而慢下来,具体命令为-XX:SoftRefLRUPolicyMSPerMB=10000。SoftRefLRUPolicyMSPerMB是测量一个软引用存活的时间指标,用于使heap中的可用空间达到规定数量。默认值为每兆1000毫秒。这可理解为,对于heap中每兆大小的可用空间而言,一个软引用有1秒的存活时间(在最后一个强引用的对象被回收后)。这个1秒的估算已非常接近真实状态了。
7
.结束语
垃圾回收会在不同application中是否成为瓶颈,主要随application的需求而定。理解application的需求和垃圾回收可调整项,能够使瓶颈问题的影戏降到最低.
转载于:https://blog.51cto.com/babyhe/685254