转载自 http://longdick.javaeye.com/blog/474764
(本文基于JDK6)
说到GC,首先要对Java 的内存模型有所了解。
Java 的内存模型各个代的默认排列有如下图(适用JDK1.4.* 到 JDK6):
Java 的内存模型分为
Young(年轻代)
Tenured(终身代)
Perm(永久代)
更多关于内存模型的文章看这里:
在堆内存中的GC可以分为Minor GC(次要GC)和 Major GC(主要GC),次要GC是在年轻代进行收集的GC,职责是在Eden区满的时候收集dead的对象和转移存活的对象;主要GC是在终生代满时进行收集的GC,主要GC较次要GC需要更多时间。
使用jvm参数-verbose:gc 就可以输出每一次GC的详细信息。
你可能在程序运行起来以后看到如下输出:
[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]
你可以看到两次Minor GC(次要GC)和一次Major GC(主要GC)。
325407K->83000K 箭头前后的数字分别代表收集前和收集后的堆内存占用情况。
(776768K) 括号内的数字代表总共分配的堆内存空间,注意,这个值不包括其中一个Survior空间,也不包括 permanent generation(永久代)。
最后的时间0.2300771 secs指的是GC所耗费的时间。
如果运行时加上VM参数-XX:+PrintGCDetails 将输出更详细的信息。如下显示了Eden区和Heap内存在GC前后的变化:
[GC [DefNew: 64575K->959K(64576K), 0.0457646 secs] 196016K->133633K(261184K), 0.0459067 secs]
如果运行时加上VM参数-XX:+PrintGCTimeStamps 则可以得到GC发生的时间。
以下输出显示了在程序运行到111.042 秒的时候发生的GC,包括一次在Eden区的次要GC和发生在Tenured区的主要GC:
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性能主要的衡量指标有两个:Throughput和Pauses。吞吐量(Throughput)是不做GC的时间与总时间的百分比,分子包括分配内存空间的时间。中断(Pauses)是测量时间段内由于GC而导致的应用暂停次数。
对用户而言,对GC的需求往往是不一样的。一般的web应用对吞吐量要求不高,由于GC而引起的偶尔中断也是可以容忍的。然而一个交互性强的实时应用系统来说,经常性的中断将带来糟糕的用户体验。
即时性(Promptness)和足印(footprint)也是某些用户考虑的问题。 即时性是对象死去到所占内存释放的时间间隔,这个指数是分布式应用如使用RMI的分布式应用的一项需要考虑的因素。足印是一种过程的集合,代表可伸缩性。
HOTSPOT JVM总共拥有3种不同的GC,各有各总自的特点和应用场景:
- serial collector (串行GC)任何时刻都是使用一个线程执行GC操作,这种GC在线程间通信没有大的开销的应用会有相对不错的运行效率。最适合单处理器的系统;多处理器系统对这种GC而言并不能提升收集的效率。JVM默认情况下就是使用这个GC,这种GC有个形象的别名叫做"stop-the-world",当JVM在用这个GC收集垃圾的时候,你的app别想干其他事。你也可以用这个参数
-XX:+UseSerialGC 显式的声明使用。
默认的serial gc可以应付绝大多数的app。除非以下情况:这是一个运行在大内存多处理器的机器上的多线程的大应用。
- parallel collector (并行GC,或者叫 throughput collector ) 会以并行的方式运行minor collections(次级GC), 能较大的减少GC的开销。其诞生的初衷就是专门给运行在多处理器,多线程硬件上的中大型应用的。在特定的硬件和OS环境条件下这是默认选项,显式声明使用
-XX:+UseParallelGC
参数。始于JDK1.3.1。 parallel compaction 是在 J2SE 5.0 update 6引入的新特性,并在Java SE 6 得到增强。它允许使用并行的方式运行Major collections(主要GC)。如果不开启parallel compaction, major collections 将以单一线程的方式运行。 通过参数-XX:+UseParallelOldGC
显式使用该特性。
- concurrent collector (同步GC)同时运行大多数的任务 (GC的同时应用也在运行)来保证GC引起的中断时间尽量的短。主要应用在实时性要求重于总体吞吐量要求的中大型应用,即使如此,降低中断时间的技术还是会导致应用程序性能的少许降低。可以使用参数
-XX:+UseConcMarkSweepGC 使用该特性。
parallel collector的一些注意点:
parallel collector从JDK5开始就是Server端JVM的默认选择,需要加VM参数 -server 。
parallel collector还采用一些细节调优的策略如:
- 限定最大GC中断时间
- 吞吐量限定
- 足印(伸缩量)设定
可以用vm参数 -XX:MaxGCPauseMillis=<N>
来限定最大GC中断时间,单位是ms,规定GC产生的中断时间不能超过指定的时间。默认没有这个限制。一旦使用了这个参数,heap空间和其他相关参数会做出相应的调整来满足最大GC中断时间的要求。
吞吐量限定使用-XX:GCTimeRatio=<N>
来设定GC时间的比率,N 的值 = 没有花在GC上的时间/GC的时间 因此GC的时间占用总时间的百分比公式= 1 / (1 + <N>)
。比如 -XX:GCTimeRatio=19 意味着将有1/20的时间花在GC上。默认值=99。
足印(伸缩量)实际上就是heap堆内存的调整。最大Heap容量使用参数 -Xmx<N>
声明。
以上参数中任何一个的改动,都会引起另外两个的改变。三者的优先级如上顺序一至。
如果太多时间黑白花费在GC上,parallel collector将抛出OOM,这临界值大概是98%;也可以使用-XX:-UseGCOverheadLimit
关闭这个特性。
concurrent collector的一些注意点:
不适用于单处理器的系统,事实上在单处理器系统上运行concurrent collector 效率反而降低。如果只能运行在单处理器的系统上,那记得开启增量模式(incremental mode)。
前面几种GC都是在Tenured区满了以后触发主要GC操作;concurrent collector却是在Tenured区满溢之前就进行主要GC。如果concurrent collector没有赶在Tenured区满前收集完或者还没有开始收集的话,就会产生长时间的中断。参数-XX:CMSInitiatingOccupancyFraction=<N> 可以指定触发主要GC的临界值,N(0-100)代表的是Tenured区饱和程度百分比。一旦Tenured区饱和程度达到这个临界值,主要GC就发生了。
concurrent collection的生命周期一般包括如下几步:
- 停止应用的所有线程,标识出所有可到达的对象集合,然后恢复应用的所有线程
- 使用一个或几个处理器资源同步跟踪可到达的对象,应用线程同时运行
- 使用一个处理器资源同步地重新定位那些自从上一个步骤以来可能修改过的对象
- 停止应用的所有线程,重新定位那些自从上一个步骤以来可能修改过的对象,然后恢复应用的所有线程
- 使用一个处理器资源同步地收集不被引用的对象
- 使用一个处理器资源同步地重新定义堆内存,并且为下一次GC生命周期作好数据准备
concurrent collection在整个收集的过程中,至少会占有一到两个处理器,而且不会自动放弃占有的处理器资源。
这个特性会让只有一到两个处理器的系统很难过。为处理这个问题,需要借助增量模式(incremental mode)。
增量模式 的核心思想是 将整个GC生命周期分解成一段段的时间块分步进行,以此减少中断的时间,但是不可避免的是伴随着吞吐量的下降。
使用参数 -XX:+CMSIncrementalMode 开启增量模式。
参考资料:
http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html
http://java.sun.com/j2se/1.5.0/docs/guide/vm/gc-ergonomics.html