JVM的GC调优

谁需要GC调优

小规模程序,垃圾收集算法能很好的工作。因为里面的对象图不大,所以收集代价不高。但是如果是大规模的程序,对象成百上千,一次遍历,就算是复制收集器中只对活动对象的遍历,都需要很长的时间。所以,大规模程序,有必要深入了解GC的工作方式,了解和调整GC的参数。

关于串行和并行GC收集算法

在JVM 1.3.1以前,只有串行收集器,没有并行收集器,对于多处理器,系统吞吐量损失很大。见下图。为此,1.3.1以后(不包含1.3.1),引入了并发GC收集算法。
JVM1.4.2上,有4种垃圾收集器,默认会选择串行收集器。
JVM5.0后,则会根据用户机器的类型自动选择收集器。

 

1% GC 中的 1%是指,在一个CPU上执行且只花1%的CPU时间做垃圾收集的应用程序。
可以看到,当这个程序在处理器为30个以上的系统上运行,系统的吞吐量会下降到80%以下,即本来在单CPU系统上只花1%时间做搜集的程序会在30个以上CPU的系统上,花上20%的时间做收集。

为什么会这样呢?因为单CPU收集算法在多CPU系统上面做收集的时候,GC算法会暂停运行在任何CPU上面的程序,而自己却只能利用一个CPU做GC,所以造成了这种情况。

代Generation

自从JVM1.2以后,Sun采用了一种分代收集的策略,即将堆分成3个不同的区域,按照对象存活的时间的不同,将对象保存在不同的堆上。在不同的代上面,应用不同的收集算法,来达到最优化收集。

这3个区域叫年轻代,年老代和持久代。
除Class和Method等元数据在持久代上面分配以外,所有的对象都在年轻代上面分配,当达到一定的条件,如经过在年轻代上面N次收集后的对象,就会保存到年老代之中。

在年轻代上面进行的收集叫Minor collections,在年轻代和年老代上面同时收集叫Major collections。

Tips:

尽量不要调用System.gc(),因为这会触发Major collections。Major collections的收集效率不高,因为它要遍历几乎所有的对象。
没有办法利用API直接触发Minor collections,但是仍然有其他的调优手段。当使用完集合对象后,把引用设置为null,这样避免gc在收集过程中,无谓地遍历那些即将就要释放的对象。

实验表明,年轻代上面的对象98%的对象都会在短时间内死亡,故Minor collections可以利用拷贝收集器,只遍历那些2%存活的对象,而不用管那些死亡的对象,来提高收集效率。

为了让Minor collections能充分利用年轻代上面对象大量死亡的这个特点,就需要调整以下几个参数:
1. 收集的频率
过于频繁的收集,会导致代中对象死亡率不够高,从而需要遍历这个代中大部分的对象,使得高死亡率这个条件利用的不充分。

2. 年轻代(堆)的大小
如果堆太小,一旦堆被占满,Minor collections就不得不频繁的启动,导致情况1的发生,从而降低收集效率。

下图反应了绝大多数对象在早起死亡的这一个事实:

从图中可以看到,随着时间的推进,大部分分配的字节都被回收了,少部分留了下来。被回收的这些字节,就是所谓的die young,留下的则是live longer。

Bytes allocated 已经分配的总字节数
Bytes Surviving 存活的字节数
Minor Collections 对年轻代进行收集
Major Collections 对所有代进行收集

下图描述了JVM中对堆的划分:

Young 年轻代
Tenured 年老代
Perm 持久代

Virutal是指保留,而未分配的内存。如Perm中,加上Virtual则是Perm区域最大的大小,而刚开始并不会完全分配这个堆,只会按照最小的大小分配。

Young部分,被分为了三个部分,一个Eden,和两个大小相同的Survivor。
所有的新建对象都会在Eden中分配,当Eden占满后,即剩余的大小不足以分配新的对象时,就会触发Minor collections。对Young收集时,会将对象拷贝到其中一个Survivor,另外一个Survivor保留不用。当下次收集时,则将上次Survivor中和Eden中的活动对象,拷贝到未用的Survivor中。如此反复。当某些对象经历足够长的次数或者时间后,就会被拷贝入年老代。

如果对Young的minor collections收集到的活动对象,survivor无法完全容纳,则会将某些对象拷贝到年老代,如果年老代也不能容纳新拷贝入的对象,则触发Major collections。如果Major collections后,如果还不足以容纳,就会将Virutal中预留的空间用来扩展已有的堆。当保留Virutal分配完毕后,仍然不足时,就会抛出OutOfMemory的错误。

Tips:

不要把年老代设置的过小,一般最好能比eden+survivor更大一些,这样可以避免触发Major collections。在这个前提下,年轻代越大越好。由于Young的eden区域是拷贝收集,容易产生碎片,所以此区域越大,越不容易导致因为碎片导致的内存不足而引发的minor collections。至于survivor,则要根据情况调整。过大的survior会造成浪费,过小的survior会导致,对象被直接拷贝到年老代。

JVM1.2中,未使用上面一大二小的结构,而是将Young分成两个相同大小的区域,来回进行拷贝。

调整GC的手段

 

如何看懂上面那张图:

行,分别对应了三个代
列,分别对应了实时性要求比较高的默认的和可调节的选项, 以及对吞吐量比较高的默认的和可调节的选项,还有就是调整这3个堆的选项。

-Xmx 调整JVM启动时,保留Total Size的大小
-Xms 调整JVM启动初始化时,Total Size的大小

每一次收集后,都会根据以下两个参数调整Total Size的大小

-XX:MinHeapFreeRatio(默认,40)     Total Size中可用空间小于这个比率,就会扩展堆的大小,保持这个值
-XX:MaxHeapFreeRatio(默认,70)     Total Size中可用空间大于这个比例,就缩小堆的大小,保持这个值

Tips:

如果你的程序是服务器,那么通过将-Xmx和-Xms设置成相近,或者相同,可以阻止这种堆大小的频繁调整,造成的不必要的收集和堆增长过程。

-XX:NewRatio=3     年老代和年轻代的内存分配比率,3表示,在Total Size中,年轻代占1份,年老代3份
-XX:NewSize           年轻代初始化时的大小
-XX:MaxNewSize     如果不指定,那么年轻代可以增长不受限制,但受NewRatio的限制
-XX:SurvivorRato=6 年轻代中,eden与survivor的比率,这里eden占6份,2个survivor占2份
-XX:MaxTenuringThreshold=0 阀值,超过这个阀值的对象将被拷贝到年老代

 

 

如何进行GC调优?

 

 

 

 

1.性能的几个重要度量参数

Troughput     吞吐量,除掉GC所用的时间后,真正执行程序所在总时间的百分比
Pauses          暂停时间,即由于正在做GC,而没有响应的那些时间
Footprint        内存需求,通常用page和cache line的数量来衡量

2.查看当前的GC收集

java命令运行时,输入 -verbose:gc 参数

[GC 4802K->4383K(5312K), 0.0078566 secs]         //执行了Minor collections
[Full GC 4383K->4383K(5312K), 0.0385521 secs]   //执行了Major collections
[GC 5248K->4814K(8216K), 0.0121798 secs]         //空间仍然不够,扩展了年轻代和年老代的空间

以第一行为例

GC                       表示执行了Minor collections
4802K->4383K    表示GC执行前和执行后,堆中活动对象的大小
(5312K)               表示总的堆的大小(不算持久代,而且只算2个Survivor中的1个,即用户可用堆的大小)
0.0078566 secs   表示GC所用的时间。主要,也是首先看这个,然后再看GC/Full GC是否过于频繁。

这个信息反应了什么?年轻代太小,因为执行了Minor collections之后,活动的对象并没有显著减少,4802K->4383K, 说明初始堆分配的不够大。这个GC显然是因为eden内部碎片导致的。

用-XX:+PrintGCDetails 参数,打印更详细的信息

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

// Minor collections从年轻代64575K中收集到了959K的活动对象,花费了4微秒多一点
DefNew: 64575K->959K(64576K), 0.0457646 secs

//整个堆整理后,从196016K中,收集到了133633K的活动对象
196016K->133633K(261184K)

还可以用-XX:+PrintGCTimeStamps 查看带起始和终止时间戳的信息

还有-XX:-PrintTenuringDistribution   打印出对象在放入年老代之前在年轻代做了多少次复制,如果复制次数过少,说明年轻代过小。

2类收集器

The Throughput Collector       以提高吞吐量,降低GC时间比的收集策略
The Concurrent Low Pause Collector      以提高暂停时间,以实时性为目的的收集策略

具体请看资料[1]中的内容。

其他

关于,如何遍历年轻代中的活动对象的技术,请看资料[3]。

参考资料:

1.Tuning Garbage Collection with the 5.0 Java[tm] Virtual Machine
http://java.sun.com/docs/hotspot/gc5.0/gc_tuning_5.html

2.Garbage Collector Ergonomics
http://java.sun.com/j2se/1.5.0/docs/guide/vm/gc-ergonomics.html

3.JVM1.4.1中的垃圾收集
http://www.ibm.com/developerworks/cn/java/j-jtp11253/

猜你喜欢

转载自hai0378.iteye.com/blog/2041056