Java性能权威指南-总结8

Java性能权威指南-总结8

  • 垃圾收集算法
    • 理解CMS收集器
      • 针对并发模式失效的调优

垃圾收集算法

理解CMS收集器

针对并发模式失效的调优

调优CMS收集器时最要紧的工作就是要避免发生并发模式失效以及晋升失败。 正如在CMS垃圾收集日志中看到的那样,发生并发模式失效往往是由于CMS不能以足够快的速度清理老年代空间:新生代需要进行垃圾回收时,CMS收集器计算发现老年代没有足够的空闲空间可以容纳这些晋升对象,不得不先对老年代进行垃圾回收。

初始时老年代空间中对象是一个接一个整齐有序排列的。当老年代空间的占用达到某个程度(默认值为70%)时,并发回收就开始了。一个CMS后台线程开始扫描老年代空间,寻找无用的垃圾对象时,竞争就开始了:CMS收集器必须在老年代剩余的空间(30%)用尽之前,完成老年代空间的扫描及回收工作。 如果并发回收在这场速度的比赛中失利,CMS收集器就会发生并发模式失效。

有以下途径可以避免发生这种失效:

  • 想办法增大老年代空间,要么只移动部分的新生代对象到老年代,要么增加更多的堆空间。
  • 以更高的频率运行后台回收线程。
  • 使用更多的后台回收线程。
自适应调优和CMS垃圾搜集
CMS收集器使用两个配置MaxGCPauseMllis=N和GCTimeRatio=N来确定使用多大的堆和多大的代空间。

CMS收集与其他的垃圾收集方法一个显著的不同是除非发生Full GC,否则CMS的新生代大小不会作调整。由于CMS的目标是尽量避免Full GC,
这意味着使用精细调优的CMS收集器的应用程序永远不会调整它的新生代大小。

程序启动时可能频发并发模式失效,因为CMS收集器需要调整堆和永久代(或者元空间)的大小。使用CMS收集器,初始时采用一个比较大
的堆(以及更大的永久代/元空间)是一个很好的主意,这是一个特例,增大堆的大小反而帮助避免了那些失效。

如果有更多的内存可用,更好的方案是增加堆的大小,否则可以尝试调整后台线程运行的方式来解决这个问题。

  1. 给后台线程更多的运行机会

为了让CMS收集器赢得这场比赛,方法之一是更早地启动并发收集周期。显然地,CMS收集器在老年代空间占用达到60%时启动并发周期,这和老年代空间占用到70%时才启动相比,前者完成垃圾收集的几率更大。为了实现这种配置,最简单的方法是同时设置下面这两个标志:-XX:CMSInitiatingOccupancyFraction=N-XX:+UseCMSInitiatingoccupancyonly。同时使用这两个参数能帮助CMS更容易地进行决策:如果同时设置这两个标志,那么CMS就只依据设置的老年代空间占用率来决定何时启动后台线程。 默认情况下,UseCMSInitiatingoccupancyOnly标志的值为假,CMS会使用更复杂的算法判断什么时候启动并行收集线程。如果有必要提前启动后台线程,推荐使用最简单的方法,即将UseCMSInitiatingOccupancyonly标志的值设置为真。

CMSInitiatingOccupancyFraction参数值的调整可能需要多次迭代才能确定。如果开启了UseCMSInitiatingoccupancyonly标志,CMSInitiatingoccupancyFraction的默认值就被置为70,即CMS会在老年代空间占用达到70%时启动并发收集周期。

对特定的应用程序,该标志的更优值可以根据GC日志中CMS周期首次启动失败时的值得到。具体方法是,在垃圾回收日志中寻找并发模式失效,找到后再反向查找CMS周期最近的启动记录。日志中含有CMS-initial-mark信息的一行包含了CMS周期启动时,老年代空间的占用情况如下所示:

89.976:[GC [1 CMS-initial-mark: 702254K(1398144K)]
				772530K(2027264K),0.0830120 secs]
				[Times:user=0.08 sys=0.00,real=0.08 secs]

在这个例子中,根据日志的输出,我们可以判断该时刻老年代空间的占用率为50%(老年代空间大小为1398 MB,其中702MB被占用)。不过这个值还不够早,因此我们需要调整CMSInitiatingOccupancyFraction将其值设定为小于50的某个值。(虽然CMSInitiatingOccupancyFraction的默认值为70,不过这个例子中没有开启UseCMSInitiatingoccupancyonly标志,所以例子中CMS收集器在老年代空间占用达到50%时启动了CMS后台线程。)

了解了CMSInitiatingOccupancyFraction的工作原理之后,可能会有疑问,能不能将参数值设置为0或者其他比较小的值,让CMS的后台线程持续运行。通常不推荐进行这样的设置,但是,如果对其中的取舍非常了解,适当地妥协也是可以接受的。

这其中的第一个取舍源于CPU:CMS后台线程会持续运行,它们会消耗大量的CPU时钟——每个CMS后台线程运行时都会100%地占用一颗CPU。多个CMS线程同时运行时还会有短暂的爆发,机器的总CPU使用因此也会暴涨。如果这些线程都是毫无目的地持续运行,只会白白浪费宝贵的CPU资源。

另一方面,这并不是说使用了过多的CPU周期就是问题。后台的CMS线程需要时必须运行,即使在最好的情况下,这也是很难避免的。因此,机器必须预留足够的CPU周期来运行这些CMS线程。所以规划机器时,你必须考虑留出余量给这部分CPU的使用。

CMS周期中,如果CMS后台线程没有运行,这些CPU时钟可以用于运行其他的应用吗?通常不会。如果还有另一个应用也在使用同一个时钟周期,它没有途径了解何时CMS线程会运行。因此,应用程序线程和CMS线程会竞争CPU资源,而这很可能会导致CMS线程的“失速”(lose its race)。有些时候,通过复杂的操作系统调优,有可能让应用线程以低于CMS线程优先级的方式让两种线程在同一个时钟周期内运行,但是这些方法都相当复杂,很容易出错。因此,答案是肯定的,CMS周期运行得越频繁,CPU周期越长,如果不这样,这些CPU周期就是空闲状态(idle)。

第二个取舍更加重要,它与应用程序的停顿相关。正如在GC日志中观察到的,CMS在特定的阶段会暂停所有的应用线程。 使用CMS收集器的主要目的就是要限制GC停顿的影响,因此频繁地运行更多无效的CMS周期只能适得其反。CMS停顿的时间与新生代的停顿时间比起来要短得多,应用线程甚至可能感受不到这些额外的停顿——这也是一种取舍,是要避免额外的停顿还是要减少发生并发模式失败的几率。不过,正如前面提到的,持续地运行后台GC线程所造成的停顿可能会导致总体的停顿,而这最终会降低应用程序的性能。

除非这些取舍都能接受,否则不要将CMSInitiatingoccupancyFraction参数的值设置得比堆内的活跃数据数还多,至少要少10%到20%。

  1. 调整CMS后台线程

每个CMS后台线程都会100%地占用机器上的一颗CPU。如果应用程序发生并发模式失效,同时又有额外的CPU周期可用,可以设置-XX:ConcGCThreads=N标志,增加后台线程的数目。默认情况下,ConcGCThreads的值是依据ParallelGCThreads标志的值计算得到的:

	ConcGCThreads = (3 + ParallelGCThreads) / 4

上述计算使用整数计算方法,这意味着如果ParallelGCThreads的取值区间在1到4,ConcGCThread的值就为1,如果ParallelGCThreads的取值在5到8之间,ConcGCThreads的值就为2,以此类推。

调整这一标志的要点在于判断是否有可用的CPU周期。如果ConcGCThreads标志值设置的偏大,垃圾收集会占用本来能用于运行应用线程的CPU周期;最终效果上,这种配置会导致应用程序些微的停顿,因为应用程序线程需要等待再次在CPU上继续运行的机会。

除此之外,在一个配备了大量CPU的系统上,ConcGCThreads参数的默认值可能偏大。如果没有频繁遭遇并发模式失败,可以考虑减少后台线程数,释放这部分CPU周期用于应用线程的运行。

快速小结

  1. 避免发生并发模式失效是提升CMS收集器处理能力、获得高性能的关键。
  2. 避免并发模式失效(如果有可能的话)最简单的方法是增大堆的容量。
  3. 否则,我们能进行的下一个步骤就是通过调整CMSInitiatingOccupancy-Fraction参数,尽早启动并发后台线程的运行。
  4. 另外,调整后台线程的数目对解决这个问题也有帮助。

你可能感兴趣的:(java,jvm,算法)