集册 JVM 原理详解 吞吐量收集器

吞吐量收集器

欢马劈雪     最近更新时间:2020-11-09 06:03:05

408

在实践中我们发现对于大多数的应用领域,评估一个垃圾收集 (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:-UseAdaptiveSizePolicy

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

展开阅读全文