dev2dev.bea.com.cn
首页 资源中心 dev2dev学堂 在线技术论坛 专家Blog User Group CodeShare

Sun HotSpot JVM 1.4.2 调优

2008-01-14 09:41:34 | 评论 (4) | 被访问(491)次

袁玉强
  袁玉强,Dev2Dev ID:JadeYuan 熟悉WebLogic Server,WebLogic Integration及Tuxedo产品。


Sun HotSpot JVM 1.4.2 调优

目录

1. 序言

2. 虚拟机中的""

2.1. 性能考虑

2.2. 测量

3. 调整代大小

3.1. 堆的总体大小

3.2. 新生代

3.3. 新生代的保证

4. 收集器类型

4.1. 何时使用吞吐收集器

4.2. 吞吐收集器

4.2.1. 大小自适应

4.2.2. Aggressive

4.2.3. 吞吐收集器的测量法

4.3. 何时使用并发收集器

4.4. 并发收集器

4.4.1. 并发的额外开销

4.4.2. 新生代的保证

4.4.3. 完全收集

4.4.4. 漂浮垃圾

4.4.5. 暂停

4.4.6. 并发阶段

4.4.7. 并发收集的测量

4.4.8. 并发收集器的并行次要收集

4.5. 何时使用增量收集器

4.6. 增量收集器

4.6.1. 增量收集器测量

5. 其他考虑事项

6. 总结

7. 其他文档

7.1. 输出示例

7.2. 常见问题


1. 序言

从小型桌面应用到运行在大型服务器的web服务,Java 2平台被广泛的应用。在1.4.1版本的J2SE平台中,引入了2种新的垃圾收集器garbage collector),现在总共有4种垃圾收集器供我们选择。那么,应该如何选择垃圾收集器?又有哪些因素可以作为选择的依据?本文档描述了垃圾收集器共有的特征,并对于在单线程,stop-the-world的收集器上如何最大限度的利用这些特征给出了调整指导。最后,讨论了其他3种收集器独有的特征以及在4种垃圾收集器中做出选择的一些标准。

对于用户而言,在什么情况下,垃圾收集器会带来性能问题?对于大部分应用来说,垃圾收集器不会带来性能问题,也就是说,即使适垃圾收集器的运行会带来很短的暂停,但是应用还是可以以令人满意的性能运行。但是对于使用了大量的线程、处理器、Socket以及内存的大型应用而言(使用默认的垃圾收集器),情况就不同了。

Amdahl 观察到,大部分工作是无法很好的并行处理的,有些工作总是顺序化的,所以,无法从并行机制中获益。对于J2SE平台来说,也是这样,Java虚拟机从最初一直到1.3.1版本,都没有并行的垃圾收集器,所以,相对于其他的并行应用,垃圾收集器所带来的性能影响在多处理器系统上会有所增长。

下图描述的是一个具有良好弹性的理想系统,如果不考虑垃圾收集的话。红线表示在单处理器系统上的应用,垃圾收集只占用1%的时间,当迁移到有32个处理器的平台上的时候,有超过20%吞吐量throughput)的损失;在单处理器系统上垃圾收集时间占10%的应用(不考虑那些令人无法接受的垃圾收集时间),当扩展到32处理器系统上时,吞吐量的损失超过了75%

这表明了,在开发小型应用时可以忽略不计的速度问题,当扩展到大型应用系统时却成为最为突出的性能瓶颈。但是对于这种瓶颈的小小的改良就可能收获性能上的显著提高,所以,对于大型应用来说,调整垃圾收集器是非常有价值的。

对于大部分应用来说,默认的垃圾收集器都是可以满足需要的。其他的几种垃圾收集器因为有一些特殊的行为,使用起来比较复杂。除非应用有特殊需求,一般情况下建议选用默认的垃圾收集器。当然,也有例外情况,对于那些运行在有大量内存和处理器的主机上的大型应用,可以首先尝试aggressive 堆(heap)选项(-XX:+AggressiveHeap),稍后有详细描述。

本文选用J2SE 1.4.2Sun HotSpot),Sun Solaris操作系统(SPARC平台版本),因为这个平台对于硬件和软件来说都有很好的扩展性。当然了,本文也适用于其他平台,如 LinuxMicrosoft Windows Sun SolarisX86平台版本)。尽管在不同平台上虚拟机的命令行参数是一样的,但是可能其他平台上的默认值和本文描述的有所差别。

2. 虚拟机中的"代"

J2SE平台的一个特征就是它为开发人员实现了内存的申请和释放,但是,当垃圾收集成为主要的性能瓶颈时,我们就有必要对它的实现进行深入的了解。垃圾收集器可以对应用使用对象的方式进行一个设定,反映在一些可调整的参数上,通过这些参数,在保证抽象能力的前提下提升了虚拟机的性能。

从运行的程序中,没有可以到达某一对象的指针,那么这个对象就被认为是"垃圾"。最直截了当的垃圾收集算法就是枚举每个可到达的对象,剩下的对象就是可以进行回收的垃圾对象了。这种方案所消耗的时间和活动对象的数量是成比例的,所以,对于要保持大量活动对象的大型应用,显然是不适用的。

J2SE 1.2开始,虚拟机将几种不同的垃圾收集算法组合在一起使用,就是"分代收集"(generational collection)。在幼儿收集器naive garbage collection)检查堆中活动对象的同时,分代收集器通过分析一些观测属性来避免额外的工作。

在这些观测属性中最重要的就是幼儿死亡率infant mortality)。下面图表中的蓝色区域就是对象存活期的典型分布。其中X轴表示对象的存活时间,在分配字节时测量。Y轴表示的是对应存活时间的对象字节总量。左侧的顶点表示对象可能在分配之后很短的时间内就被回收,例如在循环中的枚举对象,仅仅能够存活一个循环。

有些对象能够存活很长时间,所以上图的X轴向右延展。例如那些在初始化时就创建并且能够存活直到进程终止的对象。在这两个极端之间,就是一些只在中间计算才存活的对象,就是我们看到的到幼儿死亡率右侧的区域。可能有些应用的对象存活周期分布有些差异,但令人惊讶的是大部分应用都和上图吻合。通过关注主要的"夭折"(die young)对象来进行垃圾收集是行之有效的。

为了针对这种情况进行优化,所以将内存进行分代管理,也就是说内存池保持不同年龄段的对象。在分代管理的内存中,当一个代被对象充满的时候,就在这个代上进行垃圾收集。对象是在新生代young generation)区域中进行分配,因为大部分对象在这个代中就已经死亡。当新生代被填充满之后,在其上进行一次次要垃圾收集minor collection),由于在新生代有很高的夭折率,所以次要收集算法针对这种情况进行了优化。这种收集的消耗和被其收集的对象的数量成比例。充满死亡对象的新生代收集起来非常的快。一些仍然存活的对象被移动到旧生代tenured generation),当旧生代需要进行垃圾收集的时候,就会进行主要垃圾收集major collection),这种收集方式比起次要收集来要慢得多,因为它涉及了全部的活动对象。

下图显示,次要收集的间隔时间比较长,这样能够确保大部分对象都已经死亡。为了确保次要收集发生的间隔能够足够长,需要新生代有足够的空间,这样才能够使得次要收集充分利用新生代中高夭折率的特点。对于那些对象存活周期分布比较奇特的应用情况就不同了,还有,不合适的代大小设置也会使得在对象死亡之前就不得不进行收集。

默认的垃圾收集器可以应用在小型的和大型的应用,但是它的默认参数设置对于小型应用更高效。对于很多服务器应用来说,这些参数并不合适,这就引出了本文的中心原则:

如果垃圾收集成为性能瓶颈,建议你调整代的大小参数。并检查垃圾收集的输出信息,研究性能对于垃圾收集参数的敏感度

默认的代配置如下图所示:

在初始化时,虚拟机保持一个很大的地址空间,但并不真正的从物理内存中申请(按需申请)。用来存放对象的地址空间被划分成两部分:新生代和旧生代。

新生代由一个Eden和两个存活空间survivor space)组成,对象在Eden中进行创建和内存分配。在任何时候,都有一个存活空间是空的,以用来容纳从Eden和另外一个存活空间复制过来的活动对象。对象以这种方式在两个存活空间之间进行复制,直到它存活时间较长而被复制到旧生代。

包括1.2版本的虚拟机(Solaris操作系统)在内的其他虚拟机产品,都是使用两个大小相等的存活空间来进行对象复制,而不是像图中所示的一个Eden和两个存活空间。这就意味着,通过调整新生代大小来调整性能并不总是具有可比性的。

在旧生代中,有一部分比较特殊的,叫做持久代permanent generation),它用来存放虚拟机自己的数据,例如class)和方法method)。

2.1. 性能考虑

对于垃圾收集的性能,主要有两个指标。吞吐量throughput)指用来进行垃圾收集之外工作所用的时间占总时间的百分比,一般要通过长时间的观察和测量。吞吐量包括了分配内存所花费的时间在内(一般来说无需对分配进行调优)。暂停Pause)指由于进行垃圾收集而导致应用无法响应的时间。

用户对于垃圾收集有不同的需求,例如,对于Web服务器应用来说,吞吐量是要着重考虑的,而暂停时间可能由于网络的反应时间而不那么明显;而对于一个交互式图形界面的应用来说,即使是短暂的暂停都会带来非常不好的用户体验。

通常来说,如何设置代的大小是在这些考虑因素之间作出的一个权衡。例如,将新生代设置得很大会得到很好的吞吐性能,但是会增加暂停时间;反之,较小的新生代设置会减小暂停时间,但是降低了吞吐量。一个代的大小不应该影响在其他代上进行垃圾收集的频率和暂停时间。

对于代的大小设置,没有一个精确的计算方法。同时考虑应用对内存的使用特征以及用户对垃圾收集的需求,才能够做出最好的选择。因此,虚拟机默认的垃圾收集的相关参数可能不是最优的,需要通过用户定制这些命令行选项加以调整。

2.2. 测量

对于特定的应用来说,吞吐量和内存占用还是比较容易测量的。例如对于一个web服务应用,可以使用压力测试工具来测量它的吞吐量,内存占用可以使用Solaris操作系统提供的命令pmap来测量。另外,通过打开虚拟机的详细诊断信息来估算垃圾回收的暂停时间。

通过使用虚拟机选项-verbose:gc,能够将每次收集时的一些信息打印出来。需要注意的是,对于不同版本的J2SE平台来说,-verbose:gc的输出格式可能稍有差异。下面的示例是一个大型的服务器应用的-verbose:gc输出。

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

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

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

上面的输出中,表明虚拟机进行了两次次要收集和一次主要收集。箭头两端的数字

325407K->83000K(第一行)

表示在进行垃圾收集之前和之后活动对象的总大小。在次要收集之后的数字(83000K)包含了那些并不是必要活动的对象,但是还不能被回收。因为这些对象可能本身是活动的,或者有来自旧生代的引用。括弧中的数字

(776768K)(第一行)

表示总共的可用空间的大小。这个大小不包括持久代在内,是堆的大小减去一个存活空间的大小。此次次要收集大约用了1/4

0.2300771 secs (第一行)

主要收集的输出格式和第三行相似。如果使用了-XX:+PrintGCDetail选项,就会将垃圾收集时的详细信息打印出来。同样,对于不同的J2SE平台版本,根据虚拟机的需要,-XX:+PrintGCDetail的输出格式也有所不同。下面就是在J2SE平台1.4.2版本上使用-XX:+PrintGCDetail参数的输出示例:

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

从上面的输出信息可以看出,次要收集释放了新生代大约98%的空间,

DefNew: 64575K->959K(64576K)

用了大约46毫秒,

0.0457646 secs

整个堆空间的使用率下降到51%左右

196016K->133633K(261184K)

最后看到,相对于新生代的收集,在时间消耗上稍微多了一点点,

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]

收集在111秒的时候开始,几乎同时次要收集也开始了。对于新生代上的主要收集也有了一些附加信息,新生代的使用率下降到10%

18154K->2311K(24576K)

用了大约0.13

0.1293306 secs

3. 调整代大小

很多参数都能够影响代的大小,下图表示的就是约定空间committed space)和虚拟空间virtual space)之间的不同。在虚拟机初始化的时候,整个空间都为堆保留。可以通过-Xmx参数指定整个空间的大小,如果-Xms参数的值比-Xmx小,那么在一开始,堆所保留的空间可能到不了-Xmx参数指定的值,那么未保留的这部分空间就被表示为虚拟的。在需要更大的空间的时候,堆的不同部分(新生代,旧生代,持久代)都可能增长直到其上限。

有些参数指定的是堆中一部分空间和另外一部分的大小比率,例如,NewRatio参数表示旧生代和新生代的比率。稍后会有关于这些参数的讨论。

3.1. 堆的总体大小

当某个代被充满的时候,就会发生垃圾收集,所以,吞吐量和可用内存的大小成反比,总内存大小是影响垃圾收集的最重要因素。

默认情况下,虚拟机总是试图收缩内存大小,来保证每个代内的活动对象所占用的空间的比率维持在特定范围,这个范围就是由参数-XX:MinHeapFreeRatio=<最小值>-XX:MaxHeapFreeRatio=<最大值>以及-Xms-Xmx来确定。在Solaris操作系统平台(SPARC版本)如下表所示:

-XX:MinHeapFreeRatio=

40

-XX:MaxHeapFreeRatio=

70

-Xms

3670k

-Xmx

64m

如果使用默认值,那么在某个代中,当空闲空间低于40%,那么虚拟机就会增长这个空间以使得空闲空间能够保持在40%以上,假设这个代的大小未超过最大限制。类似的,如果代中空闲空间的大小超过了70%,虚拟机就会收缩这个代的大小,使空闲比率降到70%以下,当然了,要高于40%

根据经验,这些默认的参数设置不适和大型的服务器应用,一个问题就是启动缓慢,因为堆的初始大小比较小,不得不进行多次的主要收集来释放空间,另外一个问题就是-Xmx参数的默认设置对于这些服务器应用来说是很不合理的,所以,对于服务器应用,有这样的指导规则:

· 除非在垃圾收集的时候暂停现象严重,否则要尽可能多的给虚拟机分配内存,64M通常都太小了

· 设置-Xms-Xmx一样大,以避免虚拟机在收缩大小时的消耗。另外,如果你做出了糟糕的选择,虚拟机无法补偿

· 当增加了处理器之后,要相应的增加虚拟机内存大小,因为内存分配是可以并行进行的

关于虚拟机的全部参数的解释,可以参考:

http://java.sun.com/docs/hotspot/VMOptions.html

3.2. 新生代

第二个重要的影响因素是虚拟机中新生代的大小。新生代越大,次要回收发生的次数就越少。当然,对于一定大小的堆来说,新生代越大,就意味着旧生代越小,那么就会使得主要回收的次数增多。最佳选择应该参考应用创建的对象的存活周期来设定。

默认情况下,新生代的大小是由NewRatio参数来设置的,例如设置-XX:NewRatio=3表示新生代和旧生代之间的比率为1:3,就是说Eden和存活空间总共占堆大小的1/4

参数NewSizeMaxNewSize指定了新生代大小的范围,设定这个参数就意味着限定了新生代的大小,就像使用-Xms-Xmx参数限制堆的大小一样。最好将新生代的大小设定为经过NewRatio参数计算出的大小的整倍数。

3.3. 新生代的保证

理想的次要收集会将活动对象从新生代的一部分(Eden和第一个存活空间)复制到另外一部分(第二个存活空间)。但是,无法保证第二个存活空间能够容纳全部活动对象。所以,在旧生代一定要有足够的空闲空间来容纳全部的活动对象。最糟糕的情况就是在旧生代的空闲空间为Eden大小加上非空存活空间的大小。当旧生代也没有足够的空闲空间的时候,就需要进行一次主要收集。对于小型应用来说,这种策略是可以满足需要的,因为在旧生代的内存主要是虚拟约定的,并未真正使用。但是对于需要很大堆内存的服务器应用来说,大于堆虚拟约定内存一半的Eden大小是没有任何意义的:只会发生主要收集。需要指出的是,在新生代能够使用除"吞吐收集器"(throughput collector)之外的其他类型的收集器。如果旧生代无法容纳新生代复制过来的活动对象,吞吐收集器会在这个两个代上都进行垃圾收集。

如果有特殊需要,使用参数SurvivorRatio可以调整存活空间的大小,但是通常这个参数对性能的影响不那么重要。例如-XX:SurvivorRatio=6设定了每个存活空间和Eden的比率为1 :6,换句话说,就是每个存活空间占新生代大小的1/8(不是1/7,因为有两个存活空间)。

如果存活空间设置的太小,就会导致过于频繁的向旧生代复制对象;如果存活空间设置的太大,就会因为空闲而白白浪费。虚拟机的垃圾收集器都会为对象在复制到旧生代之前的反转复制次数选择一个阀值,通过设定这个阀值来保证存活空间一直处于半满状态。通过指定-XX:+PrintTenuringDistribution参数可以观察到这个阀值,以及新生代中对象的"年龄"。同时,这个参数对于观察应用产生对象的存活期分布是很有帮助的。

下面是Solaris操作系统平台(SPARC版本)上一些参数的默认值:

NewRatio

2client虚拟机是8

NewSize

2228k

MaxNewSize

无限

SurvivorRatio

32

新生代的最大大小是根据堆的总大小以及NewRatio来计算出来的,默认的"无限"表示:除非在命令行中指定了MaxNewSize,否则该计算出来的值不限制MaxNewSize的大小。

对于服务器应用,有这样的指导规则:

· 首先确定能够给虚拟机使用的最大内存,然后通过调整新生代的大小来找到满足性能需求的最佳值

· 除非发现性能瓶颈在于频繁的主要收集或者暂停时间,尽可能多的给新生代分配内存

· 将新生代大小设置到接近堆大小的一半会适得其反

· 当增加了处理器之后,要相应的增加新生代大小,因为内存分配是可以并行进行的

4. 收集器类型

到目前为止,我们还只是针对默认的垃圾收集器展开讨论。从J2SE平台1.4.2版本开始,加入了另外3种垃圾收集器,都是着重提高吞吐能力,降低垃圾收集时的暂停时间。

1. 吞吐收集器(throughput collector):命令行参数:-XX:+UseParallelGC。在新生代使用并行收集策略,在旧生代和默认收集器相同。

2. 并发收集器(concurrent low pause collector):命令行参数:-XX:+UseConcMarkSweepGC。在旧生代使用并发收集策略,大部分收集工作都是和应用并发进行的,在进行收集的时候,应用的暂停时间很短。如果综合使用-XX:+UseParNewGC-XX:+UseConcMarkSweepGC,那么在新生代上使用并行的收集策略。

3. 增量收集器(incremental low pause collector):命令行参数:-Xincgc。使用增量收集器要谨慎,他只是在每次进行次要收集的时候对旧生代进行一部分的收集,这样就把主要收集所带来的较长时间的停顿分散到多次的次要收集。但是,考虑到总共的吞吐,可能比旧生代上默认的收集还要慢。

注意,-XX:+UseParallelGCXX:+UseConcMarkSweepGC不能同时使用。对于J2SE1.4.2版本会检查垃圾收集相关参数组合的合法性,但是对于之前的版本没有这个检查,可能会导致不可预知的错误。

在尝试使用其他的收集器之前,建议先使用默认的收集器,通过调整代的大小以及相关参数来看看哪些方面不能满足需求,通过以上信息提供的参考,再决定选择其他的收集器。

4.1. 何时使用吞吐收集器

当你的应用运行在多个处理器的主机上时,考虑使用吞吐收集器。因为默认的收集器是由一个线程来完成收集工作的,因此会给应用增加串行执行时间。吞吐收集器是多线程的进行次要收集的,所以可以降低应用的串行执行时间。一种典型的情况就是应用中有很多线程都在创建对象。这种情况下也需要增大新生代的大小。