Java 技术,IBM 风格: 垃圾收集策略,第 1 部分

不同的策略提供了灵活性

Java™ 平台最大的好处之一是,它会替您负责处理大量垃圾收集工作,但是有时候您仍然希望调整进行垃圾收集的方式。利用 IBM 提供的最新 Java 技术实现,可以在几种垃圾收集策略中做出选择,从而让自己的应用程序发挥出最好的性能。本文是 Java 技术,IBM 风格 系列的第二篇文章。在本文中,Java 开发人员 Mattias Persson 探讨了可用的选项并详细描述了每个选项适合的场景。

Mattias Persson, 资深软件工程师, EMC

Mattias PerssonMattias Persson 在英国 IBM Software Group Development 工作,专长是 Java 平台性能和可伸缩性。他已经为 IBM 工作了 4 年,拥有瑞士 Vaxjo 大学计算机科学专业的理学硕士学位。他是 J2EE-Certified Architect 和 Principal Certified Lotus Professional。在业余时间,他常常在 IBM Hursley 中心北边的小山上骑山地车。



2006 年 6 月 01 日

可以使用 4 种不同的策略配置 IBM Developer Kit for the Java 5.0 Platform(IBM SDK)中的垃圾收集(GC)。本文(关于 GC 的两篇文章的第一篇)介绍不同的垃圾收集策略并讨论它们的性质。在阅读本文之前,您应该对 Java 平台中的垃圾收集有基本的认识。第 2 部分将给出一种选择策略的量化方法,以及一些示例。

为什么要有不同的 GC 策略?

能够使用不同的策略使开发人员增加了对应用程序的控制能力。有许多种 GC 算法,每种算法各有优缺点,这取决于工作负载的类型。(如果您不熟悉 GC 算法的一般性主题,那么请参见 参考资料 中其他读物的链接。在 IBM SDK 5.0 中,可以用 4 种策略 之一配置垃圾收集,每种策略都使用自己的算法。默认策略对于大多数应用程序已经足够了。如果对应用程序的性能没有特别的要求,那么您对本文(和下一篇文章)的内容可能不感兴趣;可以在不改变 GC 策略的情况下运行 IBM SDK 5.0。但是,如果应用程序需要最优的性能,或者很关注 GC 停顿时间的长度,那么请读下去。您会看到最新的版本比以前的版本提供了更多选择。

那么,为什么不让 Java 运行时的 IBM 实现自动地替您做出选择呢?因为这不总是可行的。运行时很难了解您的需要。在某些情况下,希望应用程序有很高的吞吐量;而在其他情况下,希望减少停顿时间。

表 1 列出可用的策略并解释每种策略应该在何时使用。后面几节分别详细描述每种策略的性质。

表 1. IBM SDK 5.0 中的 GC 策略
策略选项描述
针对吞吐量进行优化-Xgcpolicy:optthruput (可选)默认策略。对于吞吐量比短暂的 GC 停顿更重要的应用程序,通常使用这种策略。每当进行垃圾收集时,应用程序都会停顿。
针对停顿时间进行优化-Xgcpolicy:optavgpause通过并发地执行一部分垃圾收集,在高吞吐量和短 GC 停顿之间进行折中。应用程序停顿的时间更短。
分代并发-Xgcpolicy:gencon以不同方式处理短期存活的对象和长期存活的对象。采用这种策略时,具有许多短期存活对象的应用程序会表现出更短的停顿时间,同时仍然产生很好的吞吐量。
子池-Xgcpolicy:subpool采用与默认策略相似的算法,但是采用一种比较适合多处理器计算机的分配策略。建议对于有 16 个或更多处理器的 SMP 计算机使用这种策略。这种策略只能在 IBM pSeries® 和 zSeries® 平台上使用。需要扩展到大型计算机上的应用程序可以从这种策略中受益。

一些术语的定义

吞吐量是应用程序处理的数据量。衡量吞吐量的标准是与具体应用程序相关的。

停顿时间是垃圾收集器将所有应用程序线程停下来,从而对堆进行收集所经历的时间。

在本文中,用表 1 中命令行选项中的缩写来表示这些策略:optthruput 表示针对吞吐量进行优化,optavgpause 表示针对停顿时间进行优化,gencon 表示分代并发,subpool 表示子池。

何时应该考虑采用非默认的 GC 策略?

建议您总是先使用默认 GC 策略。 在放弃默认策略之前,需要了解在哪些情况下应该采用其他策略。表 2 给出了一些原因:

表 2. 切换到其他 GC 策略的原因
切换到原因
optavgpause
  • 我的应用程序无法忍受那么长的 GC 停顿时间。如果 GC 停顿时间能够减少的话,性能降低一些也可以接受。
  • 我的应用程序正在一个 64 位平台上运行并使用非常大的堆 —— 超过 3 或 4GB。
  • 我的应用程序是一个 GUI 应用程序,我很关注用户响应时间。
gencon
  • 我的应用程序分配了许多短期存活的对象。
  • 堆空间出现碎片化。
  • 我的应用程序是基于事务的(也就是说,在事务提交之后,事务中的对象就不再存活了)。
subpool
  • 在大型多处理器计算机上,我遇到了可伸缩性问题。

我要强调一点:即使出现了表 2 中提到的原因,也不足以 断言替代策略的性能会更好;它们只是提示。在所有情况下,都应该实际运行应用程序,并度量吞吐量和/或响应时间以及 GC 停顿时间。本系列的下一部分将给出进行这种测试的示例。

本文余下的几节详细描述 GC 策略之间的差异。


optthruput

堆碎片化的征兆

堆碎片化最常见的征兆是过早地出现分配失败。在详细垃圾收集(-Xverbose:gc)中,这表现为尽管堆上有空闲空间,但是仍然触发了垃圾收集。另一个征兆是出现小的线程分配缓存(见 “堆锁和线程分配缓存” 小节)。可以使用 -Xtgc:freelist 来决定平均大小。IBM SDK 5 Diagnostics Guide 中解释了这两个选项(参见 参考资料)。

optthruput 是默认策略。它是一个追踪收集器,称为标志-扫描-紧凑排列(mark-sweep-compact) 收集器。在 GC 期间总是会运行标志和扫描阶段,但是紧凑排列只在某些情况下发生。标志阶段会寻找所有存活的对象并加上标志。扫描阶段会删除所有未加标志的对象。第三个可选的步骤是紧凑排列(compaction)。在某些情况下可能会发生紧凑排列;最常见的情况是系统无法回收足够的空闲空间。

如果非常频繁地分配和释放对象,导致在堆上只留下小块的空闲内存,这时就出现了碎片化。整个堆上可能有大量的空闲空间,但是连续区域很小,导致分配失败。紧凑排列 就是将所有对象向下移动到堆的开头,一个挨一个地排列,让它们之间没有间隔空间。这会消除堆的碎片化,但这是一种代价昂贵的任务,所以只在必要时执行。

图 1 描述三个不同阶段之后的堆布局:标志、扫描和紧凑排列。深色区域表示对象,浅色区域表示空闲空间。

标志和扫描

标志 阶段遍历所有可以从线程堆栈、静态值、interned 字符串和 JNI 引用引用的对象。在这个过程中,创建一个标志位矢量,它定义所有存活对象的开头。

扫描 阶段使用标志阶段生成的标志位矢量,从而识别哪些堆存储块可以回收供以后的分配使用;这些块被添加到空闲列表中。

图 1. 垃圾收集前后的堆布局
垃圾收集前后的堆布局

不同 GC 阶段的工作细节超出了本文的范围;我主要关注确保您理解运行时性质。关于更多细节,请阅读 Diagnostics Guide(参见 参考资料)。

图 2 展示执行时间在应用程序线程(即 mutator)和 GC 线程之间如何分布。水平轴是经历的时间,垂直轴包含线程,其中 n 表示计算机上处理器的数量。对于这个图示,假设应用程序在每个处理器上使用一个线程。GC 由蓝色框表示,这说明 mutator 停止,GC 线程正在运行。这些收集线程占用 100% 的 CPU 资源,mutator 线程空闲。这个图有点儿过分笼统了,这是为了便于与本文中的其他策略进行比较。实际上,GC 的持续时间和频率依赖于应用程序和工作负载。

图 2. 在 optthruput 策略中 CPU 时间在 mutator 和 GC 线程之间的分布
在 optthruput 策略中 CPU 时间在 mutator 和 GC 线程之间的分布

mutator 与 GC 线程

mutator 线程就是分配对象的应用程序。也可以把 mutator 称为应用程序。GC 线程是内存管理的一部分,它们执行垃圾收集。

堆锁和线程分配缓存

optthruput 策略使用连续的堆区域,应用程序中的所有线程共享这个区域。线程需要排他地访问堆,以便为新对象保留空间。这个锁称为堆锁(heap lock),它们确保任意时刻只有一个线程能够分配对象。在有多个 CPU 的计算机上,这个锁会造成伸缩性问题,因为可能同时出现多个分配请求,但是每个请求需要排他地访问堆锁。

为了缓解这个问题,每个线程保留一小块内存,称为线程分配缓存(thread allocation cache) (也称为线程局部堆,TLH)。这块存储空间是一个线程专用的,所以在其中进行分配时不使用堆锁。当分配缓存满了之后,线程使用堆锁向堆请求新的分配缓存。

堆的碎片化会妨碍线程获得较大的 TLH,所以 TLH 会很快被填满,导致应用程序线程频繁地向堆请求新的分配缓存。在这种情况下,堆锁就成了瓶颈;如果出现这样的情况,genconsubpool 策略可能是比较好的替代方案。


optavgpause

对于许多应用程序,吞吐量不如响应时间那么重要。假设一个应用程序要求在 100 毫秒内完成对工作项目的处理。如果 GC 停顿时间在 100 毫秒级别,那么在 GC 期间就无法在规定时间内完成处理。垃圾收集的一个问题是,停顿时间会增加处理项目花费的最大时间。大型堆(在 64 位平台上可用)会加剧这种影响,因为垃圾收集要处理更多的对象。

关于本系列

Java 技术,IBM 风格 系列介绍 Java 平台的 IBM 实现的最新版本。您将了解 IBM 如何实现 Java 平台 5.0 版本中的一些改进,以及如何使用 IBM 在新版本中提供的一些增值特性。

如果想对文章发表评论或提问题,请与相应的作者联系。要对这个系列的整体发表评论,可以联系系列负责人 Chris Bailey。关于这里讨论的概念的更多信息以及下载 IBM 最新版本的链接,请参见 参考资料

optavgpause 是一个替代的 GC 策略,其设计目的是使停顿时间最小化。它并不保证特定的停顿时间,但是停顿时间会比默认 GC 策略产生的停顿时间短。它采用的思路是在应用程序运行的同时并发地执行一些垃圾收集工作。这通过两种手段来实现:

  • 并发的标志和扫描(concurrent mark and sweep):在堆被填满以前,每个 mutator 会让出时间对对象加标志(并发标志)。GC 仍然会停止应用程序的运行,但是停顿时间会显著缩短。在 GC 之后,mutator 线程会让出时间进行扫描(并发扫描)。
  • 后台 GC 线程:在应用程序空闲时,一个(或多个)低优先级的后台 GC 线程会执行标志工作。

根据应用程序的不同,与默认 GC 策略相比,吞吐量性能会有 5% 到 10% 的下降。

图 3 展示在使用 optavgpause 策略时执行时间在 GC 线程和 mutator 线程之间如何分布。没有显示后台追踪线程,因为它应该不会影响应用程序的性能。

图 3. 在 optavgpause 策略中 CPU 时间在 mutator 和 GC 线程之间的分布
在 optavgpause 策略中 CPU 时间在 mutator 和 GC 线程之间的分布

图中的灰色区域表示启用了并发追踪,每个 mutator 线程必须放弃它的一部分处理时间。每个并发阶段之后进行一次完整的垃圾收集,垃圾收集完成在并发阶段没有完成的标志和扫描工作。由此导致的停顿时间应该会比一般 GC(optthruput)短得多,这在图 3 中表现为 GC 框的时间跨度更小。从 GC 结束到并发阶段开始之间的间隔是变化的,但是这个阶段对性能没有显著影响。


gencon

分代的垃圾收集策略考虑到了对象的生命期,并将对象放在堆的不同区域。如果应用程序中的大多数对象在年轻时就死了 (也就是说,它们不会在许多次垃圾收集中存活下来),那么使用单一的堆就会影响性能;分代的垃圾收集方式就是试图解决这个问题。

利用分代的 GC,以不同方式对待长期存活的对象和短期存活的对象。堆分割为婴儿 区域和 长存(tenured) 区域,见图 4。在婴儿区域中创建对象,如果它们存活的时间足够长,就会转移到长存区域。对象在经历了一定次数的垃圾收集之后,如果仍然存活,就会被转移。其思路是大多数对象是短期存活的;通过频繁地收集婴儿区域中的对象,这些对象就可以被释放,而不需要负担收集整个堆的成本。对长存区域的垃圾收集不会频繁进行。

图 4. gencon GC 中的新老区域
gencon 垃圾收集中的新老区域

正如在图 4 中看到的,婴儿区域本身又分成两个区域:分配(allocate)幸存(survivor)。对象在分配区域中进行分配,当分配区域填满时,存活的对象被复制到幸存空间或长存空间(这取决于它们的 “年龄”)。然后,婴儿区域中的空间交换用途,分配变成幸存,幸存变成分配。由已死亡的对象占用的空间可以被新分配覆盖。婴儿收集称为清扫(scavenge);图 5 说明在这个过程中发生了什么:

图 5. GC 前后的堆布局示例
GC 前后的堆布局示例

当分配空间满了时,触发垃圾收集。然后,追踪存活的对象并将它们复制到幸存空间。如果大多数对象已经死亡了,那么这个过程实际上成本很低。另外,已经达到复制次数阈值的对象会转移到长存空间。此后,这个对象就被称为长存的

正如分代并发 这个术语所暗示的,gencon 策略有并发成分。长存空间采用一种与 optavgpause 策略相似的方式进行并发标志,但是没有并发扫描。在并发阶段,所有分配都会导致轻微的吞吐量损失。采用这种方式时,由于长存空间收集导致的停顿时间会比较短。

图 6 显示在运行 gencon GC 时执行时间的分布:

图 6. 在 gencon 策略中 CPU 时间在 mutator 和 GC 线程之间的分布
在 gencon 策略中 CPU 时间在 mutator 和 GC 线程之间的分布

清扫的时间很短(由红色的小框表示)。灰色表示开始并发追踪(以后是收集长存空间),其中的一些操作是并发执行的。这称为全局收集(global collection),它包括一次清扫和一次长存空间收集。进行全局收集的频繁程度取决于堆的大小和对象的生命期。长存空间收集应该相当快,因为它的大部分已经并发地收集了。


subpool

subpool 策略可以帮助在多处理器系统上提高性能。正如前面提到的,只能在 IBM pSeries 和 zSeries 计算机上使用这种策略。堆布局与 optthruput 策略相同,但是空闲列表的结构不一样。不是为整个堆使用一个空闲列表,而是有多个列表,称为子池(subpool)。每个池按照大小进行排序。特定大小的分配请求可以由此大小的池快速地满足。使用原子性(与平台相关的)高性能指令将空闲列表项弹出这个列表,避免了串行访问。图 7 展示了如何按照大小组织空闲存储块:

图 7. 按照大小排序的子池空闲块
按照大小排序的子池空闲块

当 JVM 启动时或进行了紧凑排列时,不使用子池,因为有大块的堆空间空闲着。在这些情况下,每个处理器用自己专用的小型堆来满足请求。当发生第一次垃圾收集时,扫描阶段开始填充子池,后续的分配主要使用子池。

subpool 策略可以减少分配对象花费的时间。原子性指令确保在不需要全局堆锁的情况下执行分配。处理器局部的小型堆会提高效率,因为减少了缓存冲突。这会直接影响可伸缩性,尤其是在多处理器系统上。在不能使用 subpool 的平台上,分代的 GC 可以提供相似的好处。


结束语

本文描述了 IBM SDK 5.0 中的不同 GC 策略以及它们的一些性质。默认策略对于大多数应用程序是足够的;但是,在某些情况下,其他策略的性能更好。我介绍了应该考虑切换到 optavgpausegenconsubpool 的一些一般场景。在对策略进行评估时,对应用程序性能进行度量是非常重要的,第 2 部分将详细演示这个评估过程。

参考资料

学习

获得产品和技术

讨论

  • 参与论坛讨论
  • IBM SDKs and Runtimes:请访问本系列的负责人 Chris Bailey 主持的这个论坛,可以在这里提出关于 IBM Developer Kits for the Java Platform 的问题。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=124536
ArticleTitle=Java 技术,IBM 风格: 垃圾收集策略,第 1 部分
publish-date=06012006