垃圾收集器之:throughput吞吐量收集器

在实践中咱们发现对于大多数的应用领域,评估一个垃圾收集(GC)算法如何根据以下两个标准:算法

  1. 吞吐量越高算法越好
  2. 暂停时间越短算法越好

首先让咱们来明确垃圾收集(GC)中的两个术语:吞吐量(throughput)和暂停时间(pause times)。 JVM在专门的线程(GC threads)中执行GC。 只要GC线程是活动的,它们将与应用程序线程(application threads)争用当前可用CPU的时钟周期。 简单点来讲,吞吐量是指应用程序线程用时占程序总用时的比例。 例如,吞吐量99/100意味着100秒的程序执行时间应用程序线程运行了99秒, 而在这一时间段内GC线程只运行了1秒。
缓存

术语”暂停时间”是指一个时间段内应用程序线程让与GC线程执行而彻底暂停。 例如,GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的。 若是说一个正在运行的应用程序有100毫秒的“平均暂停时间”,那么就是说该应用程序全部的暂停时间平均长度为100毫秒。 一样,100毫秒的“最大暂停时间”是指该应用程序全部的暂停时间最大不超过100毫秒。安全

吞吐量 VS 暂停时间

高吞吐量最好由于这会让应用程序的最终用户感受只有应用程序线程在作“生产性”工做。 直觉上,吞吐量越高程序运行越快。 低暂停时间最好由于从最终用户的角度来看无论是GC仍是其余缘由致使一个应用被挂起始终是很差的。 这取决于应用程序的类型,有时候甚至短暂的200毫秒暂停均可能打断终端用户体验。 所以,具备低的最大暂停时间是很是重要的,特别是对于一个交互式应用程序。服务器

不幸的是”高吞吐量”和”低暂停时间”是一对相互竞争的目标(矛盾)。这样想一想看,为了清晰起见简化一下:GC须要必定的前提条件以便安全地运行。 例如,必须保证应用程序线程在GC线程试图肯定哪些对象仍然被引用和哪些没有被引用的时候不修改对象的状态。 为此,应用程序在GC期间必须中止(或者仅在GC的特定阶段,这取决于所使用的算法)。 然而这会增长额外的线程调度开销:直接开销是上下文切换,间接开销是由于缓存的影响。 加上JVM内部安全措施的开销,这意味着GC及随之而来的不可忽略的开销,将增长GC线程执行实际工做的时间。 所以咱们能够经过尽量少运行GC来最大化吞吐量,例如,只有在不可避免的时候进行GC,来节省全部与它相关的开销。多线程

然而,仅仅偶尔运行GC意味着每当GC运行时将有许多工做要作,由于在此期间积累在堆中的对象数量很高。 单个GC须要花更多时间来完成, 从而致使更高的平均和最大暂停时间。 所以,考虑到低暂停时间,最好频繁地运行GC以便更快速地完成。 这反过来又增长了开销并致使吞吐量降低,咱们又回到了起点。
综上所述,在设计(或使用)GC算法时​​,咱们必须肯定咱们的目标:一个GC算法​​只可能针对两个目标之一(即只专一于最大吞吐量或最小暂停时间),或尝试找到一个两者的折衷。app

1、Throughput收集器

1.一、Throughput收集器两个基本操做介绍

Throughput收集器有两个基本操做:一是回收新生代垃圾(Minor GC);二是回收老年代垃圾(Full GC)。性能

下图是新生代回收以前和以后的状况spa

Minor GC一般在新生代空间用尽时。新生代收集器会把Eden空间中的全部对象挪走:一部分被移到Survivor中,其它被移到老年代。线程

 

下图是Full gc以前以后的对比图设计

老年代垃圾收集会收集新生代中全部的对象(包括Survivor中的对象),只有那些有活跃引用的对象,或者已经通过压缩整理的对象会继续在老年代中留存下来。其他对象将被回收。

 

1.二、堆大小的自适应调整和静态调整

Throughput收集器的调优几乎都是围绕停顿时间进行的,寻求堆的整体大小、新生代的大小以及老年代大小之间的平衡

两种取舍:

一、经典技术上的取舍,时间和空间上的取舍;

二、第二个取舍与完成垃圾回收所需的时长有关,增大堆可以减小Full GC停顿发送的频率但也有局限性:因为GC时间变得更长,平均响应时间也会变长(老年代空间很大,在回收老年代耗时很长)。相似的,为新生代分配更多的堆空间能够缩短Full GC的停顿时间,不过这又增大老年代垃圾回收的频率(由于老年代空间保持不变或者变得更小了)。

看一个示例说明堆大小与吞吐量的关系,在该示例中是关闭了JVM自适应调整堆大小的状况

在Throughput收集器中,可自适应调整会从新分配堆(以及代)的大小。使用这些标志能够设置相应的性能指标:-XX:MaxGCPauseMillis=N和-XX:GCTimeRatio=N。

-XX:MaxGCPauseMillis:标志用于设定应用可承受的最大停顿时间。能够设置为0或者一些很是小的值,譬如50毫秒。请注意,这个标志设定的值同时影响Minor GC和Full GC。若是设置的值很是小,那么应用的老年代最终就会很是小,致使频繁的Full GC(例如,但愿程序在50毫秒内完成垃圾回收,这将会触发很是频繁的Full GC,对应用程序的性能而言将是灾难性的。)。所以,设定该值时,尽可能保持理性,将该值设定为可达到的合理值。缺省状况下, 不设置该参数。

-XX:GCTimeRatio:标志能够设置你但愿应用程序在垃圾回收上花费多少时间(与应用线程的运行时间相比较)。它是一个百分比,默认值是99。

N值得计算稍微有些复杂。将N值代入下面的公式能够计算理想状况下应用线程的运行时间所占的百分比:

将GCTimeRatio的默认值99代入公式获得0.99,这意味着应用程序的运行时间占总时间的99%,只有1%的时间消耗在垃圾回收上。

再例如GCTimeRatio=95时,并非意味使用总时间5%去作垃圾回收;它表示的是最多会使用总时间的1.94%去作垃圾回收。因此,若是你指望程序工做时间在95%,则须要变换一下公式计算GCTimeRatio是一个更容易操做的方法。

对于95%(0.95)的吞吐量目标,利用该公式计算出的GCTimeRatio是19

JVM使用这两个标志在堆的初始值(-Xms,-XmX)之间的堆的大小。MaxGCPauseMillis的优先级别最高:若是设置了这个值,新生代和老年代会随之进行调整,直到知足对应停顿时间的目标。一旦这个目标达成,堆的总容量就开始逐渐增大,直到运行时间的比率达到设定值。这两个目标都达成后,开始缩减堆大小,尽量的以最小堆大小来知足这两个目标。

2、HotSpot虚拟机上的垃圾收集

该系列的第五部分咱们已经讨论过年轻代的垃圾收集器。 对于年老代,HotSpot虚拟机提供两类垃圾收集算法(除了新的G1垃圾收集算法),第一类算法试图最大限度地提升吞吐量,而第二类算法试图最小化暂停时间。 今天咱们的重点是第一类,”面向吞吐量”的垃圾收集算法。
咱们但愿把重点放在JVM配置参数上,因此我只会简要概述HotSpot提供的面向吞吐量(throughput-oriented)垃圾收集算法。 当年老代中因为缺少空间致使对象分配失败时会触发垃圾收集器(事实上,”分配”的一般是指从年轻代提高到年老代的对象)。 从所谓的”GC根”(GC roots)开始,搜索堆中的可达对象并将其标记为活着的,以后,垃圾收集器将活着的对象移到年老代的一块无碎片(non-fragmented)内存块中,并标记剩余的内存空间是空闲的。 也就是说,咱们不像复制策略那样移到一个不一样的堆区域,像年轻代垃圾收集算法所作的那样。 相反地,咱们把全部的对象放在一个堆区域中,从而对该堆区域进行碎片整理。 垃圾收集器使用一个或多个线程来执行垃圾收集。 当使用多个线程时,算法的不一样步骤被分解,使得每一个收集线程大多时候工做在本身的区域而不干扰其余线程。 在垃圾收集期间,全部的应用程序线程暂停,只有垃圾收集完成以后才会从新开始。 如今让咱们来看看跟面向吞吐量垃圾收集算法有关的重要JVM配置参数。

-XX:+UseSerialGC

咱们使用该标志来激活串行垃圾收集器,例如单线程面向吞吐量垃圾收集器。 不管年轻代仍是年老代都将只有一个线程执行垃圾收集。 该标志被推荐用于只有单个可用处理器核心的JVM。 在这种状况下,使用多个垃圾收集线程甚至会拔苗助长,由于这些线程将争用CPU资源,形成同步开销,却从未真正并行运行。

-XX:+UseParallelGC

有了这个标志,咱们告诉JVM使用多线程并行执行年轻代垃圾收集。 在我看来,Java 6中不该该使用该标志由于-XX:+UseParallelOldGC显然更合适。 须要注意的是Java 7中该状况改变了一点(详见本概述),就是-XX:+UseParallelGC能达到-XX:+UseParallelOldGC同样的效果。

-XX:+UseParallelOldGC

该标志的命名有点不巧,由于”老”听起来像”过期”。 然而,”老”其实是指年老代,这也解释了为何-XX:+UseParallelOldGC要优于-XX:+UseParallelGC:除了激活年轻代并行垃圾收集,也激活了年老代并行垃圾收集。 当指望高吞吐量,而且JVM有两个或更多可用处理器核心时,我建议使用该标志。
做为旁注,HotSpot的并行面向吞吐量垃圾收集算法一般称为”吞吐量收集器”,由于它们旨在经过并行执行来提升吞吐量。

-XX:ParallelGCThreads

经过-XX:ParallelGCThreads=<value>咱们能够指定并行垃圾收集的线程数量。 例如,-XX:ParallelGCThreads=6表示每次并行垃圾收集将有6个线程执行。 若是不明确设置该标志,虚拟机将使用基于可用(虚拟)处理器数量计算的默认值。 决定因素是由Java Runtime。availableProcessors()方法的返回值N,若是N<=8,并行垃圾收集器将使用N个垃圾收集线程,若是N>8个可用处理器,垃圾收集线程数量应为3+5N/8。
当JVM独占地使用系统和处理器时使用默认设置更有意义。 可是,若是有多个JVM(或其余耗CPU的系统)在同一台机器上运行,咱们应该使用-XX:ParallelGCThreads来减小垃圾收集线程数到一个适当的值。 例如,若是4个以服务器方式运行的JVM同时跑在在一个具备16核处理器的机器上,设置-XX:ParallelGCThreads=4是明智的,它能使不一样JVM的垃圾收集器不会相互干扰。

-XX:-UseAdaptiveSizePolicy

吞吐量垃圾收集器提供了一个有趣的(但常见,至少在现代JVM上)机制以提升垃圾收集配置的用户友好性。 这种机制被看作是HotSpot在Java 5中引入的”人体工程学”概念的一部分。 经过人体工程学,垃圾收集器能将堆大小动态变更像GC设置同样应用到不一样的堆区域,只要有证据代表这些变更将能提升GC性能。 “提升GC性能”的确切含义能够由用户经过-XX:GCTimeRatio和-XX:MaxGCPauseMillis(见下文)标记来指定。
重要的是要知道人体工程学是默认激活的。 这很好,由于自适应行为是JVM最大优点之一。 不过,有时咱们须要很是清楚对于特定应用什么样的设置是最合适的,在这些状况下,咱们可能不但愿JVM混乱咱们的设置。 每当咱们发现处于这种状况时,咱们能够考虑经过-XX:-UseAdaptiveSizePolicy停用一些人体工程学。

-XX:GCTimeRatio

经过-XX:GCTimeRatio=<value>咱们告诉JVM吞吐量要达到的目标值。 更准确地说,-XX:GCTimeRatio=N指定目标应用程序线程的执行时间(与总的程序执行时间)达到N/(N+1)的目标比值。 例如,经过-XX:GCTimeRatio=9咱们要求应用程序线程在整个执行时间中至少9/10是活动的(所以,GC线程占用其他1/10)。 基于运行时的测量,JVM将会尝试修改堆和GC设置以期达到目标吞吐量。 -XX:GCTimeRatio的默认值是99,也就是说,应用程序线程应该运行至少99%的总执行时间。

-XX:MaxGCPauseMillis

经过-XX:GCTimeRatio=<value>告诉JVM最大暂停时间的目标值(以毫秒为单位)。 在运行时,吞吐量收集器计算在暂停期间观察到的统计数据(加权平均和标准误差)。 若是统计代表正在经历的暂停其时间存在超过目标值的风险时,JVM会修改堆和GC设置以下降它们。 须要注意的是,年轻代和年老代垃圾收集的统计数据是分开计算的,还要注意,默认状况下,最大暂停时间没有被设置。若是最大暂停时间和最小吞吐量同时设置了目标值,实现最大暂停时间目标具备更高的优先级。 固然,没法保证JVM将必定能达到任一目标,即便它会努力去作。 最后,一切都取决于手头应用程序的行为。当设置最大暂停时间目标时,咱们应注意不要选择过小的值。 正如咱们如今所知道的,为了保持低暂停时间,JVM须要增长GC次数,那样可能会严重影响可达到的吞吐量。 这就是为何对于要求低暂停时间做为主要目标的应用程序(大多数是Web应用程序),我会建议不要使用吞吐量收集器,而是选择CMS收集器。 CMS收集器是本系列下一部分的主题。

相关文章
相关标签/搜索