现在的位置: 首页 > 综合 > 正文

Sun JVM GC总结【Final】

2013年11月08日 ⁄ 综合 ⁄ 共 10095字 ⁄ 字号 评论关闭

导言

SUN官方发布的关于垃圾收集性能调整的文档,在不同JDK下有不同的版本,文档内容大同小异。本文档基于SUN
1.3.1
版本调优文档创作,添加了关于1.4.2中可选垃圾收集器的内容。而是加入了我对于GC的一些思考,同时删除了原文档作者不合时宜的幽默。本文档前半部分内容由暴风尖塔独立完成,后半部分引用了原dev2dev版主伍昊献的翻译

这里给出官方文档在不同版本之下的链接
1.3.1  http://java.sun.com/docs/hotspot/gc/index.html
1.4.2
  http://java.sun.com/docs/hotspot/gc1.4.2/
5.0
   http://java.sun.com/docs/hotspot/gc5.0/gc_tuning_5.html

 

现在,Java 2
平台的发展已经远远超过了当时设计者的预料,从专门为小型设备和Web
小程序设计的语言,发展到广泛被应用于企业级应用。很多大型的应用软件也使用Java语言编写,如作为应用程序服务器(Application Server)BEA WebLogicIBM WebSPhereRedHat
JBOSS
Apache Tomcat等软件。使用者对此类软件通常有很高的性能要求,即要求此类软件的性能随着CPU,内存,线程数目,Socket数目等资源的增加而提升。但是很多时候,事情并不象想象中那么简单,提升性能不仅仅需要增加相应的资源,同时也需要有特别的技术方面的支持。这篇文档阐述了在获取高性能的过程中所使用到的技术:GC参数以及策略调整。

 

JDK 1.4.1上有4种备选的垃圾收集器,但哪一个都不能适用于所有的情形。在JDK5.0中,垃圾收集器的类型基于应用程序所运行的machine的类型。本节内容的目的是给出在不同情况下选择垃圾收集器的一些指导性建议。本文首先给出了不同垃圾收集器都具有的通用功能和关于这些功能的调整选项的最佳实践,使用的例子是默认的单线程收集器。然后介绍了不同垃圾收集器各自的特点,以及选择垃圾收集器时应该考虑的内容。

 

虽然多CPU主机已经成为主流,并且多线程程序也成为了大多数平台的程序标准。但是,Amdahl发现很多的工作并没有很好的利用并行处理的优势,某些工作总是串行的,不能从并行处理获得好处。Java2平台就存在这种情况。特别是,JVM
1.3.1
及以下版本没有并行GC,并且GC的运行会挂起所有正在运行的应用线程。垃圾收集算法会在一段时间里使系统停止,单线程的收集器很快会成为伸缩性瓶颈,因为在垃圾收集器将用户程序线程挂起时,除了一个处理器之外,其他的处理器都是空闲的。所以在多CPU系统之上,相对于能够多线程运行的应用程序来说,只能单线程运行的GC会造成很大的Throughput损失。

 

下图显示了一个除GC之外其余均达到完美的系统。最上面的线(红色),反映了在单处理器上,只花1%时间在GC上的应用情况:这可以理解为,在32个处理器上,将会损失至少20%的Throughput。到10%时,如果不考虑单处理器应用中GC所用的时间,那么损失的Throughput将会超过75%。 

 

 

这里说明一下为什么在高GC时间花费的情况下,吞吐量会随着CPU的增加而显著降低。通常情况下java线程的实现方式是使用操作系统级别线程,每一个java线程都会表现为一个操作系统本地线程,在多CPU的机器上,每一个线程被分配到不同的CPU运行。无并发的垃圾收集器(1.3.1的唯一gc1.4.2以及以后版本的缺省
gc)
在运行的时候,会挂起所有的应用线程。但是只使用一个线程进行垃圾收集工作。在多CPU的机器上,这样就明显的降低了吞吐量。

 

假设总的工作量是1000,在单CPU环境下,假设每个CPU每秒处理的工作量是10,则无GC处理完所有的工作时间是100秒种。

GC每秒吞吐量=1000/100=10

GC时间花费10%的情况下,花费的时间是110秒。则实际吞吐量是1000/110=
GC
每秒吞吐量=1000/110=9.1

降低的吞吐量比为(10-9.1)/10=9%

 

10CPU的情况下,无GC处理完所有的工作的时间是10

GC每秒吞吐量=1000/10=100

加上GC时间,总花费时间20秒。

GC每秒吞吐量=1000/20=50

降低的吞吐量比为(100-50)/100=50%

 

32CPU的情况下,无GC处理完所有的工作的时间是3.125

GC每秒吞吐量=1000/3.125=320

加上GC时间,总花费时间13.125秒。

GC每秒吞吐量=1000/13.125=76.2

降低的吞吐量比为(320-76.2)/ 320=76.1%

 

这就证明了当GC花费时间比例增大的时候,在小型系统应用上所损失的Throughput可能会成为瓶颈问题。但是请无需担心,对这个瓶颈问题的一点小改进能获得很高的性能提升;对于一个大型的系统来讲,调整GC也同样是值得做的一个工作。

另外,在此给出一些专有名词的意思:

Ø     
gcgarbage collection(垃圾收集)

Ø     
infant mortality:对象分配以后很快成为垃圾,就称该对象具有“infant
mortality”

Ø     
minor collection:较小收集,指发生在young
generation
gc

Ø     
major collection:较大收集,指发生在older generationgc

Ø     
older generation:年老代,在1.4.2版本之后改称为tenured
generation

Ø     
tenured generation:年老代,在1.3.1之前称为older
generation

Ø     
permanent generation:永久代。又称为永久域,方法区。

Ø     
young generation:年轻代

Ø     
footprint:是一批工作进程的集合,以页和缓冲行数计量,在物理内存有限或者有很多处理器的系统里,footprint
可代表伸缩性

Ø     
survivor spaces:生存空间

Ø     
eden:新的对象分配的地方

Ø     
throughput:是未消耗在垃圾收集的时间占总时间的百分比

 

(一)GC按代收集

Java 2
平台一个很强的特性之一就是屏蔽内存分配和GC的复杂性。然而,一旦GC成为瓶颈,那么就要理解所隐藏的实现细节。垃圾收集器对应用使用对象的方式作了限定,这些限定就反映在可调整参数中。这些参数可以被调整,在不牺牲抽象能力情况下获取更高的性能。

 

在一个运行的程序中,如果一个对象不再有任何引用,那么它将成为垃圾。大部分GC算法就是简单地对每个可触及对象进行遍历:任何不可触及的对象,将成为垃圾。这种算法所花的时间和实际活动对象的数量成比例,因此对于具有大量活动数据的大型应用,就不再适用。

 

J2SE平台的1.2版本开始,引入了集许多不同的GC算法为一体的新算法,这些不同的算法是通过分代收集结合在一起的,因此称为分代垃圾收集器。当GCHeap中检查每一个活动的对象时,分代收集利用大多数应用的几个属性来避免额外的工作。

 

这些属性中,最重要的是infant mortality(对象分配以后很快成为垃圾)。下图中蓝色区域显示了对象生命周期的典型分布。左边的峰值代表在分配之后能很快收集的对象。例如,迭代器对象(Iterator
objects
)的生命周期通常只是在一个循环语句的执行期间是可触及的。

 

这里做一下简单解释,IteratorJava内嵌的一种设计模式,Iterator模式是用于遍历集合类的标准访问方法。它可以把访问逻辑从不同类型的集合类中抽象出来,从而避免向客户端暴露集合的内部结构。示例代码如下:

for(Iterator it = c.iterater(); it.hasNext(); ) { ... }

 

可见,it只有在for循环语句内部才是可以访问的。在for循环外部,由于没有不再使用的变量会从局部变量表中清除,所以it局部变量被清除,同时对象不再被引用,可以被垃圾收集。 

 

 

 

一些对象存活时间越长,就越向右进行分布。例如,典型的例子是,一些在初始化时就被分配并一直存活到程序退出的对象。在这两个极端之间的是一些在中间计算中所存活的对象,就是这里那个峰值右边的区域。尽管一些应用有不同的分布情况,但大多数应用都符合这个通用图形。通过关注大多数对象的infant
mortality
进行有效的收集是可能的。

 

为此,内存是分代管理的:内存池对不同代中的对象进行管理。GC是在每代中内存池满的时候进行的:如上图中竖线所示。对象分配在Eden中,那是多数初期对象变成垃圾的地方。当Eden
填满时,将会引起minor collection,在其中的存活的对象将会移动到tenured generation中。当tenured generation需要去收集的时候,那就是major collection,通常会比较慢。因为它包含了所有存活的对象。

 

上图显示了一个调整好的系统,在该系统中,大多数对象在第一次的垃圾收集前就销毁掉了。一个对象活动时间越长,经历GC的次数就越多,GC速度就越慢。通过控制大多数对象存活不到一次收集就销毁,可使GC变得十分有效。但是,如果对象有异常生命周期分布,或者generation的大小设置不当引起频繁gc,这种令人满意的情况就会被破坏。默认的GC参数对大多数小型应用都是有效的。对于许多服务器应用,它们并不是最佳参数。这就引出了这篇文档的主旨:如果GC成为瓶颈,你可以定制代的大小。检查详细的GC输出,研究
GC
参数对性能的影响。

 

默认情况下的分代排列如下图所示: 

 

 

 

在进行初始化的时候,最大的地址空间只是事实上的设定,在实际需要的时候,才分配物理内存。全部的地址空间分成young generationtenured generation

 

young generation包括Eden和两个survivor
spaces
。对象最初分配在Eden中。其中保证一个送survivor spaces在任何时候都是空的,当垃圾收集发生时, Eden中的存活的对象复制到survivor spaces,此后该对象就在survivor spaces之间复制,直到到达最大阀值(老化),然后复制到tenured
generation
。(其它的虚拟机,包括JVM 1.2版本 For Solaris,使用两个大小相等的空间来复制,而不是使用一个大的Eden加两个小空间)。这就是说定义young generation
参数,并不能直接可比较的。

 

tenured generation在合适的时候,使用Mark-compact方式进行收集。名为永久代选项比较特别,因为它保存包括JVM
自身的所有反映数据(reflective data),例如类以及方法。所以永久域的另外一个名字被称为方法区。

 

(二)GC性能指标

衡量GC性能有两个指标。Throughput是未消耗在垃圾收集的时间占总时间的百分比,Throughput包括花在分配上的时间(不包括调整分配速度的时间),停顿(Pauses)是应用因为垃圾收集而停止响应的时间。

 

用户对于垃圾收集有不同的需求,例如,对于web服务器的主要衡量标准是Throughput,因此垃圾收集所造成的停顿并非是不可容忍的,因为用户可能认为是网络延时而已。
但是,对于交互式图形程序,哪怕是非常短暂的延迟也会影响用户的使用体验。

 

一些用户对于其他一些考虑敏感,Footprint是处理的工作区,用页面和cache line
作为尺度测量.在有限的物理内存或许多处理器的系统上,footprint
可以显示伸缩性.Promptness是从对象死亡到对象占用的内存变得可用之间的时间间隔.另一个对于分布时系统比较重要的考量标准是远程方法调用(RMI)

 

一般来说,选择某个代大小时要平衡考虑各种考虑因素.例如,一个非常大的young generation也许会最大化throughput,但是以footprint,promptness为代价的,也就是说会引起长时间的pause。小的young
generation
incremental collection可以使停顿时间的减少,但是以牺牲Throughput为代价的。

 

没有一种正确的方式去衡量代的大小:最好的选择是由应用使用用户需要的内存。因此,JVM
默认的GC可能并不是最好的,可以由用户使用命令行参数去覆盖。

 

(三)GC测量方法

Throughputfootprint是最好的标准,最好使用对于应用来说特定的手段测量。例如,一个web
server
Throughput可以使用客户端的压力负载工具来测试,同时在Solaris操作系统上,服务器的footprint可以用pmap命令来衡量。换句话说,由于GC而停顿,很容易由于JVM自己的诊断输出来得到。

 

命令行的参数: -verbosegc
显示了每次收集时的打印的信息。例如,这里时从大型的服务器应用中输出结果:

 [GC 325407K->83000K(776768K),
0.2300771 secs]

 [GC 325816K->83372K(776768K), 0.2454258 secs]

[Full GC 267628K->83769K(776768K), 1.8479984 secs]

上面,我们看到两个minor collection和一个major collection。箭头前后的数

325407K->83000K(776768K)

显示了GC前后活动对象空间的大小。值得一提的是,在minor collection之后,这个数字包括不再需要存活但是不能被回收的对象所占用的空间,因为它们或者是活动的,或者是被tenured
generration
中的对象所引用)。括号里的数目0.2300771 secs是总空闲空间的大小,它是堆的总的大小减去一个survivor spaces。总空闲空间中不包括永久域。第三行中输出的major collection的信息格式和上面类似。标示-XX:+PrintGCDetails
会输出一些额外的信息,具体的信息和jvm的版本有关。以下是在1.4.2版本中带有-XX:+PrintGCDetails标示时输出的情况:

[GC [DefNew: 64575K->959K(64576K), 0.0457646 secs] 196016K->133633K(261184K), 0.0459067 secs]]

上面的信息指示minor collection回收了将近98%的young generation,使用了将近46毫秒的时间(0.0457646
secs)
。整体堆的使用量减少到51%左右(196016K->133633K(261184K))。另外有一个另外的整体时间统计,稍微大于发生在young generationgc时间。另外有一个另外的整体时间统计,稍微大于发生在young generationgc时间。标示-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]

gc在系统启动111秒之后发生,minor
collection
同时启动,有一些关于major collection的额外的信息。major collectiontenured generation的使用率降低到10(18154K->2311K(24576K))。并且使用了将近0.13秒的时间(0.1290354
secs)

 

(四)调整Sun JVM代的大小

很多的参数都会影响分代的大小。下面的这副图举例说明了调整JVM1.3.1最重要的一点。许多参数实际用比率来表示xy,
分别用黑色部分(用x来表示),灰色部分(用y来表示)来显示。

 

 

 

堆的整体大小

generation被占满的时候,相对应的收集就发生了,throughput与可用内存的数量成反比,总可用内存是影响垃圾收集性能最重要的因素。默认情况下,JVM在每次收集之后,增长或减少堆,来保持可用的内存和活动对象的比例。通过参数-XXMinHeapFreeRatio=<minimum>
-XXMaxHeapFreeRatio=<maximum>,这个范围被设定为一个百分率,总大小在-Xms
-Xmx 之间。

Solaris上的默认参数,显示如下:

-XXMinFreeHeapRatio=40

-XXMaxHeapFreeRatio=70

-Xms 3584k

-Xmx  64m

 

大型的服务器应用经常经历两个问题。一个是启动很慢,因为初始化的堆很小,必须通过多次的major collectiosns
后调整大小。更严重的问题是默认的maximum 堆大小是对于大多数的服务器应用是不合适的。服务器应用的设置规则是:

除非有停顿问题,否则尽量设置JVM更多的内存。默认情况下,64M太小了。设置-xms
-xmx值一样大。确定去提高内存,正像你提高线程数一样,尽管GC不是并行的,但分配内存可以并行,所以在增加处理器的时候确保增加内存。

 

Young generation

第二个影响性能的问题是young generation的大小。young generation越大,minor collections将会经常发生。然而,由于堆的整体大小是固定的,young
generation
越大,tenured generation越小,越会增加major collections的执行的次数。所以最佳的选择是由对象的生命周期分布所决定。

 

默认情况下,年轻代是由NewRatio参数所决定的。例如,设置 –XXNewRatio=3
意思是tenured generationyoung generation的比例是3。换句话说,Edensurvivor spaces组合大小是整个堆的1/4

 

参数NewSizeMaxNewSize设置young generation的最小和最大值。设置这两个值相等,就固定了young
generation
,正像设置-xms ,-xmx相等,就固定了整个堆的大小一样。

 

因为young generation使用复制收集,在tenured generation中必须有足够大的内存大小,才能保证minor
collections
进行。在最基本的情况下,这个值至少等于Eden的大小加上非空的survivor spaces的大小。如果在tenured generation中没有足够的内存,major collections将会发生。对于一些小应用,这种规则是很好的,因为在tenured
generation
保留的内存只具有形式意义,只是虚拟上的使用,而不是实际使用。但是对于需要更大堆的应用,超过虚拟堆大小一半的Eden是没有用的,因为这种情况下只有major collections会发生。

 

如果需要,参数SurvivorRatio被用来调整survivor spaces,但是对于性能这是不重要的。例如,设置
6设置每个survivor spacesEden的比例是16;换句话说,每个survivor spaces将是young generations1/8。(不是1/7,因为有两个survivor
spaces

 

如果survivor spaces太小,拷贝收集直接溢出到tenured generation,如果幸存空间太大,它们将无用地空着。虚拟机会适当选择对象在老化前能被拷贝的次数。并利用这个次数始终保持survivor
spaces
半空,对象达到拷贝次数之后会被提升到tenured generation。选项XX+PrintTenuringDistribution被用来显示这个次数,和new generation中对象的年龄。它也可以用来发现应用的对象生命分周期布。

 

这儿是Solaris操作系统上默认值:

NewRatio        2(client JVM:8)

NewSize       
 2172k

MaxNewSize    32m

SurvivorRatio    25

 

那么,服务器应用规则如下:首先决定可以提供给虚拟机的总内存,然后根据young generation的大小绘制你自己的性能曲线,找到最好的设置。不要young
generation
让达到总堆大小的一半,那样做并不提高性能。增加处理器的数量时,请确保增加 young generation
,因为分配可以并行。

 

垃圾收集器

在前面提高,分代收集器是一些垃圾收集器算法的总称。默认情况下,每个分代有一个相关联的GC类型,在1.3.1中,JVM实现了三种不同的GCCopying(有时,称为清扫):这个收集者可以有效的在两个或多个分代中进行对象的移动。原分代变空,可以将遗留的对象销毁。然而,Copying需要空间去操作,并需要拷贝所需的footprint

 

1.3.1,复制收集用于所有的minor collections.也就是说复制收集针对young generationMark-compact:这个收集者允许分代在适当的时候进行分配,而不需要额外的内存。然后,这种紧凑的比复制方式,速度上要慢一些。在1.3.1中,紧凑标记的方式主要用于major
collection.
也就是说针对tenured generationIncremental:只有在命令行中设置了 -Xincgc之后,这种收集方式才起作用。借助于详细的记录,递增式的GC一次只能收集tenured generation的一部分,在多次minor
collections
之后,才尝试进行major collections。然而,如果考虑所有的Throughput的话,这种方式比紧凑标记的速度还要慢。

 

JDK 1.4.1中,为了解决多处理器系统中垃圾收集器的问题,增加了2种具备并行处理功能的新的垃圾收集器。分别是并行复制收集器(throughput
collector)
和并发标记-清除收集器(Concurrent Low Pause Collector),这些并行收集器被设计为减少收集暂停时间或者是在提高在大堆上的吞吐能力而设计的。

 

并行复制收集器和并发标记-清除收集器基本上是默认的复制收集器和标记-整理收集器的并发版本。并行复制收集器:用 JVM
选项 -XX:+UseParNewGC 启用,是一个发生在young generation的并行版本的复制收集器,它将垃圾收集的工作分为与 CPU
数量一样多的线程。针对多处理器系统上非常大(G字节以及更大的)堆进行了优化。在指定-XX:+UseParNewGC参数时,tenured generation使用默认的Mark-compact收集器。

 

并发标记-清除收集器:由 -XX:+UseConcMarkSweepGC
选项启用,它是一个发生在tenured generation的并发版本标记-清除收集器,它在初始标记阶段(及在以后暂短重新标记阶段)暂短地停止整个系统,然后恢复用户程序,同时垃圾收集器线程与用户程序并发地执行。特别的是,如果在命令行指定了-XX:+UseConcMarkSweepGC参数,那么UseParNewG也被缺省设置为true,如果没有进行特别指定的话。

 

增量收集选项自 1.2
起就成为 JDK 的一部分。增量收集减少了垃圾收集暂停,以牺牲吞吐能力为代价,这使它只在更短的收集暂停非常重要时才值得考虑,如接近实时的系统。要使用增量收集使用-Xincgc参数记住,-XX:+UseParallelGC参数不能和-XX:+UseConcMarkSweepGC参数同时在命令行中使用,这是因为进程对命令行参数进行解析的机制造成的。同时设置这两个参数的结果是不确定的。 

 

 

 

 

何时使用Throughput Collector

在多处理器环境下,如果希望提升应用程序的性能,可以考虑使用Throughput Collector。默认的垃圾收集行为是由一个线程完成的,垃圾收集行为增加了了应用程序完成工作花费的时间。Throughput
Collector
使用多个线程来完成minor collection,因此减少了时间花费。一个典型案例就是应用程序中有许多线程都要申请对象,这样的应用程序往往会需要一个很大的young generation,在这种情况下,使用Throughput Collector是非常合适的。

 

抱歉!评论已关闭.