1)JVM对堆空间的管理
JVM 在初始化的过程中分配堆。堆的大小取决于指定或者默认的最小和最大值以及堆的使用情况。如果用Heapbase表示堆底,heaptop表示堆能够增长到的最大绝对值,用heaplimit表示实际的堆顶;则两者的差值(heaptop - heapbase)由命令行参数 -Xmx 决定。heaplimit指针可以随着堆的扩展而上升,随着堆的收缩而下降。heaplimit永远不能超过heaptop,也不能低于使用 -Xms 指定的初始堆大小。任何时候堆的大小都是 heaplimit - heapbase。如果整个堆的*空间比例低于 -Xminf 指定的值(minf 是最小*空间),堆就会扩展。如果整个堆的*空间比例高于 -Xmaxf 指定的值(maxf 是最大*空间),堆就会收缩。-Xminf 和 -Xmaxf 的默认值分别是 0.3 和 0.6,因此 JVM 总是尝试将堆的*空间比例维持在 30% 到 60% 之间。参数 -Xmine(mine 是最小扩展大小)和 -Xmaxe(maxe 是最大扩展大小)控制扩展的增量。这 4 个参数对固定大小的堆不起作用(用相等的 -Xms 和 -Xmx 值启动 JVM,这意味着 HeapLimit = HeapTop),因为固定大小的堆不能扩展或收缩。
2)基本收集算法
- 复制:将堆内分成两个相同空间,从根(ThreadLocal的对象,静态对象)开始访问每一个关联的活跃对象,将空间A的活跃对象全部复制到空间B,然后一次性回收整个空间A。因为只访问活跃对象,将所有活动对象复制走之后就清空整个空间,不用去访问死对象,所以遍历空间的成本较小,但需要巨大的复制成本和较多的内存。
- 标记清除(mark-sweep):收集器先从根开始访问所有活跃对象,标记为活跃对象。然后再遍历一次整个内存区域,把所有没有标记活跃的对象进行回收处理。该算法遍历整个空间的成本较大暂停时间随空间大小线性增大,而且整理后堆里的碎片很多。
- 标记整理(mark-sweep-compact):综合了上述两者的做法和优点,先标记活跃对象,然后将其合并成较大的内存块。
3)分代
分代是Java垃圾收集的一大亮点,根据对象的生命周期长短,把堆分为年轻代和年老代,根据不同代的特点采用不同的收集算法。
年轻代(New Area)
实际上大部分对象都是朝生暮死,随生随灭的,因此所有收集器都为年轻代选择了复制算法。复制算法优点是只访问活跃对象,缺点是复制成本高。因为年轻代只有少量的对象能熬到垃圾收集,因此只需少量的复制成本。而且复制收集器只访问活跃对象,对那些占了最大比率的死对象视而不见,充分发挥了它遍历空间成本低的优点。年轻代随堆内存增大而增大,JVM会根据情况动态管理其大小变化。-Xmns<value>, -Xmnx<value>, -Xmos<value>, -Xmox<value> 等JVM选项可以设置年轻代与年老代的初始尺寸和最大尺寸。
年轻代里面又分为2个区域,一个是Allocate区,所有新建对象都会存在于该区,另一个是Survivor区;并实施复制算法。每次复制就是将Allocate中的活对象复制到Survivor或者年老代中(如果符合一定的年老化条件),然后将Allocate区与Survivor区的角色互换。
年老代(Tenured Area)
年轻代的对象如果能够经历过数次收集,就会进入年老区。年老区使用标记整理算法。因为年老区的对象通常有较长的生命周期,采用复制算法就要反复地复制对象,很不合算,所以采用标记清理算法。
年老期限(Tenure age)
年老期限是用于衡量一个年轻代的对象在什么情况下被升级为年老代的对象。这个参数会被JVM动态调整,并达到一个最大值14。每次垃圾收集之后存活下来的对象的年老期限会递增一。一个年老期限为x的对象意味着,当该对象经历了Allocate区和Survivor区的x次反转后仍然存活,则该对象会被升级为年老代对象。该阈值的调整是基于年轻代空间所占堆空间的比例。
倾斜比率(Tilt ratio)
Allocate区在年轻代区域中占用的空间是使用一种称为Tilting的技术进行最大化的。Tilting控制Allocate区和Survivor区的相对大小。基于每次反转之后存活下来的对象所占空间的总数,该倾斜比率(Tilt ratio)会被调整以使Survivor区变得更小。比如,如果初始年轻代的大小为500MB,那么Allocate区和Survivor区将各占一半,即250MB。随着应用程序的运行,一次垃圾收集事件被触发,而且只有50MB的对象存活下来。在这种情况下,Survivor区的空间将被减少,从而为Allocate区提供更多的空间。较大的Allocate区意味着将经历更长的时间才会发生下一次垃圾收集。如下图所示,Survivor区的空间会被逐步调整到最合适的比例。
垃圾收集前后的Allocate区和Survivor区的分布举例
4)verbosegc日志输出
verbosegc日志由 JVM 在指定 -verbosegc 命令行参数时生成,是一种非常可靠的独立于平台的调试工具。启用 verbosegc 可能对应用程序的性能有一定影响。如果这种影响是无法接受的,则应该使用测试系统来收集 verbosegc 日志。这是监控整个 JVM 是否运转良好的一种好办法,在出现 OutOfMemory 错误的情况下,这种方法尤其重要。
5)正确设置堆的大小
计算正确的堆大小参数很容易,但它可能对应用程序启动时间和运行时性能有很大的影响。初始大小和最大值分别由参数 -Xms 和 -Xmx 控制,这些值通常是根据理想情况和重负荷情况下堆的使用情况的估计来设置的,但 verbosegc 可以帮助确定这些值,而避免胡乱猜测。下面是从启动到完成程序的初始化(或者进入“就绪”状态)这段时间里,一个应用程序的 verbosegc 输出,如下所示。
计算正确的堆大小参数很容易,但它可能对应用程序启动时间和运行时性能有很大的影响。初始大小和最大值分别由参数 -Xms 和 -Xmx 控制,这些值通常是根据理想情况和重负荷情况下堆的使用情况的估计来设置的,但 verbosegc 可以帮助确定这些值,而避免胡乱猜测。下面是从启动到完成程序的初始化(或者进入“就绪”状态)这段时间里,一个应用程序的 verbosegc 输出,如下所示。
<GC[0]: Expanded System Heap by 65536 bytes
<GC[0]: Expanded System Heap by 65536 bytes
<AF[1]: Allocation Failure. need 64 bytes, 0 ms since last AF>
<AF[1]: managing allocation failure, action=1 (0/3983128) (209640/209640)>
<GC(1): GC cycle started Tue Oct 29 11:05:04 2002
<GC(1): freed 1244912 bytes, 34% free (1454552/4192768), in 10 ms>
<GC(1): mark: 9 ms, sweep: 1 ms, compact: 0 ms>
<GC(1): refs: soft 0 (age >= 32), weak 5, final 237, phantom 0>
<AF[1]: completed in 12 ms>
上述记录表明,第一次发生 AF 时,堆中的*空间为 0%(3983128 中有 0 字节可用)。此外,第一次垃圾收集之后,*空间比例上升到 34%,略高于 -Xminf 标记(默认为 30%)。根据应用程序的使用,使用 -Xms 分配更大的初始堆可能会更好一些。几乎可以肯定的是,上例中的应用程序在下一次 AF 时会导致堆扩展。分配更大的初始堆可以避免这种情况。一旦应用程序进入 Ready 状态,通常不会再遇到 AF,因此也就确定了比较好的初始堆大小。类似地,通过增加应用程序负载也可以探测到避免出现 OutOfMemory 错误的 -Xmx 值。
如果堆太小,即使应用程序不会长期使用很多对象,也会频繁地进行垃圾收集。因此,自然会出现使用很大的堆的倾向。但是由于平台和其他方面的因素,堆的最大大小还受物理因素的限制。如果堆被分页,性能就会急剧恶化,因此堆的大小一定不能超出安装在系统上的物理内存总量。比如,如果 AIX 机器上有 1 GB 的内存,就不应该为 Java 应用程序分配 2 GB 的堆。
垃圾收集周期所花费的时间直接与堆的大小成正比。一条好的原则是根据需要设置堆的大小,而不是将它配置得太大或太小。
常见的一种性能优化技术是将初始堆大小(-Xms)设成与最大堆大小(-Xmx)相同。因为不会出现堆扩展和堆收缩,所以在某些情况下,这样做可以显著地改善性能。通常,只有需要处理大量分配请求的应用程序时,才在初始和最大堆大小之间设置较大的差值。但是要记住,如果指定 -Xms100m -Xmx100m,那么 JVM 将在整个生命期中消耗 100 MB 的内存,即使利用率不超过 10%。
6)避免堆失效
如果使用大小可变的堆(比如,-Xms 和 -Xmx 不同),应用程序可能遇到这样的情况,不断出现分配失败而堆没有扩展。这就是堆失效,是由于堆的大小刚刚能够避免扩展但又不足以解决以后的分配失败而造成的。通常,垃圾收集周期释放的空间不仅可以满足当前的分配失败,而且还有很多可供以后的分配请求使用的空间。但是,如果堆处于失效状态,那么每个垃圾收集周期释放的空间刚刚能够满足当前的分配失败。结果,下一次分配请求时,又会进入垃圾收集周期,依此类推。大量生存时间很短的对象也可能造成这种现象。避免这种循环的一种办法是增加 -Xminf 和 -Xmaxf 的值。比方说,如果使用 -Xminf.5,堆将增长到至少有 50% 的*空间。同样,增加 -Xmaxf 也是很合理。如果 -Xminf等于 5,-Xmaxf 为默认值 0.6,因为 JVM 要把*空间比例保持在 50% 和 60% 之间,所以就会出现太多的扩展和收缩。两者相差 0.3 是一个不错的选择,这样 -Xmaxf.8 可以很好地匹配 -Xminf.5。
如果记录表明,需要多次扩展才能达到稳定的堆大小,但可以更改 -Xmine,根据应用程序的行为来设置扩展大小的最小值。目标是获得足够的可用空间,不仅能满足当前的请求,而且能满足以后的很多请求,从而避免过多的垃圾收集周期。-Xmine、-Xmaxf 和 -Xminf 为控制应用程序的内存使用特性提供了很大的灵活性。
7)应该避免的开关
下列命令行开关应避免使用:
- -Xnocompactgc 该参数完全关闭压缩。虽然在性能方面有短期的好处,最终应用程序堆将变得支离破碎,即使堆中有足够的*空间也会导致 OutOfMemory 错误
- -Xcompactgc 使用该参数将导致每个垃圾收集周期都执行压缩,无论是否有必要。JVM 在压缩时要做大量的决策,在普通模式下会推迟压缩
- -Xgcthreads 该参数控制 JVM 在启动过程中创建的垃圾收集帮助器线程个数。对于 N-处理器机器,默认的线程数为 N-1。这些线程提供并行标记和并行清理模式中的并行机制
参考文献:
- <<IBM JDK 5.0 Diagnostics Guide>>
- http://www-128.ibm.com/developerworks/cn/java/