返回首页 JVM 实用参数系列

吞吐量收集器

原文链接 本文连接 译者:张军 校对:梁海舰

在实践中我们发现对于大多数的应用领域,评估一个垃圾收集 (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 算法​​只可能针对两个目标之一(即只专注于最大吞吐量或最小暂停时间),或尝试找到一个二者的折衷。

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= 我们可以指定并行垃圾收集的线程数量。 例如,-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= 我们告诉 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 收集器是本系列下一部分的主题。

上一篇: 新生代垃圾回收 下一篇: CMS 收集器