Java Platform, Standard Edition HotSpot 虚拟机垃圾收集调优指南

参考文档

  1. Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide (JDK8官方)

目录


前言

_Java 平台标准版 HotSpot 虚拟机垃圾收集调整指南_描述了 Java HotSpot 虚拟机 (Java HotSpot VM) 中包含的垃圾收集方法,并帮助您确定最适合您需要的方法。

观众

本文档适用于希望提高应用程序性能的应用程序开发人员和系统管理员,尤其是那些处理大量数据、使用多线程和具有高事务率的应用程序。

文档辅助功能

有关 Oracle 对可访问性的承诺的信息,请访问 Oracle 可访问性计划网站:http://www.oracle.com/pls/topic/lookup?ctx=acc&id=docacc

获得 Oracle 支持

购买了支持的 Oracle 客户可以通过 My Oracle Support 获得电子支持。 有关信息,请访问 http://www.oracle.com/pls/topic/lookup?ctx=acc&id=info ,或者如果您有听力障碍的话,也可以访问 http://www.oracle.com/pls/topic/lookup?ctx=acc&id=trs

相关文件

有关详细信息,请参阅以下文档:

惯例

本文档中使用了以下文本约定:

惯例 意义
黑体字 粗体字表示与操作相关的图形用户界面元素,或在文本或词汇表中定义的术语。
斜体 斜体表示书名、重点或您为其提供特定值的占位符变量。
等宽 等宽字体表示段落中的命令、URL、示例中的代码、出现在屏幕上的文本或您输入的文本。

译者注:由于原文和译文的格式差异,该惯例可能存在一些差异。

1 简介

各种各样的应用程序使用 Java Platform, Standard Edition (Java SE),从桌面上的小程序到大型服务器上的 Web 服务。 为了支持这种多样化的部署,Java HotSpot 虚拟机实现 (Java HotSpot VM) 提供了多个垃圾收集器,每个垃圾收集器旨在满足不同的需求。 这是满足大型和小型应用程序需求的重要部分。 Java SE 根据运行应用程序的计算机类别选择最合适的垃圾收集器。 但是,此选择可能并非对每个应用程序都是最佳选择。 具有严格性能目标或其他要求的用户、开发人员和管理员可能需要明确选择垃圾收集器并调整某些参数以达到所需的性能水平。 本文档提供了有助于完成这些任务的信息。 首先,垃圾收集器的一般特性和基本调优选项在串行停止世界收集器的上下文中进行了描述。 然后介绍了其他收集器的具体特征以及选择收集器时要考虑的因素。

垃圾收集器 (GC) 是一种内存管理工具。 它通过以下操作实现了自动内存管理:

垃圾收集器的选择何时重要?对于某些应用程序,答案是否定的。也就是说,应用程序可以在存在垃圾收集的情况下以适度的频率和持续时间暂停执行良好。 但是,对于大量应用程序,尤其是那些具有大量数据(数 GB)、许多线程和高事务率的应用程序,情况并非如此。

阿姆达尔定律(给定问题的并行加速受限于问题的顺序部分)意味着大多数工作负载无法完美并行化; 某些部分始终是顺序的,并且不会从并行性中受益。 Java 平台也是如此。 特别是,Oracle 为 Java SE 1.4 之前的 Java 平台提供的虚拟机不支持并行垃圾收集,因此垃圾收集对多处理器系统的影响相对于其他并行应用程序而言会增加。

图 1-1,“比较垃圾收集中花费的时间百分比” 模拟了一个理想的系统,除了垃圾收集 (GC) 之外,该系统可完美扩展。 红线是应用程序在单处理器系统上仅花费 1% 的时间进行垃圾回收。 这意味着具有 32 个处理器的系统的吞吐量损失超过 20%。 洋红色线表明,对于 10% 的时间用于垃圾收集的应用程序(在单处理器应用程序中,垃圾收集的时间不算离谱),当扩展到 32 个处理器时,超过 75% 的吞吐量会丢失。

**_图1-1 比较垃圾收集所花费的时间百分比
image
“图1-1比较垃圾回收时间百分比”的说明

该图模拟了一个完美可扩展的理想系统,但垃圾收集 (GC) 除外。 它绘制了处理器数量(x 轴)与吞吐量(y 轴)的关系图。 它包含标记为 1% GC、2% GC、3% GC、10% GC、20% GC 和 30% GC 的六条标绘线。 每条线代表一个应用程序吞吐量的变化,该应用程序在单处理器系统和多处理器系统上花费了指定百分比的时间用于垃圾收集。 该图在其前面的文本中进行了描述。

这表明在小型系统上开发时可以忽略不计的速度问题可能会成为扩展到大型系统时的主要瓶颈。 然而,在减少这种瓶颈方面的小改进可以产生很大的性能提升。 对于一个足够大的系统,选择正确的垃圾收集器并在必要时对其进行调整是值得的。

串行收集器通常足以满足大多数“小型”应用程序(那些需要最多约 100 兆字节 (MB) 的堆(在现代处理器上)。其他收集器有额外的开销或复杂性,这是专门行为的代价。如果应用程序 不需要备用收集器的特殊行为,请使用串行收集器。串行收集器不是最佳选择的一种情况是大型、重线程应用程序运行在具有大量内存和两个的机器上 或更多处理器。当应用程序在此类服务器级机器上运行时,默认情况下会选择并行收集器。请参阅 工效学 部分。

本文档是在 Solaris 操作系统(SPARC 平台版)上使用 Java SE 8 作为参考开发的。 但是,此处介绍的概念和建议适用于所有受支持的平台,包括 Linux、Microsoft Windows、Solaris 操作系统(x64 平台版)和 OS X。此外,提到的命令行选项在所有受支持的平台上都可用,尽管 某些选项的默认值在每个平台上可能不同。

2 工效学

工效学 是 Java 虚拟机 (JVM) 和垃圾收集调整(例如基于行为的调整)提高应用程序性能的过程。 JVM 为垃圾收集器、堆大小和运行时编译器提供依赖于平台的默认选择。 这些选择满足不同类型应用程序的需求,同时需要较少的命令行调整。 此外,基于行为的调整会动态调整堆的大小以满足应用程序的特定行为。 本节介绍这些默认选择和基于行为的调整。 在使用后续部分中描述的更详细的控件之前,请先使用这些默认值。

垃圾收集器、堆和运行时编译器默认选择

称为服务器类机器的一类机器被定义为具有以下内容的机器:

  • 2 个或更多物理处理器
  • 2 GB 或更多的物理内存

在服务器级机器上,默认选择以下选项:

  • 吞吐量垃圾收集器
  • 初始堆大小为物理内存的 1/64,最高可达 1 GB
  • 最大堆大小为物理内存的 1/4,最高可达 1 GB
  • 服务器运行时编译器 有关 64 位系统的初始堆和最大堆大小,请参阅 默认堆大小6 并行收集器 中。

服务器级机器的定义适用于所有平台,但运行 Windows 操作系统版本的 32 位平台除外。 表 2-1,“默认运行时编译器”,显示了为 不同平台的运行时编译器。

表 2-1 默认运行时编译器

平台 操作系统 默认值Foot1 默认是否是服务器类Footref1
i586 Linux Client Server
i586 Windows Client ClientFoot2
SPARC (64-bit) Solaris Server ServerFoot3
AMD (64-bit) Linux Server ServerFootref3
AMD (64-bit) Windows Server ServerFootref3
  • [^Footnote1] 客户端表示使用客户端运行时编译器。 服务器表示使用服务器运行时编译器。
  • [^Footnote2] 该策略被选择为即使在服务器类机器上也使用客户端运行时编译器。 做出此选择是因为以前客户端应用程序(例如,交互式应用程序)在这种平台和操作系统的组合上运行得更频繁。
  • [^Footnote3] 仅支持服务器运行时编译器。

基于行为的调优

对于并行收集器,Java SE 提供了两个基于实现应用程序特定行为的垃圾收集调优参数:最大暂停时间目标和应用程序吞吐量目标; 请参阅 并行收集器 部分。 (这两个选项在其他收集器中不可用。)请注意,并非总能满足这些行为。 该应用程序需要一个足够大的堆,以至少容纳所有实时数据。 此外,最小堆大小可能会妨碍实现这些预期目标。

最长停顿时间目标

暂停时间是垃圾收集器停止应用程序并回收不再使用的空间的持续时间。 最大暂停时间目标的目的是限制最长的暂停时间。 垃圾收集器维护暂停的平均时间和该平均值的变化。 平均值是从执行开始时获取的,但经过加权,因此最近的暂停次数更多。 如果平均值加上停顿时间的方差大于最大停顿时间目标,则垃圾收集器认为没有达到目标。 最大暂停时间目标是使用命令行选项 -XX:MaxGCPauseMillis=<nnn> 指定的。 这被解释为对垃圾收集器的提示,即需要 <nnn> 毫秒或更短的暂停时间。 垃圾收集器将调整 Java 堆大小和其他与垃圾收集相关的参数,以试图使垃圾收集暂停时间短于 <nnn> 毫秒。 默认情况下没有最大暂停时间目标。 这些调整可能会导致垃圾收集器更频繁地发生,从而降低应用程序的整体吞吐量。 垃圾收集器尝试在吞吐量目标之前满足任何暂停时间目标。 但是,在某些情况下,无法达到所需的暂停时间目标。

吞吐量目标

吞吐量目标是根据收集垃圾所花费的时间和在垃圾收集之外所花费的时间(称为 应用程序时间 )来衡量的。 目标由命令行选项 -XX:GCTimeRatio=<nnn> 指定。垃圾收集时间与应用程序时间的比率为 1 / (1 + <nnn> )。 例如,-XX:GCTimeRatio=19 将目标设置为垃圾收集总时间的 1/20 或 5%。

垃圾收集花费的时间是年轻代和老年代收集的总时间。如果未达到吞吐量目标,则会增加各代的大小,以增加应用程序可以在两次收集之间运行的时间。

Footprint占用空间目标

如果达到了吞吐量和最大暂停时间目标,则垃圾收集器会减小堆的大小,直到无法满足其中一个目标(总是吞吐量目标)。 然后解决未实现的目标。

调整策略

不要为堆选择最大值,除非您知道您需要一个大于默认最大堆大小的堆。 选择一个足以满足您的应用程序的吞吐量目标。

堆将增长或缩小到支持所选吞吐量目标的大小。 应用程序行为的变化会导致堆增大或缩小。 例如,如果应用程序开始以更高的速率分配,堆将增长以保持相同的吞吐量。

如果堆增长到其最大大小并且未满足吞吐量目标,则最大堆大小对于吞吐量目标来说太小了。 将最大堆大小设置为接近平台上总物理内存但不会导致应用程序交换的值。 再次执行应用程序。 如果仍然没有达到吞吐量目标,那么应用程序时间的目标对于平台上的可用内存来说太高了。

如果可以达到吞吐量目标,但停顿时间过长,则选择最大停顿时间目标。 选择最大暂停时间目标可能意味着无法满足您的吞吐量目标,因此请选择应用程序可接受的折衷值。

当垃圾收集器试图满足竞争目标时,堆的大小通常会波动。 即使应用程序已达到稳定状态也是如此。 实现吞吐量目标(可能需要更大的堆)的压力与最大暂停时间和最小占用空间(两者都可能需要较小的堆)的目标竞争。

3 代

Java SE 平台的优势之一是它使开发人员免于内存分配和垃圾收集的复杂性。 然而,当垃圾收集成为主要瓶颈时,了解这个隐藏实现的某些方面是很有用的。 垃圾收集器对应用程序使用对象的方式做出假设,这些都反映在可调参数中,可以在不牺牲抽象能力的情况下调整这些参数以提高性能。

当运行程序中的任何指针都无法再访问某个对象时,该对象就被认为是垃圾。 最直接的垃圾收集算法遍历每个可到达的对象。 剩下的任何对象都被视为垃圾。 这种方法所花费的时间与活动对象的数量成正比,这对于维护大量活动数据的大型应用程序来说是令人望而却步的。

虚拟机包含许多不同的垃圾收集算法,这些算法使用 generational collection 组合。朴素的垃圾收集检查堆中的每个活动对象,而分代收集利用大多数应用程序的几个凭经验观察到的属性来最小化回收未使用(垃圾)对象所需的工作。 这些观察到的属性中最重要的是弱_世代假设_,它指出大多数物体只能存活很短的时间。

图 3-1,“对象生命周期的典型分布” 中的蓝色区域 是对象生命周期的典型分布。 x 轴是以分配的字节为单位测量的对象生命周期。 y 轴上的字节数是具有相应生命周期的对象中的总字节数。左边的尖峰表示在分配后不久就可以回收(换句话说,已经“死亡”)的对象。 例如,迭代器对象通常在单个循环的持续时间内处于活动状态。

图 3-1 对象生命周期的典型分布
image
“图 3-1 对象生命周期的典型分布”说明

此图的 x 轴“分配的字节数”表示以分配的字节数衡量的对象生命周期。 y 轴“Bytes surviving”是具有相应生命周期的对象的总字节数。 该图左侧的三分之一标记为“次要收集”。 图表右侧的三分之二标记为“主要收藏”。 绘制线下方的区域为实心且颜色为蓝色。 该区域代表对象生命周期的典型分布。 该区域在左侧急剧上升并向右延伸。 该图在它周围的文本中有进一步描述。

有些物体确实寿命更长,因此分布向右延伸。 例如,通常有一些在初始化时分配的对象会一直存在到进程退出。 在这两个极端之间的是在某些中间计算期间存在的对象,这里被视为初始峰值右侧的块。 一些应用程序看起来分布非常不同,但数量惊人的多都具有这种一般形状。 通过关注大多数对象“早逝”这一事实,可以实现高效收集。

为了针对这种情况进行优化,内存在 generations(内存池保存不同年龄的对象)中进行管理。 垃圾收集发生在每一代填满时。 绝大多数对象都分配在专用于年轻对象(年轻一代)的池中,并且大多数对象都死在那里。 当年轻一代填满时,它会引发一个 minor collection,其中只有年轻一代被收集; 其他世代的垃圾不回收。 Minor collections 可以优化,假设弱代假设成立并且年轻代中的大多数对象都是垃圾并且可以被回收。 此类收集的成本首先与收集的活动对象数量成正比; 很快就会收集充满死对象的年轻一代。 通常,在每次小收集期间,年轻一代中的一些幸存对象会被移动到老年代。 最终,tenured generation 将填满并且必须被收集,导致 major collection,其中收集整个堆。 主要收集通常比次要收集持续的时间长得多,因为涉及的对象数量要多得多。

正如 工效学 部分所述,工效学会动态选择垃圾收集器以提供良好的性能 在各种应用程序上。 串行垃圾收集器专为具有小数据集的应用程序而设计,其默认参数被选择为对大多数小应用程序有效。 并行或吞吐量垃圾收集器旨在与具有中型到大型数据集的应用程序一起使用。 工效学选择的堆大小参数加上自适应大小策略的特性旨在为服务器应用程序提供良好的性能。 这些选择在大多数(但不是所有)情况下都适用,这导致了本文档的中心原则:

注:
如果垃圾收集成为瓶颈,您很可能必须自定义总堆大小以及各个代的大小。 检查详细的垃圾收集器输出,然后探索您的个人性能指标对垃圾收集器参数的敏感性。

图 3-2,“代的默认排列,并行收集器和 G1 除外” 显示默认的世代安排(对于除并行收集器和 G1 之外的所有收集器):

图 3-2 生成的默认排列,并行收集器和 G1 除外
image
[“图 3-2 默认分代安排,并行收集器和 G1 除外”的说明](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/img_text/jsgct_dt_001_armgnt_gn. 网页)

此图由一排六个矩形组成。 这些矩形标记如下(从左到右):

  1. Eden
  2. Survivor
  3. Spaces
  4. Virtual
  5. No label
  6. Virtual
    矩形 1 到 4 标记为“年轻”。 矩形 5 到 6 标记为“Tenured”。

在初始化时,最大地址空间实际上是保留的,但除非需要,否则不会分配给物理内存。 为对象内存保留的完整地址空间可以分为年轻代和老年代。

新生代由伊甸园和两个幸存者空间组成。 大多数对象最初是在伊甸园中分配的。 一个幸存者空间在任何时候都是空的,作为伊甸园中任何活物体的归宿; 另一个幸存者空间是下一次复制收集期间的目的地。 对象以这种方式在幸存者空间之间复制,直到它们足够老可以被永久使用(复制到永久代)。

性能注意事项

垃圾收集性能有两个主要衡量标准:

  • 吞吐量 是长时间内未花费在垃圾收集上的总时间的百分比。 吞吐量包括花费在分配上的时间(但通常不需要调整分配速度)。
  • 暂停 是应用程序由于正在进行垃圾收集而出现无响应的时间。

用户对垃圾回收有不同的要求。 例如,一些人认为 Web 服务器的正确指标是吞吐量,因为垃圾收集期间的暂停可能是可以容忍的,或者只是被网络延迟所掩盖。 然而,在交互式图形程序中,即使是短暂的停顿也可能对用户体验产生负面影响。

一些用户对其他注意事项很敏感。 Footprint占用空间 是进程的工作集,以页面和缓存行为单位进行衡量。 在物理内存有限或进程较多的系统上,占用空间可能决定可伸缩性。 Promptness(敏捷) 是对象死亡和内存变为可用之间的时间,这是分布式系统(包括远程方法调用 (RMI))的一个重要考虑因素。

通常,为特定代选择大小是这些考虑因素之间的权衡。 例如,一个非常大的年轻一代可能会最大化吞吐量,但这样做会以占用空间、及时性和暂停时间为代价。 可以通过以吞吐量为代价使用较小的年轻代来最小化年轻代暂停。 一代的大小不会影响另一代的收集频率和暂停时间。

没有一种正确的方法来选择一代的大小。 最佳选择取决于应用程序使用内存的方式以及用户要求。 因此,虚拟机对垃圾收集器的选择并不总是最佳的,并且可能会被 调整代数 部分中描述的命令行选项覆盖。

测量

吞吐量和占用空间最好使用特定于应用程序的指标来衡量。 例如,可以使用客户端负载生成器测试 Web 服务器的吞吐量,而可以使用 pmap 命令在 Solaris 操作系统上测量服务器的占用空间。 但是,通过检查虚拟机本身的诊断输出可以很容易地估计由于垃圾收集而导致的暂停。 命令行选项 -verbose:gc 会导致在每次收集时打印有关堆和垃圾收集的信息。 例如,这是一个大型服务器应用程序的输出:

[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]

输出显示两个次要收集,然后是一个主要收集。箭头前后的数字(例如,第一行的 325407K->83000K )分别表示垃圾回收前后活动对象的组合大小。 minor collections 后,size 中包含了一些对象,这些对象是垃圾(不再存活)但无法回收。 这些对象要么包含在 tenured generation 中,要么从 tenured generation 中引用。

括号中的下一个数字(例如,来自第一行的 (776768K) )是堆的提交大小:无需从操作系统请求更多内存即可用于 Java 对象的空间量。 请注意,此数字仅包括一个幸存者空间。 除了垃圾回收期间,在任何给定时间都只会使用一个幸存者空间来存储对象。

该行的最后一项(例如,0.2300771 秒)表示执行收集所花费的时间,在本例中大约为四分之一秒。

第三行中主要收集的格式类似。

注:
-verbose:gc 产生的输出格式在未来的版本中可能会发生变化。

命令行选项 -XX:+PrintGCDetails 会导致打印有关收集的附加信息。 此处显示了使用串行垃圾收集器的 -XX:+PrintGCDetails 输出示例。

[GC [DefNew: 64575K->959K(64576K), 0.0457646 secs] 196016K->133633K(261184K), 0.0459067 secs]

这表明 minor collection 恢复了大约 98% 的年轻代,DefNew: 64575K->959K(64576K) 并花费了 0.0457646 秒(大约 45 毫秒)。 整个堆的使用率减少到大约 51% (196016K->133633K(261184K)),并且收集有一些额外的开销(超过年轻代的收集),如最终结果所示 0.0459067 秒 的时间。

注:
-XX:+PrintGCDetails 产生的输出格式在未来的版本中可能会发生变化。 选项 -XX:+PrintGCTimeStamps 在每个收集的开始添加时间戳。 这对于查看垃圾收集发生的频率很有用。

111.042: [GC 111.042: [DefNew: 8128K->8128K(8128K), 0.0000505 secs] 111.042: [Tenured: 18154K->2311K(24576K), 0.1290354 secs] 26282K->2311K(32704K), 0.1293306 secs]

收集在应用程序执行后大约 111 秒开始。 次要收集大约在同一时间开始。 此外,还显示了由 Tenured 划定的主要馆藏的信息。 老年代使用率降低到大约 10% (18154K->2311K(24576K)) 并花费了 0.1290354 秒(大约 130 毫秒)。

4 确定世代规模

许多参数影响生成大小。 图 4-1,“堆参数” 说明了堆中的 Committed 和 Virtual 之间的区别。 在虚拟机初始化时,为堆保留整个空间。 保留空间的大小可以使用 -Xmx 选项指定。 如果“-Xms”参数的值小于“-Xmx”参数的值,那么并非所有保留的空间都会立即提交给虚拟机。 未提交的空间在此图中标记为“虚拟”。 堆的不同部分(tenured generation 和 young generation)可以根据需要增长到虚拟空间的极限。

一些参数是堆的一部分与另一部分的比率。 例如,参数 NewRatio 表示老一代与年轻一代的相对大小。

图 4-1 堆参数
image
图4-1堆参数说明

该图由一排六个矩形组成,标记如下(从左到右):

  1. Eden
  2. Survivor
  3. Survivor
  4. Virtual
  5. Tenured
  6. Virtual
    矩形组标记如下:
  • 总大小:矩形1到6
  • Committed vs. Virtual:Committed 由矩形 1 到 3 和 5 组成; virtual 由矩形 4 和 6 组成
  • Tenured to Young ratio:Tenured由矩形1-4组成; 年轻的由矩形 5-6 组成
  • Eden to Survivor 空间比例:Eden 为矩形 1; 幸存者空间为矩形 2

总堆

以下关于堆和默认堆大小的增长和收缩的讨论不适用于并行收集器。 (请参阅 6 并行收集器 部分在 4 确定世代规模 有关并行收集器的堆大小调整和默认堆大小的详细信息。)但是,控制总数的参数 堆的大小和世代的大小确实适用于并行收集器。 影响垃圾收集性能的最重要因素是可用内存总量。 因为收集发生在各代填满时,吞吐量与可用内存量成反比。

默认情况下,虚拟机会在每次收集时增大或缩小堆,以尽量将每次收集时可用空间与活动对象的比例保持在特定范围内。 此目标范围由参数-XX:MinHeapFreeRatio=<minimum>-XX:MaxHeapFreeRatio=<maximum> 设置为百分比,并且总大小低于-Xms<min> 及以上为 -Xmx<max>。 64 位 Solaris 操作系统(SPARC 平台版)的默认参数显示在 表 4-1 64 位 Solaris 操作系统的默认参数

表 4-1 64 位 Solaris 操作系统的默认参数 ^dc3d9b

Parameter Default Value
MinHeapFreeRatio 40
MaxHeapFreeRatio 70
-Xms 6656k
-Xmx calculated

使用这些参数,如果一代中可用空间的百分比低于 40%,则该代将扩展以保持 40% 的可用空间,直至达到该代允许的最大大小。 类似地,如果可用空间超过 70%,则生成将收缩,以便只有 70% 的空间可用,以生成的最小大小为准。

表 4-1 64 位 Solaris 操作系统的默认参数 ,默认最大堆大小是由 JVM 计算的值。 Java SE 中用于并行收集器和服务器 JVM 的计算现在用于所有垃圾收集器。 部分计算是最大堆大小的上限,这对于 32 位平台和 64 位平台是不同的。

请参阅 6 并行收集器 中的 默认堆大小 部分。客户端 JVM 有类似的计算,这导致最大堆大小小于服务器 JVM。

以下是有关服务器应用程序堆大小的一般准则:

  • 除非您遇到暂停问题,否则请尝试为虚拟机分配尽可能多的内存。 默认大小通常太小。
  • -Xms-Xmx 设置为相同的值,通过从虚拟机中删除最重要的大小调整决策来提高可预测性。 但是,如果您做出了错误的选择,虚拟机将无法进行补偿。
  • 通常,随着处理器数量的增加而增加内存,因为分配可以并行化。

年轻一代

在总可用内存之后,影响垃圾收集性能的第二大影响因素是专用于年轻代的堆的比例。 年轻一代越大,minor collections 发生的频率就越低。 然而,对于有限的堆大小,较大的新生代意味着较小的老年代,这将增加主要收集的频率。 最佳选择取决于应用程序分配的对象的生命周期分布。

默认情况下,新生代大小由参数 NewRatio 控制。 例如,设置-XX:NewRatio=3表示年轻代和老年代的比例为1:3。 换句话说,eden 和 survivor 空间的组合大小将是总堆大小的四分之一。

参数 NewSize 和 MaxNewSize 从下方和上方限制了年轻代的大小。 将这些设置为相同的值可以修复年轻代,就像将 -Xms 和 -Xmx 设置为相同的值可以修复总堆大小一样。 这对于以比 NewRatio 允许的整数倍更细的粒度调整年轻代很有用。

幸存者空间大小

您可以使用参数 SurvivorRatio 来调整幸存者空间的大小,但这通常对性能并不重要。 例如,-XX:SurvivorRatio=6 将伊甸园和幸存者空间之间的比例设置为 1:6。 换句话说,每个幸存者空间将是伊甸园大小的六分之一,因此是年轻代大小的八分之一(不是七分之一,因为有两个幸存者空间)。

如果幸存者空间太小,复制收集会直接溢出到老年代。 如果幸存者空间太大,它们将毫无用处地空着。 在每次垃圾回收时,虚拟机都会选择一个阈值,即一个对象在被使用之前可以被复制的次数。 选择此阈值是为了让幸存者保持半满状态。 命令行选项 -XX:+PrintTenuringDistribution(并非所有垃圾收集器都可用)可用于显示此阈值和新一代对象的年龄。 它对于观察应用程序的生命周期分布也很有用。

表 4-2,“Survivor 空间大小调整的默认参数值”提供了 64 位 Solaris 的默认值:

表 4-2 幸存者空间大小调整的默认参数值

Parameter Server JVM Default Value
NewRatio 2
NewSize 1310M
MaxNewSize not limited
SurvivorRatio 8

新生代的最大大小将根据总堆的最大大小和 NewRatio 参数的值计算得出。 MaxNewSize 参数的“不受限制”默认值意味着计算值不受 MaxNewSize 的限制,除非在命令行上指定了 MaxNewSize 的值。

以下是服务器应用程序的一般准则:

  • 首先决定你能负担得起的最大堆大小给虚拟机。 然后将你的性能指标与年轻一代的大小进行对比,以找到最佳设置。
    • 请注意,最大堆大小应始终小于机器上安装的内存量,以避免过多的页面错误和抖动。
  • 如果总堆大小是固定的,那么增加新生代的大小需要减少老年代的大小。 保持老年代足够大以容纳应用程序在任何给定时间使用的所有实时数据,外加一定量的松弛空间(10% 到 20% 或更多)。
  • 受制于前面所述的对终身一代的限制:
    • 为年轻一代提供充足的内存。
    • 随着处理器数量的增加而增加新生代的大小,因为分配可以并行化。

5 可用的收集器

到目前为止的讨论都是关于串行收集器的。 Java HotSpot VM 包括三种不同类型的收集器,每种都有不同的性能特征。

  • 串行收集器使用单个线程来执行所有垃圾收集工作,这使得它相对高效,因为线程之间没有通信开销。 它最适合单处理器机器,因为它不能利用多处理器硬件,尽管它在多处理器上对于具有小数据集(最多大约 100 MB)的应用程序很有用。 在某些硬件和操作系统配置中默认选择串行收集器,或者可以使用选项-XX:+UseSerialGC 显式启用。
  • 并行收集器(也称为 吞吐量收集器 并行执行次要收集,这可以显着减少垃圾收集开销。 它适用于在多处理器或多线程硬件上运行的具有中型到大型数据集的应用程序。 并行收集器在某些硬件和操作系统配置上默认选择,或者可以使用选项 -XX:+UseParallelGC 显式启用。
    • 并行压缩是一种使并行收集器能够并行执行主要收集的功能。 如果没有并行压缩,主要的收集是使用单个线程执行的,这会显着限制可伸缩性。 如果指定了选项-XX:+UseParallelGC,则默认启用并行压缩。 关闭它的选项是-XX:-UseParallelOldGC
  • 大多数并发收集器 并发执行其大部分工作(例如,当应用程序仍在运行时)以保持垃圾收集暂停时间短。 它专为具有中型到大型数据集的应用程序而设计,在这些应用程序中响应时间比总吞吐量更重要,因为用于最小化暂停的技术会降低应用程序性能。 Java HotSpot VM 提供了两个主要并发的收集器之间的选择; 请参阅 大多数并发收集器。 使用选项-XX:+UseConcMarkSweepGC 启用 CMS 收集器或使用选项-XX:+UseG1GC 启用 G1 收集器。

选择收集器

除非你的应用程序有相当严格的暂停时间要求,否则首先运行你的应用程序并允许 VM 选择一个收集器。 如有必要,调整堆大小以提高性能。 如果性能仍未达到您的目标,请使用以下指南作为选择收集器的起点。

  • 如果应用程序的数据集较小(最多约 100 MB),则 使用选项-XX:+UseSerialGC 选择串行收集器。
  • 如果应用程序将在单处理器上运行并且没有暂停时间要求,则让 VM 选择收集器,或者使用选项“-XX:+UseSerialGC”选择串行收集器。
  • 如果 (a) 峰值应用程序性能是第一优先级并且 (b) 没有暂停时间要求或 1 秒或更长时间的暂停是可以接受的,那么让 VM 选择收集器,或者选择带有 -XX:+UseParallelGC
  • 如果响应时间比整体吞吐量更重要,并且垃圾收集暂停必须保持短于大约 1 秒,则选择带有 -XX:+UseConcMarkSweepGC-XX:+UseG1GC 的并发收集器。

这些指南仅提供了选择收集器的起点,因为性能取决于堆的大小、应用程序维护的实时数据量以及可用处理器的数量和速度。 暂停时间对这些因素特别敏感,因此前面提到的 1 秒阈值只是一个近似值:并行收集器在许多数据大小和硬件组合上都会经历超过 1 秒的暂停时间; 相反,并发收集器可能无法在某些组合上保持短于 1 秒的暂停。

如果推荐的收集器没有达到预期的性能,首先尝试调整堆和生成大小以满足预期的目标。 如果性能仍然不足,则尝试不同的收集器:使用并发收集器来减少暂停时间并使用并行收集器来增加多处理器硬件上的整体吞吐量。

6 并行收集器

并行收集器(这里也称为 吞吐量收集器 )是一种类似于串行收集器的分代收集器; 主要区别在于使用多个线程来加速垃圾收集。 并行收集器通过命令行选项 -XX:+UseParallelGC 启用。 默认情况下,使用此选项,并行执行次要和主要收集以进一步减少垃圾收集开销。

在具有 N 硬件线程(其中 N 大于 8)的机器上,并行收集器使用 N 的固定分数作为垃圾收集器线程数。 对于较大的 N 值,分数约为 5/8。 在 N 的值低于 8 时,使用的数字是 N。 在选定的平台上,该比例降至 5/16。 垃圾收集器线程的具体数量可以通过命令行选项进行调整(稍后介绍)。 在具有一个处理器的主机上,由于并行执行所需的开销(例如,同步),并行收集器的性能可能不如串行收集器。 然而,当运行具有中型到大型堆的应用程序时,它通常在具有两个处理器的机器上略胜串行收集器,并且通常在有两个以上处理器可用时比串行收集器表现更好。

可以使用命令行选项 -XX:ParallelGCThreads=<N> 控制垃圾收集器线程的数量。 如果使用命令行选项显式调整堆,那么并行收集器获得良好性能所需的堆大小与串行收集器所需的堆大小相同。 但是,启用并行收集器应该可以缩短收集暂停时间。 因为多个垃圾收集器线程正在参与次要收集,所以由于在收集期间从年轻代提升到老年代,可能会产生一些碎片。 minor collection 中涉及的每个垃圾回收线程都会保留一部分老年代用于提升,将可用空间划分为这些“提升缓冲区”会导致碎片效应。 减少垃圾收集器线程的数量并增加老年代的大小将减少这种碎片效应。

世代

如前所述,并行收集器中代的排列是不同的。 这种安排显示在 图 6-1,“并行收集器中的世代安排”

图6-1 并行收集器中代的排列
image
“图 6-1 并行收集器中世代排列”的说明

此图显示了并行收集器中代的排列。 该图由一排六个矩形组成。 这些矩形标记如下(从左到右):

  1. No label
  2. Virtual
  3. Virtual
  4. Eden
  5. Survivor
  6. Spaces
    矩形 1 和 2 标记为“Tenured”。 矩形 3 到 6 标记为“年轻”。 矩形 7 和 8 标记为“Perm”。

并行收集器工效学

服务器级机器默认选择并行收集器。 此外,并行收集器使用一种自动调整方法,允许您指定特定行为而不是生成大小和其他低级别调整细节。 您可以指定最大垃圾收集暂停时间、吞吐量和占用空间(堆大小)。

  • 最大垃圾收集暂停时间:最大暂停时间目标是使用命令行选项 -XX:MaxGCPauseMillis=<N> 指定的。 这被解释为暗示需要 <N> 毫秒或更短的暂停时间; 默认情况下,没有最长暂停时间目标。 如果指定了暂停时间目标,则会调整堆大小和与垃圾收集相关的其他参数,以尝试使垃圾收集暂停时间短于指定值。 这些调整可能会导致垃圾收集器降低应用程序的整体吞吐量,并且无法始终满足所需的暂停时间目标。
  • 吞吐量:吞吐量目标是根据垃圾收集所花费的时间与垃圾收集之外所花费的时间(称为应用程序时间)来衡量的。 目标由命令行选项 -XX:GCTimeRatio=<N> 指定,它将垃圾收集时间与应用程序时间的比率设置为 1 / (1 +<N>) .
    • 例如,-XX:GCTimeRatio=19 将目标设置为垃圾收集总时间的 1/20 或 5%。 默认值为 99,目标是 1% 的时间用于垃圾回收。
  • Footprint占用空间:最大堆占用空间使用选项 -Xmx<N> 指定。 此外,收集器有一个隐含的目标,即在满足其他目标的情况下最小化堆的大小。

目标的优先级

这些目标按以下顺序处理:

  1. 最大停顿时间目标
  2. 吞吐量目标
  3. 最小占用空间目标

首先满足最大暂停时间目标。 只有在满足它之后,吞吐量目标才会得到解决。 同样,只有在满足前两个目标后,才会考虑footprint占用空间目标。

生成大小调整

收集器保存的平均停顿时间等统计信息在每次收集结束时更新。 然后进行确定是否已达到目标的测试,并对生成的大小进行任何必要的调整。 例外情况是显式垃圾收集(例如,调用 System.gc())在保留统计信息和调整生成大小方面会被忽略。

增加和缩小一代的大小是通过增量来完成的,增量是一代大小的固定百分比,以便一代逐步增加或减少到其所需的大小。 增长和收缩以不同的速度进行。 默认情况下,一代以 20% 的增量增长,以 5% 的增量收缩。 增长的百分比由年轻一代的命令行选项 -XX:YoungGenerationSizeIncrement=<Y> 和老一代的 -XX:TenuredGenerationSizeIncrement=<T> 控制。 一代收缩的百分比由命令行标志 -XX:AdaptiveSizeDecrementScaleFactor=<D> 调整。 如果增长增量为 X 百分比,则收缩减量为 X/D 百分比。

如果收集器决定在启动时增长一代,那么增量中会添加一个补充百分比。 此补品随收藏次数递减,无远期作用。 补充的目的是提高启动性能。 收缩的百分比没有补充。

如果未达到最大暂停时间目标,则一次仅缩小一代的大小。 如果两代人的停顿时间都在目标之上,那么停顿时间较大的那一代人的规模首先缩小。

如果未达到吞吐量目标,则两代的大小都会增加。 每一个都与其各自对总垃圾收集时间的贡献成比例地增加。 例如,如果新生代的垃圾收集时间是总收集时间的 25%,如果新生代的完整增量为 20%,那么新生代将增加 5%。

默认堆大小

除非在命令行中指定初始和最大堆大小,否则它们是根据机器上的内存量计算的。

客户端 JVM 默认初始和最大堆大小

默认最大堆大小是物理内存的一半,最大物理内存大小为 192 兆字节 (MB),否则为物理内存的四分之一,最大物理内存大小为 1 千兆字节 (GB)。

例如,如果您的计算机有 128 MB 的物理内存,那么最大堆大小为 64 MB,大于或等于 1 GB 的物理内存导致最大堆大小为 256 MB。

JVM 实际上并不使用最大堆大小,除非您的程序创建了足够多的对象来需要它。 在 JVM 初始化期间分配的数量要少得多,称为 初始堆大小 。 此数量至少为 8 MB,否则为物理内存的 1/64,直到 1 GB 的物理内存大小。

分配给新生代的最大空间量是总堆大小的三分之一。

服务器 JVM 默认初始和最大堆大小

默认的初始和最大堆大小在服务器 JVM 上的工作方式与在客户端 JVM 上的工作方式类似,只是默认值可以更高。 在 32 位 JVM 上,如果有 4 GB 或更多的物理内存,默认的最大堆大小可以达到 1 GB。 在 64 位 JVM 上,如果有 128 GB 或更多的物理内存,默认的最大堆大小可以达到 32 GB。 您始终可以通过直接指定这些值来设置更高或更低的初始堆和最大堆; 请参阅下一节。

指定初始和最大堆大小

您可以使用标志“-Xms”(初始堆大小)和“-Xmx”(最大堆大小)指定初始和最大堆大小。 如果您知道您的应用程序需要多少堆才能正常运行,您可以将 -Xms 和 -Xmx 设置为相同的值。 如果不是,JVM 将首先使用初始堆大小,然后增加 Java 堆,直到它在堆使用和性能之间找到平衡。

其他参数和选项会影响这些默认值。 要验证您的默认值,请使用“-XX:+PrintFlagsFinal”选项并在输出中查找“MaxHeapSize”。 例如,在 Linux 或 Solaris 上,您可以运行以下命令:

java -XX:+PrintFlagsFinal <GC options> -version | grep MaxHeapSize

过多的 GC 时间和 OutOfMemoryError

如果在垃圾收集 (GC) 上花费了太多时间,并行收集器会抛出一个 OutOfMemoryError:如果超过 98% 的总时间花在了垃圾收集上并且只有不到 2% 的堆被回收,那么一个 OutOfMemoryError 被抛出。 此功能旨在防止应用程序长时间运行而由于堆太小而进展甚微或根本没有进展。 如有必要,可以通过向命令行添加选项-XX:-UseGCOverheadLimit 来禁用此功能。

测量

并行收集器的详细垃圾收集器输出与串行收集器的输出基本相同。

7 大多数并发收集器

Java Hotspot VM在JDK 8中有两个主要是并发的收集器:

并发标记扫描(CMS)收集器:此收集器适用于那些喜欢较短垃圾收集暂停时间并且能够与垃圾收集共享处理器资源的应用程序。

垃圾第一垃圾收集器:这个服务器风格的收集器适用于具有大内存的多处理器机器。它在实现高吞吐量的同时,高概率地满足垃圾收集暂停时间目标。

并发开销

大多数并发收集器将处理器资源(否则应用程序将可用)用于更短的主要收集暂停时间。最明显的开销是在收集的并发部分使用一个或多个处理器。在N个处理器的系统上,收集的并发部分将使用可用处理器的K/N,其中1<=K<=上限{N/4}。(请注意,K的精确选择和边界可能会发生变化。)除了在并发阶段使用处理器之外,还需要额外的开销来实现并发。因此,虽然并发收集器的垃圾收集暂停时间通常要短得多,但应用程序吞吐量也往往略低于其他收集器。

在具有多个处理核心的机器上,在收集的并发部分,处理器可用于应用程序线程,因此并发垃圾收集器线程不会“暂停”应用程序。这通常会导致更短的暂停,但应用程序可用的处理器资源也会更少,而且应该会出现一些放缓,尤其是在应用程序最大限度地使用所有处理核心的情况下。随着N的增加,由于并发垃圾收集导致的处理器资源减少会变小,并发收集带来的好处也会增加。并发标记扫描(CMS)收集器中的并发模式故障一节讨论了这种扩展的潜在限制。

因为在并发阶段至少有一个处理器用于垃圾收集,所以并发收集器通常不会在单处理器(单核)机器上提供任何好处。然而,CMS(而不是G1)有一个单独的模式,可以在只有一个或两个处理器的系统上实现低暂停;有关详细信息,请参阅 并发标记扫描(CMS)收集器 中的 增量模式 。此功能在Java SE 8中已被弃用,可能会在以后的主要版本中删除。

其他参考文献

垃圾优先垃圾回收器:

http://www.oracle.com/technetwork/java/javase/tech/g1-intro-jsp-135488.html

垃圾优先垃圾回收器调优:

http://www.oracle.com/technetwork/articles/java/g1gc-1984535.html

8 并发标记扫描(CMS)收集器

Concurrent Mark Sweep(CMS)收集器是为那些喜欢较短垃圾收集暂停时间的应用程序设计的,并且能够在应用程序运行时与垃圾收集器共享处理器资源。通常,具有相对较大的长期数据集(大量长期生成)并在具有两个或多个处理器的机器上运行的应用程序往往会从使用此收集器中受益。但是,对于任何暂停时间要求较低的应用程序,都应该考虑使用此收集器。CMS收集器是通过命令行选项-XX:+UseConcMarkSweepGC启用的。

与其他可用的收集器类似,CMS收集器是一代式的;因此,既发生了小收集,也发生了大收集。CMS收集器通过使用单独的垃圾收集器线程在执行应用程序线程的同时跟踪可访问对象,试图减少由于主要收集而导致的暂停时间。在每个主要的收集周期中,CMS收集器在收集开始时暂停所有应用程序线程一小段时间,并在收集中间再次暂停。第二次停顿往往是两次停顿中较长的一次。在两次暂停期间都使用多个线程来完成收集工作。收集的其余部分(包括对活动对象的大部分跟踪和对不可访问对象的清除)是由一个或多个与应用程序同时运行的垃圾收集器线程完成的。小收集可以与正在进行的主循环交错进行,其方式类似于并行收集器(特别是,应用程序线程在小收集期间会停止)。

并发模式故障

CMS收集器使用一个或多个与应用程序线程同时运行的垃圾收集器线程,其目标是在终身使用的生成变满之前完成收集。如前所述,在正常操作中,CMS收集器在应用程序线程仍在运行的情况下进行大部分跟踪和清除工作,因此应用程序线程只能看到短暂的暂停。但是,如果CMS收集器无法在终身生成填满之前完成回收不可访问的对象,或者如果分配无法满足终身生成中的可用可用空间块,则应用程序将暂停,并且在停止所有应用程序线程的情况下完成收集。无法同时完成收集被称为并发模式故障,表示需要调整CMS收集器参数。如果并发收集被显式垃圾收集(System.gc())或为诊断工具提供信息所需的垃圾收集中断,则会报告并发模式中断。

GC时间过长和内存不足错误

如果在垃圾收集中花费了太多时间,CMS收集器将抛出“OutOfMemoryError”:如果超过98%的总时间用于垃圾收集,而堆的恢复率不到2%,则抛出“OutOfMemoryError”。此功能旨在防止应用程序长时间运行,同时由于堆太小而几乎没有进展。如有必要,可以通过在命令行中添加选项 -XX:-UseGCOverheadLimit 来禁用此功能。

该策略与并行收集器中的策略相同,只是执行并发收集所花费的时间不计入98%的时间限制。换句话说,只有在应用程序停止时执行的收集才会计入过多的GC时间。这样的收集通常是由于并发模式故障或显式收集请求(例如,对 System.gc() 的调用)造成的。

漂浮垃圾

CMS收集器与Java HotSpot VM中的所有其他收集器一样,是一个跟踪收集器,它至少标识堆中所有可访问的对象。用Richard Jones和Rafael D.Lins在他们的出版物《Garbage Collection:Algorithms for Automated Dynamic Memory》中的说法,它是一个增量更新收集器。由于应用程序线程和垃圾收集器线程在主收集过程中同时运行,因此垃圾收集器线程跟踪的对象可能会在收集过程结束时变得不可访问。这种尚未回收的无法访问的对象被称为浮动垃圾。 浮动垃圾 的数量取决于并发收集周期的持续时间和应用程序进行引用更新(也称为 mutations )的频率。此外,由于年轻一代和终身一代是独立收集的,因此每一代都是另一代的根源。作为一个粗略的指导方针,尝试将终身发电的规模增加20%,以考虑浮动垃圾。在一个并发收集周期结束时堆中的浮动垃圾将在下一个收集周期中收集。

暂停

CMS收集器在并发收集周期中暂停应用程序两次。第一个暂停是将从根(例如,来自应用程序线程堆栈和寄存器的对象引用、静态对象等)和堆中其他地方(例如,年轻一代)直接可达的对象标记为活动对象。第一次暂停被称为 初始标记暂停 。第二次暂停出现在并发跟踪阶段结束时,并查找由于CMS收集器完成跟踪对象后应用程序线程更新对象中的引用而导致并发跟踪遗漏的对象。第二次暂停被称为 再标记暂停

并行阶段

可达对象图的并发跟踪发生在初始标记暂停和再标记暂停之间。在这个并发跟踪阶段,一个或多个并发垃圾收集器线程可能正在使用处理器资源,否则这些资源将可用于应用程序。因此,即使应用程序线程没有暂停,在这个和其他并发阶段,计算绑定的应用程序可能会看到应用程序吞吐量的相应下降。在再标记暂停之后,同时进行的扫描阶段会收集被标识为不可访问的对象。一旦收集周期完成,CMS收集器将等待,几乎不消耗任何计算资源,直到下一个主要收集周期开始。

启动并发收集周期

对于串行收集器,每当终身生成变满并且在收集完成时停止所有应用程序线程时,就会发生主要收集。相比之下,并发收集的开始必须定时,以便收集可以在终身生成变满之前完成;否则,由于并发模式故障,应用程序将观察到更长的暂停时间。有几种方法可以启动并发收集。

根据最近的历史记录,CMS收集器保持对终身发电耗尽前剩余时间的估计,以及对并发收集周期所需时间的估计。使用这些动态估计,开始一个并发的收集周期,目的是在终身生成耗尽之前完成收集周期。为了安全起见,对这些估计值进行了填充,因为并发模式故障的成本可能非常高。

如果终生代的占用率超过初始占用率(终生代的百分比),也会启动并发收集。该初始占用阈值的默认值约为92%,但该值可能会随发布而变化。可以使用命令行选项 -XX:CMSInitatingOccupancyFraction=<N> 手动调整此值,其中<N>是终身生成大小的整数百分比(0到100)。

计划暂停

年轻一代收集和终身一代收集的暂停是独立发生的。它们不重叠,但可能会快速连续发生,因此一个收集的暂停,紧接着另一个收集中的暂停,可能看起来是一个更长的暂停。为了避免这种情况,CMS收集器试图将再标记暂停安排在上一代和下一代年轻一代暂停的大致中间。这种调度目前没有针对初始标记暂停进行,初始标记暂停通常比再标记暂停短得多。

增量模式

请注意,增量模式在Java SE 8中已被弃用,并可能在未来的主要版本中被删除。

CMS收集器可以在以增量方式完成并发阶段的模式下使用。回想一下,在并发阶段,垃圾收集器线程正在使用一个或多个处理器。增量模式旨在通过周期性地停止并发阶段来减少长并发阶段的影响,从而将处理器交还给应用程序。这种模式在这里被称为 i-cms ,它将收集器同时完成的工作划分为小块时间,这些时间安排在年轻一代的收集之间。当需要CMS收集器提供的低暂停时间的应用程序在具有少量处理器(例如,1或2)的机器上运行时,此功能非常有用。

并发收集周期通常包括以下步骤:

  • 停止所有应用程序线程,识别可从根访问的对象集,然后恢复所有应用程序进程。
  • 在应用程序线程执行时,使用一个或多个处理器并行跟踪可访问对象图。
  • 使用一个处理器同时回溯自上一步跟踪以来修改的对象图部分。
  • 停止所有应用程序线程,并返回根和对象图中自上次检查以来可能已修改的部分,然后恢复所有应用程序线程。
  • 同时使用一个处理器将无法访问的对象清除到用于分配的空闲列表中。
  • 使用一个处理器同时调整堆的大小,并为下一个收集周期准备支持数据结构。

通常,CMS收集器在整个并发跟踪阶段使用一个或多个处理器,而不会自动放弃它们。类似地,一个处理器用于整个并发扫描阶段,同样不会放弃它。对于响应时间有限制的应用程序来说,这种开销可能会造成太大的中断,否则这些应用程序可能会使用处理核心,尤其是在只有一个或两个处理器的系统上运行时。增量模式通过将并发阶段分解为短的活动突发来解决这个问题,这些活动突发计划在小的暂停之间进行。

i-cms模式使用占空比来控制cms收集器在自愿放弃处理器之前可以做的工作量。duty cycle 是允许CMS收集器运行的新一代收集之间的时间百分比。i-cms模式可以根据应用程序的行为自动计算占空比(推荐的方法称为 automatic pacing ),也可以在命令行上将占空比设置为固定值。

命令行选项

表 8-1,“i-cms 的命令行选项” 列出命令 - 控制 i-cms 模式的行选项。 推荐选项 部分建议了一组初始选项。 表 8-1 i-cms 的命令行选项

Option Description Default Value, Java SE 5 and Earlier Default Value, Java SE 6 and Later
-XX:+CMSIncrementalMode 启用增量模式。 请注意,还必须启用 CMS 收集器(使用 -XX:+UseConcMarkSweepGC)才能使此选项起作用。 disabled disabled
-XX:+CMSIncrementalPacing 启用自动步调。 增量模式占空比根据 JVM 运行时收集的统计数据自动调整。 disabled disabled
-XX:CMSIncrementalDutyCycle=<N> 允许 CMS 收集器运行的次要收集之间的时间百分比(0 到 100)。 如果启用了 CMSIncrementalPacing,那么这只是初始值。 50 10
-XX:CMSIncrementalDutyCycleMin=<N> 启用 CMSIncrementalPacing 时占空比的下限百分比(0 到 100)。 10 0
-XX:CMSIncrementalSafetyFactor=<N> 计算占空比时用于添加保守性的百分比(0 到 100) 10 10
-XX:CMSIncrementalOffset=<N> 增量模式占空比在次要收集之间的时间段内向右移动的百分比(0 到 100)。 0 0
-XX:CMSExpAvgFactor=<N> 在计算 CMS 集合统计信息的指数平均值时用于对当前样本加权的百分比(0 到 100)。 25 25

推荐选项

要在 Java SE 8 中使用 i-cms,请使用以下命令行选项:

-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode \
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps

前两个选项分别启用 CMS 收集器和 i-cms。 最后两个选项不是必需的; 它们只是将有关垃圾收集的诊断信息写入标准输出,以便可以看到垃圾收集行为并在以后进行分析。

对于 Java SE 5 和更早的版本,Oracle 建议使用以下作为 i-cms 的一组初始命令行选项:

-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode \
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps \
-XX:+CMSIncrementalPacing -XX:CMSIncrementalDutyCycleMin=0
-XX:CMSIncrementalDutyCycle=10

尽管控制 i-cms 自动步调的三个选项的值在 JavaSE6 中成为默认值,但 JavaSE8 也推荐相同的值。

基本故障排除

i-cms 自动调步功能使用程序运行时收集的统计信息来计算占空比,以便并发收集在堆变满之前完成。 然而,过去的行为并不能完美地预测未来的行为,并且估计可能并不总是足够准确以防止堆变满。 如果出现太多完全收集,请尝试 [表 8-2,“排除 i-cms 自动定步功能”](https://docs.oracle.com/javase/8/docs/technotes/guides/ vm/gctuning/cms.html#troubleshooting_i_cms) ,一次一个。

表 8-2 i-cms 自动调步功能故障排除

Step Options
1. 提高安全系数。 -XX:CMSIncrementalSafetyFactor=<N>
2. 增加最小占空比。 -XX:CMSIncrementalDutyCycleMin=<N>
3. 禁用自动调步并使用固定占空比。 -XX:-CMSIncrementalPacing -XX:CMSIncrementalDutyCycle=<N>

测量

示例 8-1,“CMS 收集器的输出” 是来自 CMS 收集器,带有选项 -verbose:gc 和 -XX:+PrintGCDetails,删除了一些小细节。 请注意,CMS 收集器的输出散布在次要收集器的输出中; 通常在并发收集周期中会发生许多次要收集。 CMS-initial-mark表示并发收集周期开始,CMS-concurrent-mark表示并发标记阶段结束,CMS-concurrent-sweep表示并发清除阶段结束。 之前没有讨论的是由 CMS-concurrent-preclean 指示的预清洗阶段。 Precleaning 表示可以同时完成的工作,以准备备注阶段 CMS-remark。 最后阶段由 CMS-concurrent-reset 指示,并为下一次并发收集做准备。

示例 8-1 CMS 收集器的输出 ^48ea40

[GC [1 CMS-initial-mark: 13991K(20288K)] 14103K(22400K), 0.0023781 secs]
[GC [DefNew: 2112K->64K(2112K), 0.0837052 secs] 16103K->15476K(22400K), 0.0838519 secs]
...
[GC [DefNew: 2077K->63K(2112K), 0.0126205 secs] 17552K->15855K(22400K), 0.0127482 secs]
[CMS-concurrent-mark: 0.267/0.374 secs]
[GC [DefNew: 2111K->64K(2112K), 0.0190851 secs] 17903K->16154K(22400K), 0.0191903 secs]
[CMS-concurrent-preclean: 0.044/0.064 secs]
[GC [1 CMS-remark: 16090K(20288K)] 17242K(22400K), 0.0210460 secs]
[GC [DefNew: 2112K->63K(2112K), 0.0716116 secs] 18177K->17382K(22400K), 0.0718204 secs]
[GC [DefNew: 2111K->63K(2112K), 0.0830392 secs] 19363K->18757K(22400K), 0.0832943 secs]
...
[GC [DefNew: 2111K->0K(2112K), 0.0035190 secs] 17527K->15479K(22400K), 0.0036052 secs]
[CMS-concurrent-sweep: 0.291/0.662 secs]
[GC [DefNew: 2048K->0K(2112K), 0.0013347 secs] 17527K->15479K(27912K), 0.0014231 secs]
[CMS-concurrent-reset: 0.016/0.016 secs]
[GC [DefNew: 2048K->1K(2112K), 0.0013936 secs] 17527K->15479K(27912K), 0.0014814 secs]

初始标记暂停通常比次要收集暂停时间短。 并发阶段(并发标记、并发预清理和并发清除)通常比次要收集暂停持续的时间长得多,如 示例 8-1,“CMS 收集器的输出” 。 但是请注意,应用程序不会在这些并发阶段暂停。 备注停顿的长度通常与次要集合相当。 重新标记暂停受某些应用程序特性(例如,高对象修改率会增加此暂停)和自上次次要收集以来的时间(例如,年轻代中的更多对象可能会增加此暂停)的影响。

9 垃圾优先(G1)垃圾收集器

Garbage-First (G1) 垃圾收集器是一种服务器风格的垃圾收集器,针对具有大内存的多处理器机器。 它尝试以高概率满足垃圾收集 (GC) 暂停时间目标,同时实现高吞吐量。 全局标记等整堆操作与应用程序线程同时执行。 这可以防止与堆或实时数据大小成比例的中断。

G1 收集器通过多种技术实现高性能和暂停时间目标。

堆被划分为一组大小相同的堆区域,每个区域都是连续的虚拟内存范围。 G1 执行并发全局标记阶段以确定整个堆中对象的活跃度。 标记阶段完成后,G1 知道哪些区域大部分是空的。 它首先收集这些区域,这通常会产生大量的可用空间。 这就是为什么这种垃圾收集方法被称为垃圾优先的原因。 顾名思义,G1 将其收集和压缩活动集中在堆中可能充满可回收对象(即垃圾)的区域。 G1 使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数量。

G1 将对象从堆的一个或多个区域复制到堆上的单个区域,并在此过程中压缩和释放内存。 此疏散在多处理器上并行执行,以减少暂停时间并提高吞吐量。 因此,在每次垃圾回收中,G1 都会不断努力减少碎片。 这超出了之前两种方法的能力。 CMS(Concurrent Mark Sweep)垃圾收集不做压缩。 并行压缩仅执行整堆压缩,这会导致相当长的暂停时间。

重要的是要注意 G1 不是实时收集器。 它很可能满足设定的暂停时间目标,但不是绝对确定的。 根据之前收集的数据,G1 估计在目标时间内可以收集多少区域。 因此,收集器有一个合理准确的区域收集成本模型,它使用这个模型来确定在停顿时间目标内收集哪些区域和多少区域。

G1 的第一个重点是为运行需要大堆且 GC 延迟有限的应用程序的用户提供解决方案。 这意味着大约 6 GB 或更大的堆大小,以及低于 0.5 秒的稳定且可预测的暂停时间。

如果应用程序具有以下一个或多个特征,那么现在使用 CMS 或并行压缩运行的应用程序将受益于切换到 G1。

  • 超过 50% 的 Java 堆被实时数据占用。
  • 对象分配率或晋升率差异显着。
  • 应用程序正在经历不需要的长时间垃圾收集或压缩暂停(超过 0.5 到 1 秒)。

G1 计划作为 Concurrent Mark-Sweep Collector (CMS) 的长期替代品。 将 G1 与 CMS 进行比较揭示了使 G1 成为更好解决方案的差异。 一个区别是 G1 是一个压缩收集器。 此外,G1 提供比 CMS 收集器更可预测的垃圾收集暂停,并允许用户指定所需的暂停目标。

与 CMS 一样,G1 专为需要更短 GC 暂停的应用程序而设计。

G1 将堆划分为固定大小的区域(灰色框),如 图 9-1,“按 G1 划分堆”

图 9-1 堆按 G1 划分
image
“图9-1按G1划分堆”的说明 ^12df4e

该图由一个 10×10 的网格组成。 大多数网格的单元格都是灰色的。 十九个单元格为深蓝色。 这些深蓝色单元格随机分布在网格的上六行中。 其中两个深蓝色单元格包含一个红色框。 一个两格宽一格高的格子(出现在第一行)和一个三格宽一格高的格子(出现在第六行)被涂成深蓝色并标记为“H”。 八个单元格为浅蓝色并包含一个红色框。 其中两个细胞被标记为“S”。 这些带有红色框的浅蓝色单元格随机分布,大部分位于网格的上半部分。

G1在逻辑意义上是分代的。 一组空区域被指定为逻辑年轻代。 图中,年轻一代为淡蓝色。 分配是从那个合乎逻辑的年轻一代完成的,当年轻一代已满时,该组区域将被垃圾收集(年轻收集)。 在某些情况下,年轻区域集之外的区域(深蓝色的旧区域)可以同时被垃圾收集。 这称为 混合收集 。 在图中,正在收集的区域用红色框标记。 该图说明了一个混合收集,因为正在收集年轻区域和旧区域。 垃圾收集是一种压缩收集,它将活动对象复制到选定的、最初为空的区域。 根据幸存对象的年龄,可以将对象复制到幸存区域(用“S”标记)或旧区域(未具体显示)。 以“H”标记的区域包含大于半个区域的巨大物体,并被特殊处理; 请参阅 9 垃圾优先(G1)垃圾收集器

分配(疏散)失败

与 CMS 一样,G1 收集器在应用程序继续运行的同时运行其部分收集,并且存在应用程序分配对象的速度快于垃圾收集器恢复可用空间的风险。 请参阅 8 并发标记扫描(CMS)收集器 中的 并发模式故障 部分用于类似的 CMS 行为。 在 G1 中,故障(Java 堆耗尽)发生在 G1 将实时数据从一个区域复制(疏散)到另一个区域时。 复制是为了压缩实时数据。 如果在疏散被垃圾收集的区域期间找不到空闲(空)区域,则会发生分配失败(因为没有空间从被疏散的区域分配活动对象)和停止世界( STW) 全部收集完成。

浮动垃圾

对象可以在 G1 收集期间死亡并且不会被收集。 G1 使用一种称为开始快照 (SATB) 的技术来保证垃圾收集器找到所有活动对象。 SATB 声明在并发标记(整个堆上的标记)开始时处于活动状态的任何对象都被视为出于收集目的而处于活动状态。 SATB 以类似于 CMS 增量更新的方式允许浮动垃圾。

停顿

G1 暂停应用程序以将活动对象复制到新区域。 这些暂停可以是只收集年轻区域的年轻收集暂停,也可以是疏散年轻区域和老区域的混合收集暂停。 与 CMS 一样,在应用程序停止时有一个最终标记或重新标记暂停来完成标记。 CMS 也有初始标记暂停,而 G1 将初始标记工作作为疏散暂停的一部分。 G1 在收集结束时有一个清理阶段,部分是 STW 部分是并发的。 清理阶段的 STW 部分识别空区域并确定作为下一次收集候选的旧区域。

卡表和并发阶段

如果垃圾收集器不收集整个堆( 增量收集 ),则垃圾收集器需要知道从堆的未收集部分到正在收集的堆部分的指针在哪里。 这通常适用于分代垃圾收集器,其中堆中未收集的部分通常是老年代,堆中已收集的部分是新生代。 保存此信息的数据结构(老一代指针指向年轻一代对象)是一个 记忆集卡片表 是一种特殊类型的记忆集。 Java HotSpot VM 使用字节数组作为卡片表。 每个字节都称为一张 card 。 一张卡片对应堆中的一个地址范围。 弄脏卡片 意味着将字节的值更改为 脏值 ; 一个脏值可能包含一个新的指针,在卡覆盖的地址范围内从老年代指向年轻代。

Processing a card 意味着查看卡片以查看是否存在老年代指向年轻代的指针,并可能对该信息执行某些操作,例如将其传输到另一个数据结构。

G1 具有并发标记阶段,用于标记从应用程序中找到的活动对象。 并发标记从 evacuation pause 结束(初始标记工作完成的地方)延伸到 remark。 并发清理阶段将集合清空的区域添加到空闲区域列表中,并清除这些区域的记忆集。 此外,并发细化线程根据需要运行,以处理已被应用程序写入弄脏且可能具有跨区域引用的卡片表条目。

启动并发收集周期

如前所述,年轻区域和老区域都是在混合收集中进行垃圾收集的。 为了收集旧区域,G1 对堆中的活动对象进行了完整标记。 这种标记是由并发标记阶段完成的。 当整个 Java 堆的占用率达到参数 InitiatingHeapOccupancyPercent 的值时,并发标记阶段开始。 使用命令行选项 -XX:InitiatingHeapOccupancyPercent=<NN> 设置此参数的值。 InitiatingHeapOccupancyPercent 的默认值为 45。

暂停时间目标

使用标志 MaxGCPauseMillis 为 G1 设置暂停时间目标。 G1 使用预测模型来决定在该目标暂停时间内可以完成多少垃圾收集工作。 在一次收集结束时,G1 选择下一次收集(收集集)中要收集的区域。 收集集将包含年轻区域(其大小的总和决定了逻辑年轻代的大小)。 G1 部分地通过选择收集集中年轻区域的数量来控制 GC 暂停的长度。 与其他垃圾收集器一样,您可以在命令行上指定新生代的大小,但这样做可能会妨碍 G1 达到目标暂停时间的能力。 除了暂停时间目标之外,您还可以指定可以发生暂停的时间段长度。 您可以指定此时间跨度 (GCPauseIntervalMillis) 的最小增变器使用量以及暂停时间目标。 MaxGCPauseMillis 的默认值为 200 毫秒。 GCPauseIntervalMillis 的默认值 (0) 相当于对时间跨度没有要求。

10 垃圾优先垃圾收集器调优

本节介绍如何调整和调整垃圾优先垃圾收集器 (G1 GC) 以进行评估、分析和性能。

垃圾优先垃圾收集器 部分所述,G1 GC 是区域化的 和分代垃圾收集器,这意味着Java对象堆(heap)被分成许多大小相等的区域。 启动时,Java 虚拟机 (JVM) 会设置区域大小。 区域大小可以从 1 MB 到 32 MB 不等,具体取决于堆大小。 目标是不超过 2048 个区域。 eden、survivor 和 old generations 是这些区域的逻辑集合,并不连续。

G1 GC 有一个它试图达到的暂停时间目标(软实时)。 在新生代回收期间,G1 GC 调整其新生代(伊甸园和幸存者大小)以满足软实时目标。 请参阅 暂停 和 [暂停时间目标](https://docs. oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html#pause_time_goal) 在 [垃圾优先垃圾收集器](https://docs.oracle.com/javase/8/docs/technotes /guides/vm/gctuning/g1_gc.html#garbage_first_garbage_collection) 有关 G1 GC 暂停的原因以及如何设置暂停时间目标的信息。

在混合收集期间,G1 GC 根据混合垃圾收集的目标数量、堆中每个区域中存活对象的百分比以及总体可接受的堆浪费百分比来调整收集的旧区域的数量。 G1 GC 通过将活动对象从一组或多组区域(称为收集集 (CSet))增量并行复制到一个或多个不同的新区域以实现压缩,从而减少堆碎片。 目标是回收尽可能多的堆空间,从包含最多可回收空间的区域开始,同时尝试不超过暂停时间目标(垃圾优先)。

G1 GC 使用独立的记忆集 (RSets) 来跟踪对区域的引用。 独立 RSet 支持并行和独立的区域集合,因为只有区域的 RSet 必须扫描以获取对该区域的引用,而不是整个堆。 G1 GC 使用写后屏障来记录对堆的更改并更新 RSets。

垃圾收集阶段

除了构成停止世界 (STW) 年轻垃圾回收和混合垃圾回收的疏散暂停(请参阅 垃圾优先垃圾收集器 中的 分配(疏散)失败 ) 之外,G1 GC 还具有并行、并发和多阶段标记周期。 G1 GC 使用开始时快照 (SATB) 算法,该算法在逻辑上会在标记周期开始时对堆中的活动对象集进行快照。 活动对象集还包括自标记周期开始以来分配的对象。 G1 GC 标记算法使用预写屏障来记录和标记属于逻辑快照一部分的对象。

年轻的垃圾收集

G1 GC 满足来自添加到 eden 区域集的区域的大多数分配请求。 在新生代垃圾回收期间,G1 GC 会收集上一次垃圾回收中的伊甸园区域和幸存者区域。 来自伊甸园和幸存者区域的活动对象被复制或疏散到一组新区域。 特定对象的目标区域取决于对象的年龄; 已经充分老化的对象疏散到老年代区域(即,它被提升); 否则,该对象将撤离到幸存者区域,并将包含在下一次年轻或混合垃圾收集的 CSet 中。

混合垃圾收集

成功完成并发标记周期后,G1 GC 从执行年轻垃圾回收切换到执行混合垃圾回收。 在混合垃圾回收中,G1 GC 可选择将一些旧区域添加到将要收集的伊甸园和幸存者区域集合中。 添加的旧区域的确切数量由许多标志控制(请参阅 建议 部分中的“驯服混合垃圾收集器” )。 在 G1 GC 收集到足够数量的旧区域(通过多次混合垃圾回收)后,G1 恢复执行年轻垃圾回收,直到下一个标记周期完成。

标记周期的阶段

标记周期有以下阶段:

  • 初始标记阶段:G1 GC 在此阶段标记根。 此阶段搭载在正常 (STW) 年轻垃圾收集上。
  • Root region scanning phase:G1 GC 扫描在初始标记阶段标记的 survivor region 以查找对老年代的引用,并标记被引用的对象。 此阶段与应用程序(不是 STW)同时运行,并且必须在下一个 STW 年轻垃圾收集开始之前完成。
  • 并发标记阶段:G1 GC 在整个堆中查找可达(活动)对象。 这个阶段与应用程序同时发生,并且可以被 STW 新生代垃圾回收中断。
  • Remark phase:这个阶段是STW收集,帮助完成marking cycle。 G1 GC 排空 SATB 缓冲区,跟踪未访问的活动对象,并执行引用处理。
  • 清理阶段:在这个最后阶段,G1 GC 执行会计和 RSet 清理的 STW 操作。 在核算期间,G1 GC 识别完全空闲区域和混合垃圾收集候选区域。 当清理阶段重置并将空区域返回到空闲列表时,清理阶段是部分并发的。

重要默认值

G1 GC 是一种自适应垃圾收集器,其默认设置使其无需修改即可高效工作。 表 10-1,“G1 垃圾收集器重要选项的默认值” 列出 Java HotSpot VM build 24 中的重要选项及其默认值。您可以通过在 JVM 命令行上更改设置,输入表 10-1,“G1 垃圾收集器重要选项的默认值” 中的选项来调整和调整 G1 GC 以满足您的应用程序性能需求。

表 10-1 G1 垃圾收集器重要选项的默认值

Option and Default Value Option
-XX:G1HeapRegionSize=n 设置 G1 区域的大小。 该值将是 2 的幂,范围从 1 MB 到 32 MB。 目标是根据最小 Java 堆大小拥有大约 2048 个区域。
-XX:MaxGCPauseMillis=200 为所需的最大暂停时间设置目标值。 默认值为 200 毫秒。 指定的值不适合您的堆大小。
-XX:G1NewSizePercent=5 设置堆的百分比以用作年轻代大小的最小值。 默认值为 Java 堆的 5%。Foot1
这是一个实验性标志。 有关示例,请参阅如何解锁实验性 VM 标志。 此设置取代了 -XX:DefaultMinNewGenPercent 设置。
-XX:G1MaxNewSizePercent=60 设置堆大小的百分比以用作年轻代大小的最大值。 默认值为 Java 堆的 60%。Footref1
这是一个实验性标志。 有关示例,请参阅如何解锁实验性 VM 标志。 此设置取代了 -XX:DefaultMaxNewGenPercent 设置。
-XX:ParallelGCThreads=n 设置 STW 工作线程的值。 将 n 的值设置为逻辑处理器的数量。 n 的值与逻辑处理器的数量相同,最大为 8。
如果有超过 8 个逻辑处理器,请将 n 的值设置为大约 5/8 的逻辑处理器。 这在大多数情况下都有效,但较大的 SPARC 系统除外,其中 n 的值可能约为逻辑处理器的 5/16。
-XX:ConcGCThreads=n 设置并行标记线程数。 将 n 设置为并行垃圾收集线程数 (ParallelGCThreads) 的大约 1/4。
-XX:InitiatingHeapOccupancyPercent=45 设置触发标记周期的 Java 堆占用阈值。 默认占用率为整个 Java 堆的 45%。
-XX:G1MixedGCLiveThresholdPercent=85 设置要包含在混合垃圾回收周期中的旧区域的占用阈值。 默认占用率为 85%。 Footref1
这是一个实验性标志。 有关示例,请参阅如何解锁实验性 VM 标志。 此设置取代了 -XX:G1OldCSetRegionLiveThresholdPercent 设置。
-XX:G1HeapWastePercent=5 设置您愿意浪费的堆的百分比。 当可回收百分比小于堆垃圾百分比时,Java HotSpot VM 不会启动混合垃圾收集周期。 默认值为 5%。Footref1
-XX:G1MixedGCCountTarget=8 设置标记周期后混合垃圾收集的目标数量,以收集具有最多 G1MixedGCLIveThresholdPercent 实时数据的旧区域。 默认值为 8 次混合垃圾回收。 混合收藏的目标是在这个目标数字内。Footref1
-XX:G1OldCSetRegionThresholdPercent=10 设置混合垃圾收集周期中要收集的旧区域数量的上限。 默认值为 Java 堆的 10%。 Footref1
-XX:G1ReservePercent=10 设置保留内存的百分比以保持空闲,以降低空间溢出的风险。 默认值为 10%。 当您增加或减少百分比时,请确保将总 Java 堆调整相同的数量。 Footref1

[^footnote1] 此设置在 Java HotSpot VM build 23 或更早版本中不可用。

如何解锁实验性

VM 标志 要更改实验标志的值,您必须先解锁它们。 您可以通过在任何实验性标志之前在命令行上显式设置 -XX:+UnlockExperimentalVMOptions 来做到这一点。 例如:

java -XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=10 -XX:G1MaxNewSizePercent=75 G1test.jar

建议

当您评估和微调 G1 GC 时,请牢记以下建议:

  • 新生代大小:避免使用 -Xmn 选项或任何或其他相关选项(例如 -XX:NewRatio)明确设置新生代大小。 固定年轻一代的大小会覆盖目标暂停时间目标。
  • 暂停时间目标:当您评估或调整任何垃圾收集时,总会有延迟与吞吐量的权衡。 G1 GC 是一种增量垃圾收集器,具有统一的暂停,但在应用程序线程上的开销也更大。 G1 GC 的吞吐量目标是 90% 的应用程序时间和 10% 的垃圾收集时间。 将其与 Java HotSpot VM 并行收集器进行比较。 并行收集器的吞吐量目标是 99% 的应用程序时间和 1% 的垃圾收集时间。 因此,当您评估 G1 GC 的吞吐量时,放宽暂停时间目标。 设定过于激进的目标表明您愿意承担垃圾收集开销的增加,这对吞吐量有直接影响。 当您评估 G1 GC 的延迟时,您设置了您想要的(软)实时目标,G1 GC 将尝试满足它。 作为副作用,吞吐量可能会受到影响。 请参阅 9 垃圾优先(G1)垃圾收集器 了解更多信息。
  • 驯服混合垃圾收集:在调整混合垃圾收集时试验以下选项。 有关这些选项的信息,请参阅 重要默认值 部分:
    • -XX:InitiatingHeapOccupancyPercent:用于更改标记阈值。
    • -XX:G1MixedGCLiveThresholdPercent-XX:G1HeapWastePercent:用于更改混合垃圾收集决策。
    • -XX:G1MixedGCCountTarget-XX:G1OldCSetRegionThresholdPercent:用于调整旧区域的 CSet。

溢出和耗尽的日志消息

当您在日志中看到 to-space overflow 或 to-space exhausted 消息时,G1 GC 没有足够的内存用于 survivor 或 promoted 对象,或两者。 Java 堆不能,因为它已经达到最大值。 示例消息:

  • 924.897: [GC pause (G1 Evacuation Pause) (mixed) (to-space exhausted), 0.1957310 secs]
  • 924.897: [GC pause (G1 Evacuation Pause) (mixed) (to-space overflow), 0.1957310 secs]

要缓解此问题,请尝试进行以下调整:

  • 增加-XX:G1ReservePercent 选项的值(以及相应的总堆)以增加“to-space”的预留内存量。
  • 通过减少“-XX:InitiatingHeapOccupancyPercent”的值来更早地开始标记周期。
  • 增加-XX:ConcGCThreads 选项的值以增加并行标记线程的数量。

有关这些选项的说明,请参阅 重要默认值 部分。

巨大的对象和巨大的分配

对于 G1 GC,任何超过区域大小一半的对象都被视为 巨大对象 。 这样的对象直接在老年代分配到 humongous regions 。 这些巨大的区域是一组连续的区域。 StartsHumongous 标记连续集合的开始,ContinuesHumongous 标记集合的延续。

在分配任何巨大的区域之前,检查标记阈值,并在必要时启动并发循环。

在清理阶段以及完整的垃圾收集周期中,在标记周期结束时释放死的巨大对象。

为了减少复制开销,巨大的对象不包括在任何疏散暂停中。 一个完整的垃圾收集周期将巨大的对象压缩到位。

因为每一组单独的 StartsHumongousContinuesHumongous 区域只包含一个巨大的对象,所以巨大对象的末端和该对象跨越的最后一个区域的末端之间的空间未被使用。 对于略大于堆区域大小倍数的对象,未使用的空间会导致堆变得碎片化。

如果您看到由于大量分配而启动的背靠背并发循环,并且如果此类分配使您的老一代碎片化,则增加 -XX:G1HeapRegionSize 的值,以便以前的大量对象不再是巨大的,并且将遵循常规 分配路径。

11 其他注意事项

本节介绍影响垃圾收集的其他情况。

终结和弱引用、软引用和虚引用

一些应用程序通过使用最终化和弱引用、软引用或虚引用与垃圾回收进行交互。

但是,不鼓励使用终结。 它可能导致安全性、性能和可靠性方面的问题。 例如,依靠终结来关闭文件描述符会使外部资源(描述符)依赖于垃圾收集的及时性。

终结

一个类可以声明一个终结器——方法 protected void finalize() ——它的主体释放任何底层资源。 GC会调度一个不可达对象的finalizer,在GC回收对象内存之前调用。

当没有从 GC 根到对象的路径时,对象变得不可访问,因此有资格进行垃圾收集。 (有关可达性的更多信息,请参阅 引用对象类型 。) GC 根包括来自活动线程的引用和内部 JVM 引用; 它们是将对象保存在内存中的引用。

请参阅 Java Platform, Standard Edition Troubleshooting Guide 中的 监控待终结的对象 以确定是否可终结对象 正在你的系统中建立。 此外,您可以使用以下工具之一:

  • JDK 任务控制:
    1. JVM 浏览器 中,右键单击您的 JVM 并选择 启动 JMX 控制台
    2. MBean 浏览器MBean 树 中,展开 java.lang 并选择 内存
    3. MBean Features 中,属性 ObjectPendingFinalizationCount 是待完成的对象的大概数量。
  • jcmd
    • 运行以下命令以打印有关 Java 终结队列的信息; 值 _<pid>_ 是您的 JVM 的 PID:
      jcmd _<pid>_ GC.finalizer_info

从终结迁移

为避免终结,请使用 try-with-resources 语句。 这是声明一个或多个资源的 try 语句。 资源是一个对象,在程序使用完它后必须关闭。 try-with-resources 语句可确保在代码块末尾关闭每个资源,即使出现一个或多个异常也是如此。 有关更多信息,请参阅 Try-with-resources 声明

引用对象类型

共有三种引用对象类型:SoftReferenceWeakReferencePhantomReference。 每个引用对象类型对应于不同级别的可达性。 以下是可达性的不同级别,从最强到最弱,它们反映了对象的生命周期:

  • 如果某个线程可以在不遍历任何引用对象的情况下访问某个对象,则该对象是 strongly reachable 。 新创建的对象可以被创建它的线程强访问。
  • 如果对象不是强可达但可以通过遍历软引用到达,则该对象是 软可达
  • 如果对象既不是强可达也不是软可达但可以通过遍历弱引用到达,则该对象是 弱可达 。 当对弱可达对象的弱引用被清除时,该对象就有资格进行终结。
  • 一个对象是 幻影可达 如果它既不是强可达的,也不是弱可达的,它已经被终结,并且有一些幻影引用引用它。
  • 一个对象是 unreachable,因此当它无法通过任何先前的方式到达时,它有资格被回收。

每个引用对象类型都封装了对特定对象的单个引用,称为 referent。 引用对象提供了清除引用对象的方法。 以下是引用对象实例最常见的用途:

  • 保持对对象的访问,同时如果系统需要释放内存(例如可以在需要时重新生成的缓存值),则仍允许对其进行垃圾收集
  • 确定并可能在对象达到特定可达性级别时采取一些行动(结合ReferenceQueue类)

显式垃圾收集

应用程序可以与垃圾收集交互的另一种方式是通过调用 System.gc() 显式调用完整垃圾收集。 这可能会在不必要时强制执行主要收集(例如,当次要收集就足够时),因此通常应该避免。 显式垃圾收集的性能影响可以通过使用标志 -XX:+DisableExplicitGC 禁用它们来衡量,这会导致 VM 忽略对 System.gc() 的调用。

显式垃圾回收最常见的用途之一是远程方法调用 (RMI) 的分布式垃圾回收 (DGC)。 使用 RMI 的应用程序引用其他虚拟机中的对象。 如果不偶尔调用本地堆的垃圾收集,就无法在这些分布式应用程序中收集垃圾,因此 RMI 会定期强制执行完全收集。 可以使用属性控制这些集合的频率,如下例所示:

java -Dsun.rmi.dgc.client.gcInterval=3600000
    -Dsun.rmi.dgc.server.gcInterval=3600000 ...

此示例指定每小时一次显式垃圾收集,而不是每分钟一次的默认速率。 但是,这也可能导致某些对象需要更长的时间才能被回收。 如果不需要 DGC 活动的及时性上限,这些属性可以设置为 Long.MAX_VALUE,以使显式收集之间的时间有效无限。

软引用

与客户端相比,软引用在服务器虚拟机中的存活时间更长。 可以使用命令行选项 -XX:SoftRefLRUPolicyMSPerMB=<N> 控制清除率,它指定软引用将保持活动状态的毫秒数 (ms)(一旦它不再是强可达的 ) 对于堆中每兆字节的可用空间。 默认值为每兆字节 1000 毫秒,这意味着对于堆中每兆字节的可用空间,软引用将存活(在收集到对象的最后一个强引用之后)1 秒。 这是一个近似值,因为软引用仅在垃圾收集期间被清除,这可能偶尔会发生。

类元数据

Java 类在 Java Hotspot VM 中有一个内部表示,被称为类元数据。 在以前的 Java Hotspot VM 版本中,类元数据在所谓的永久代中分配。 在 JDK 8 中,永久代被移除,类元数据分配在本机内存中。 默认情况下,可用于类元数据的本机内存量是无限的。 使用选项 MaxMetaspaceSize 对用于类元数据的本机内存量设置上限。

Java Hotspot VM 显式管理用于元数据的空间。 空间从操作系统请求,然后分成块。 类加载器从其块中为元数据分配空间(块绑定到特定的类加载器)。 当为类加载器卸载类时,它的块将被回收以供重用或返回给操作系统。 元数据使用由 mmap 而非 malloc 分配的空间。

如果启用了 UseCompressedOops 并使用了 UseCompressedClassesPointers,那么本机内存的两个逻辑上不同的区域将用于类元数据。 UseCompressedClassPointers 使用 32 位偏移来表示 64 位进程中的类指针,就像用于 Java 对象引用的 UseCompressedOops 一样。 为这些压缩类指针(32 位偏移量)分配一个区域。 区域的大小可以使用 CompressedClassSpaceSize 设置,默认为 1 GB。 压缩类指针的空间保留为由 mmap 在初始化时分配的空间,并根据需要提交。 MaxMetaspaceSize 适用于已提交的压缩类空间与其他类元数据空间的总和。

当相应的 Java 类被卸载时,类元数据被释放。 Java 类作为垃圾收集的结果被卸载,并且可能引发垃圾收集以卸载类和释放类元数据。 当为类元数据提交的空间达到一定水平(高水位线)时,将引发垃圾收集。 垃圾收集后,高水位线可能会根据从类元数据中释放的空间量而升高或降低。 将提高高水位线,以免过早引发另一次垃圾收集。 高水位线最初设置为命令行选项“MetaspaceSize”的值。 它根据选项 MaxMetaspaceFreeRatioMinMetaspaceFreeRatio 升高或降低。 如果可用于类元数据的已提交空间占类元数据的总已提交空间的百分比大于“MaxMetaspaceFreeRatio”,则高水位线将降低。 如果它小于 MinMetaspaceFreeRatio,那么将提高高水位线。

为选项 MetaspaceSize 指定一个更高的值,以避免为类元数据引入早期垃圾回收。 为应用程序分配的类元数据量取决于应用程序,并且不存在选择“MetaspaceSize”的一般准则。 MetaspaceSize 的默认大小取决于平台,范围从 12 MB 到大约 20 MB。

有关用于元数据的空间的信息包含在堆的打印输出中。 示例 11-1,“典型堆打印输出” 中显示了典型的输出。

示例 11-1 典型的堆打印输出

Heap
  PSYoungGen      total 10752K, used 4419K
    [0xffffffff6ac00000, 0xffffffff6b800000, 0xffffffff6b800000)
    eden space 9216K, 47% used
      [0xffffffff6ac00000,0xffffffff6b050d68,0xffffffff6b500000)
    from space 1536K, 0% used![image](uploading...)
      [0xffffffff6b680000,0xffffffff6b680000,0xffffffff6b800000)
    to   space 1536K, 0% used
      [0xffffffff6b500000,0xffffffff6b500000,0xffffffff6b680000)
  ParOldGen       total 20480K, used 20011K
      [0xffffffff69800000, 0xffffffff6ac00000, 0xffffffff6ac00000)
    object space 20480K, 97% used 
      [0xffffffff69800000,0xffffffff6ab8add8,0xffffffff6ac00000)
  Metaspace       used 2425K, capacity 4498K, committed 4864K, reserved 1056768K
    class space   used 262K, capacity 386K, committed 512K, reserved 1048576K

在以“Metaspace”开头的行中,“used”值是用于加载类的空间量。 capacity 值是当前分配的块中可用于元数据的空间。 committed 值是可用于块的空间量。 reserved 值是为元数据保留(但不一定提交)的空间量。 以 class space 行开头的行包含压缩类指针的元数据的相应值。

posted @ 2023-05-14 02:53  紫零大仙  阅读(141)  评论(0编辑  收藏  举报