吞吐量与用户线程暂停时间
衡量垃圾回收算法优劣的指标有两个:
- 吞吐量越高,则算法越好
- 暂停时间越短,则算法越好
首先说明吞吐量和暂停时间的含义。
垃圾回收时,JVM会启动几个特定的GC线程来完成垃圾回收的任务,这些GC线程与应用的用户线程产生竞争关系,共同竞争处理器资源以及CPU的执行时间。GC线程不会对用户带来的任何价值,因此,好的GC应该占用资源少,执行快,在不显著影响用户线程的情况下,迅速完成垃圾回收任务。
吞吐量是针对用户线程而言的,具体指的是在一个给定的时间段内,用户线程运行的时间所占的百分比(在这段时间内,GC运行也要占用CPU的运行时间)。比如,吞吐量99/100表示平均100秒的时间段内,有99秒在运行用户程序,而有1秒在运行GC线程,
暂停的含义是指,在一个给定的时间段内,应用线程由于GC而完全暂停的时间。比如,假如一次GC产生了100毫秒的用户线程暂停时间,这意味着在这100毫米里,用户线程完全处于不活跃状态。平均暂停时间是指在某个时间段内,用户线程在每次GC过程产生的暂停时间的平均值,最大暂停时间是在某个时间段内,用户线程在每次GC过程中产生暂停时间的最大值。
吞吐量 vs 用户线程暂停时间
高吞吐量和低暂停时间是应用所希望的,因为运行JVM不是为了运行垃圾回收线程的,而是能带来价值的应用系统。另外,平均暂停时间可能比较小,但是最大暂停时间却可能比较大,对于交互式应用程序,最大暂停时间应该比较小。
然而,高吞吐量和低暂停时间是竞争关系。比如,为了避免GC线程潜在的导致与用户线程发生线程同步和数据不一致问题,这要求GC线程在决定哪些对象可以回收,哪些对象仍然不能回收(仍然被活下来的对象引用)时的过程中,用户线程不能修改对象的状态,基于此,用户线程在GC过程中必须停下来(更确切的说,根据所用GC算法不同,用户线程可能在一次GC的某几个阶段停下来而不是完全暂停,比如CMS垃圾收集,一次GC分6个阶段,但是只有两个阶段需要暂停用户线程)。不仅如此,线程切换也会带来额外的开销:上下文切换带来的直接开销,缓存作用带来的间接开销(causes additional costs for thread scheduling: direct costs through context switches and indirect costs because of cache effects),在加上JVM内部的安全感考量,这意味着GC不仅仅带来GC线程本身的开销,还额外的增了一些开销,这些开销对于应用来说,都是"无价值"开销。因此,为了获得最大吞吐量,JVM必须尽可能少的运行GC,只有在迫不得已的情况下(比如新生代或者老年代已经满了)才运行GC。但是,这种方式也有问题,只在不得已的情况下才运行GC,那么每次运行GC时,需要做的事情会很多,比如有更多的对象积累在堆上等待回收,每次的GC时间会很高,由此引起的平均和最大暂停时间也会很高,这就要求GC不能在迫不得已的情况才运行,这回到了刚才的问题。
当设计GC算法或者选用给予某种GC算法的垃圾收集器时,我们要根据应用的特点选择合适的收集器,每个垃圾收集器要么只是把吞吐量和暂停时间两个目标之一作为它的设计目标,要么在两个之间做一个折中而兼顾两个目标。
Sun/Oracle HotSpot JVM的垃圾回收
对于老年代的垃圾回收,HotSpot JVM有两类垃圾回收算法。第一类算法目标是最大化吞吐量(面向吞吐量的垃圾回收),第二类算法目标在于缩短等待时间。第一类垃圾回收在HotSpot JVM中称为Throughput Collector。
吞吐量收集器的工作方式
吞吐量垃圾回收实在老年代没有足够的空间来分配对象时进行触发(分配给老年代的对象大多是从新生代升级到老年代的对象)。
1.从 "GC roots"开始,GC在老年代堆空间上查找可达对象,并将它们标记为存活状态。
2. GC把老年代上标记为存活状态的对象移动到一端,因此,这些存活的对象会占用单块连续的内存空间。这里采用的是对象移动(Move)的方式,而不是复制(copy)到另外的堆空间,因此,可以解决碎片问题。
3.将剩下的堆空间标记位可回收状态
4.吞吐量收集器使用一个或者几个GC线程来执行线程操作。当使用多个线程来进行垃圾回收时,算法分解为多个不同步骤,每个步骤由不同的GC线程进行处理,因此这些GC线程不会相互干扰,只负责自己的那块内存空间。
5.在GC过程中,所有的用户线程都会暂停。当GC结束,JVM会恢复用户线程的执行
与吞吐量相关的JVM选项
-XX:+UseSerialGC
使用这个选项指示JVM启动单线程(串行)的面向吞吐量的垃圾收集器。新生代和老年代都是使用单线程进行垃圾回收。这个选项通常只用于单核CPU的情况,如果只有单核而使用多线程并发的进行垃圾回收,由于频繁的线程切换,反而降低了效率
-XX:+UseParallelGC
使用这个选项指示JVM启动多个GC线程,并行的进行新生代的垃圾回收。对于Java6,使用-XX:+UseParallelOldGC更可取。而到了Java7,使用
-XX:+UseParallelGC和使用
-XX:+UseParallelOldGC
一样的效果
-XX:+UseParallelOldGC
这里的Old不是老的GC算法,而是指老年代,但是这个选项的名字及其容易产生误解,以为只是对老年代进行并行回收。实际上,-XX:+UseParallelOldGC不但对老年代进行并行回收,还会对新生代进行并行的垃圾回收。如果是多核CPU并且高吞吐量是一个希望的目标,那么应该加上这个选项。吞吐量浏览器之所以称为吞吐量浏览器
原因就在于它把高吞吐作为这个算法的卖点。
XX:ParallelGCThreads
使用-XX:ParallelGCThreads=<value>选项可以用来设定进行并行GC的线程数。例如,
-XX:ParallelGCThreads=6
表示使用6个线程来并行的执行GC任务。
JVM设置的默认值gcThreadNum计算方法是基于CPU的内核数,
N=Runtime.availableProcessors();
if (N <= 8) {
gcThreadNum = N;
} else {
gcThreadNum = 3 + 5*N/8;
}
通常,可以使用JVM提供的默认值,但是如果一台服务器上有多个JVM进程,那么应该将值设置的小一些以避免多个JVM进程的GC线程过多导致的并发竞争。此时的N就是CPU内核数除以JVM进程数
-XX:UseAdaptiveSizePolicy