调整JVM ( v1.3.1) GC
关键词:
gc:garbage collection(垃圾收集)
infant mortality:对象分配以后很快成为垃圾,就称该对象具有“infant mortality”
minor collection:较小收集
major collection:较大收集
older generation:年老代
young generation:年轻代
footprint:是一批工作进程的集合,以页和缓冲行数计量,在物理内存有限或者有很多处理器的系统里,footprint 可代表伸缩性
survivor spaces:生存空间
eden:新的对象分配的地方
throughput:是未消耗在垃圾收集的时间占总时间的百分比
简介:
Java 2平台越来越多的应用于大型的服务器应用,如web services。这类应用要求有可扩展性,并直接受益于多线程,多处理器,sockets以及内存。然而,“big iron”性能被誉为一种艺术形式,并需要特殊技术,这种技术超出改善小型系统性能所需要的技术。幸运的是,JVM和Solaris操作环境提供了线程、I/O和内存管理的有利条件。这篇文档阐述了在获取高性能的过程中所遇到的难题:GC难调。
Amdahl发现大部分的工作不能被很好地并行化:某些工作总是串行的,但是并不能从并行化获得好处。Java2平台也是这种情况。特别是,JVM 1.3.1及以上版本没有并行GC,所以相对于并行收集的应用,在多处理器系统的GC的影响会增长。
下图显示一个完美的理想系统,除GC外,具有良好的伸缩性。最上面的线(红色),反映了在单处理器上,只花1%时间在GC上的应用情况:这可以理解为,在32个处理器上,将会损失至少20%的Throughput。到10%时,如果不考虑单处理器应用中GC所用大量时间,那么损失的Throughput将会超过75%。
这就证明了当GC花费时间比例增大的时候,在小型系统应用上所损失的Throughput可能会成为瓶颈问题。唯一的希望就是,对这个瓶颈问题的一点小改进能获得很高的性能。对于一个大型的系统来讲,调整GC则是值得的。
这篇文档描述的是Solaris(SPARC Platform Edition)操作环境中的JVM 1.3.1,因为这个平台提供了当今Java2平台最具伸缩性的软硬件环境。然而,这些描述的文字同样适用于其他的平台,包括Linux,Windows,Solaris(Intel Architecture)操作环境,以达到升级硬件的最大可用程度。尽管命令行选项适用于大部分的平台,但是一些平台可能有与这里所述不同的缺省值。
分代收集:
Java 2 平台一个很强的特性之一就是屏蔽内存分配和GC的复杂性。然而,一旦GC成为瓶颈,那么就要理解所隐藏实现的细节。垃圾收集器对应用使用对象的方式作了限定,这些限定就反映在可调整参数中。这些参数可以被调整,在不牺牲抽象能力情况下获取更高的性能。
在一个运行的程序中,如果一个对象不再有任何引用,那么它将成为“垃圾”。大部分GC算法简单就是对每个可获取对象进行遍历:任何被遗弃的对象,将成为“垃圾”。这种算法所花的时间和实际活动对象的数量成比例,但对于具有大量活动数据的大型应用,就不可行了。
JVM v1.3 集许多不同的GC算法为一体,这些不同的算法是通过分代收集结合在一起的。当GC在Heap中检查每一个活动的对象时,分代收集利用大多数应用的几个属性来避免额外的工作。
这些属性中,最重要的是infant mortality(对象分配以后很快成为垃圾)。下图中蓝色区域显示了对象生命周期的典型分布。左边的峰值代表在分配之后能很快收集的对象。例如,重复对象(Iterator objects)在一个单循环期间,经常是活动的。
一些对象存活时间越长,就越向右进行分布。例如,典型的例子是,一些在初始化时就被分配并一直存活到程序退出的对象。在这两个极端之间的是一些在中间计算中所存活的对象,就是这里那个峰值右边的区域。尽管一些应用有不同的分布情况,但大多数应用都符合这个通用图形。通过关注大多数对象的infant mortality进行有效的收集是可能的。
为此,内存是分代管理的:内存池对不同代中的对象进行管理。GC是在每代中内存池满的时候进行的:如上图中竖线所示。对象分配在Eden中,那是多数初期对象变成垃圾的地方。当Eden 填满时,将会引起minor collection,在其中的存活的对象将会移动到older generation中。当older generation需要去收集的时候,那就是major collection,通常会比较慢。因为它包含了所有活动的对象。
这图显示了一个调整好的系统,在该系统中,大多数对象在第一次的垃圾收集前就销毁掉了。一个对象活动时间越长,经历GC的次数就越多,GC速度就越慢。通过让大多数对象存活不到一次收集就销毁,可使GC变得十分有效。但是这种令人满意的情况,在具有不寻常的生命周期分布的应用中或造成收集频繁的大小不合适的代中就会被破坏。
默认的GC参数对大多数小型应用都是有效的。对于许多服务器应用,它们并不是最佳参数。这就引出了这篇文档的主旨:
如果GC成为瓶颈,你可以定制代的大小。检查详细的GC输出,研究 GC 参数对性能的影响。
收集的类型:
每个分代有一个相关联的GC类型,这些类型的GC可以进行配置,产生不同的算法时间,空间,中止交易。在1.3中,JVM实现了三种不同的GC:
1, Copying(有时,称为清扫):这个收集者可以有效的在两个或多个分代中进行对象的移动。原分代变空,可以将遗留的销毁对象进行回收。然而,需要空间去操作,并拷贝所需的footprint。在1.3.1,复制收集用于所有的minor collections.
2, Mark-compact:这个收集者允许分代在适当的时候进行分配,而不需要额外的内存。然后,这种紧凑的比复制方式,速度上要慢一些。在1.3.1中,紧凑标记的方式主要用于major collection.
3, Incremental:(有时称为序列)。只有在命令行中设置了 -Xincgc之后,这种收集方式才起作用。借助于详细的记录,递增式的GC一次只能收集older generation的一部分,在多次minor collections之后,才尝试进行major collections。然而,如果考虑所有的Throughput的话,这种方式比紧凑标记的速度还要慢。
因此,复制方式是最快的,在收集时尽量使用这种方式来收集对象。
默认情况下的分代排列如下图所示:
在进行初始化的时候,最大的地址空间只是事实上的设定,在实际需要的时候,才分配物理内存。全部的地址空间分成young generation和older generation。
young generation包括Eden和两个survivor spaces。对象最初分配在Eden中。其中保证一个送survivor spaces在任何时候都是空的,当垃圾收集发生时, Eden中的活的对象复制到survivor spaces,对象就在survivor spaces之间复制,直到到达最大门限值(老化),然后复制到older generation。
(其它的虚拟机,包括JVM 1.2版本 For Solaris,使用两个大小相等的空间来复制,而不是使用一个大的Eden加两个小空间)。这就是说定义young generation 参数,并不能直接可比较的。
older generation在合适的时候,使用Mark-compact方式进行收集。名为永久代选项比较特别,因为它保存包括JVM 自身的所有反映数据(reflective data),例如类以及方法。
性能指标
衡量GC性能有两个指标。Throughput是未消耗在垃圾收集的时间占总时间的百分比,Throughput包括花在分配上的时间(不需要调整分配的速度),停顿(Pauses)是应用因为垃圾收集而停止响应的时间。
用户对于垃圾收集有不同的需求,例如, 对于web服务器的主要尺度是Throughput,因为垃圾收集的停顿也许是不可容忍的,或者只是被网络延时所遮盖。 然而,对于交互式图形程序,短暂的延迟也会影响用户的体验。
一些用户对于其他一些考虑敏感,Footprint是处理的工作区,用页面和cache line 作为尺度测量.在有限的物理内存或许多处理器的系统上,footprint 可以显示伸缩性.Promptness是对象死亡和内存可用之间的时间.另一个对于分布时系统比较重要的考量标准是远程方法调用(RMI)。
一般来说,选择某个代大小时要平衡考虑各种考虑因素.例如,一个非常大的young generation也许会最大化throughput,但是以footprint,promptness为代价的。小的young generation和incremental collection可以使停顿时间的减少,但是以牺牲Throughput为代价的。
没有一种正确的方式去衡量代的大小:最好的选择是由应用使用用户需要的内存。因此,JVM 默认的GC可能并不是最好的,可以由用户使用命令去覆盖。
测量方法
Throughput和footprint是最好的标准,最好使用对于应用来说特定的手段测量。例如,一个web server的Throughput用一个客户端的来测试,同时在Solaris操作系统上,服务器的footprint可以用pmap命令来衡量。换句话说,由于GC而停顿,很容易由于JVM自己的诊断输出来得到。
命令行的参数: -verbose:gc 显示了每次收集时的打印的信息。例如,这里时从大型的服务器应用中输出结果:
[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。箭头前后的数字显示了GC前后活动对象合并大小。(在minor collections之后,这数目包括不再需要存活的对象,但是不能被回收,因为它们是活动的,或因为在older generration中还被引用)。括号里的数目是总共可获取的空间大小,它是堆的总的大小减去一个survivor spaces。
确定代的大小
很多的参数都会影响分代的大小。下面的这副图举例说明了调整JVM1.3.1最重要的一点。许多参数实际用比率来表示x:y, 分别用黑色部分(用x来表示),灰色部分(用y来表示)来显示。
总堆
当代满的时候,收集就发生了,throughput与可用内存的数量成反比,总可用内存是影响垃圾收集性能最重要的因素。
默认情况下,JVM在每次收集之后,增长或减少堆,来保持可用的内存和活动对象的比例。通过参数-XX:MinHeapFreeRatio=<minimum> 和 -XX:MaxHeapFreeRatio=<maximum>,这个范围被设定为一个百分率,总大小在-Xms 和 -Xmx 之间。
在Solaris上的默认参数,显示如下:
-XX:MinFreeHeapRatio= |
40 |
-XX:MaxHeapFreeRatio= |
70 |
-Xms |
3584k |
-Xmx |
64m |
大型的服务器应用经常经历两个问题。一个是启动很慢,因为初始化的堆很小,必须通过多次的major collectiosns 后调整大小。更严重的问题是默认的maximum 堆大小是对于大多数的服务器应用是不合适的。
服务器应用的设置规则是:
除非有停顿问题,否则尽量设置JVM更多的内存。默认情况下,64M太小了。设置-xms 和-xmx值一样大。换句话说,如果你做不好的决定,JVM是不会做补偿的。
确定去提高内存,正像你提高线程数一样,尽管收集不能平放,但是GC是不平放。
尽管GC不是并行的,但分配内存可以并行,所以在增加处理器的时候确保增加内存。
年轻代
第二个最有影响的问题是堆和年轻代。young generation越大,minor collections将会经常发生。然而,对于一个有限的堆大小,older generation越小,越会增加major collections的执行的次数。最佳的选择是由分布式应用的生命周期所决定。
默认情况下,年轻代是由NewRatio参数所决定的。例如,设置 –XX:NewRatio=3 意识是young generation和older generation的比例是1:3。换句话说,Eden的和survivor spaces组合大小是整个堆的1/4
参数NewSize和MaxNewSize设置年轻代的最小和最大值。设置这两个值相等,就固定了young generation,正像设置-xms ,-xmx相等,就固定了整个堆的大小一样。
因为young generation使用复制收集,在old generation中必须有足够大的内存大小,才能保证minor collections进行。在最坏的情况下,这个值等于Eden的大小加上非空的survivor spaces的大小。如果在old generation中没有足够的内存,major collections将会发生。对于一些小应用,这种规则是很好的,因为在old generation保留的内存具有代表性,只是虚拟上的使用,而不是实际使用。但是对于需要更大堆的应用,超过虚拟堆大小一半的Eden是没有用的:只有major collections会发生。
如果需要,参数SurvivorRatio被用来调整survivor spaces,但是对于性能这是不重要的。例如,设置 =6设置每个survivor spaces和Eden的比例是1:6;换句话说,每个survivor spaces将是young generations的1/8。(不是1/7,因为有两个survivor spaces)
如果survivor spaces太小,拷贝收集直接溢出到olde generation,如果幸存空间太大,它们将无用地空着。 每一次垃圾收集,虚拟机选择一个起始数量次数被拷贝的对象在其被以前。这个被选择的开始保持survivor spaces半空。选项XX:+PrintTenuringDistribution被用来显示这个起始,和new generation中对象的年龄。它也可以用来发现应用的对象生命分布。
这儿是Solaris操作系统上默认值:
NewRatio |
2 (client JVM: 8) |
NewSize |
2172k |
MaxNewSize |
32m |
SurvivorRatio |
25 |
那么,服务器应用规则如下:
首先决定可以提供给虚拟机的总内存,然后根据young generation的大小绘制你自己的性能曲线,找到最好的设置。
除非有停顿问题,否则尽量设置JVM更多的内存。默认情况下,64M太小了。
达到总堆大小的一半或少些时,增加 young generation 并不提高性能。
增加处理器的数量时,请确保增加 young generation ,因为分配可以并行。
其它的事项
对于大多数应用,permanent generation与垃圾收集器的性能无关,然而一些应用动态产生和装载许多类.如JSP页面.如有必要,用MaxPermSize增加permanent generation的大小。
一些应用通过finalization和weak/soft/phantom引用于垃圾收集器交互.这些特性在color: black; letter-spacing: 0.45p