现在不懂点虚拟机都不好意思说是Java程序员了,这方面的文章不少,但质量参差不齐。在百度谷歌上看了一圈大部分是你抄我我抄你,要不就是泛泛而谈,看完之后还是一头雾水,看到Oracle官网上有一篇将虚拟机调优的文章Garbage Collection Tuning不错,有理论有实操,试着翻译下。

1 垃圾收集优化介绍

从桌面上的小应用程序到大型服务器上的网络服务,各种各样的应用都在使用Java SE平台。为了支持多样化的部署,Java HotSpot虚拟机提供了多种垃圾收集器,每个都是为了满足不同的需要而设计的。Java SE根据运行应用程序的计算机的类别选择最合适的垃圾收集器。然而,这种选择并不是对每个应用都是最佳的。具有严格性能或其他要求的用户、开发人员和管理员可能需要显式选择合适的垃圾收集器,并调整某些参数以达到所需的性能水平。本文档提供了帮助完成这些任务的信息。

首先,垃圾收集器的一般特性和基本的调优选项将在串行stop-the-world收集器一节描述。然后介绍其他收集器的具体特征以及选择收集器时要考虑的因素。

本节主题:

  • 什么是垃圾收集器?
  • 垃圾收集器的选择为什么重要?
  • 文档中支持的操作系统

什么是垃圾收集器?

垃圾收集器用来自动管理应用程序的动态内存分配请求。

垃圾收集器通过以下操作执行自动动态内存管理:

  • 从操作系统分配内存和将内存返还给操作系统。
  • 根据应用程序的请求,将内存分配给它。
  • 确定哪些内存还在使用。
  • 回收未使用的内存供应用程序重用。

Java HotSpot垃圾收集器采用各种技术来提高这些操作的效率:

  • 将年代清理与老化结合使用,将精力集中在堆中最有可能包含大量可回收内存的区域。
  • 使用多线程并行操作,或者在应用程序的后台并行执行一些长期运行的操作。
  • 通过压缩存活对象,尝试恢复更大的连续可用内存。

垃圾收集器的选择为什么重要?

垃圾收集器的目的是将应用程序开发人员从手动动态内存管理中解放出来。开发人员无需关心内存的分配与回收,也不用关注分配的动态内存的生存期。这完全消除了一些与内存管理相关的错误,代价是增加了一些运行时开销。Java HotSpot虚拟机提供了一系列垃圾收集算法可供选择。

垃圾收集器的选择什么时候重要?对于某些应用,答案是永远不会。也就是说,一些应用程序的垃圾收集运行良好,暂停的频率和持续时间适中。然而,对于一大类应用程序,尤其是那些具有大量数据(几十亿字节)、许多线程和高事务率的应用程序,情况并非如此。

Amdahl定律(给定问题中的并行加速受问题串行部分的限制)意味着大多数工作负载不能完全并行化;有些部分总是串行的,并没有从并行中获益。在Java平台中,目前有四种支持的垃圾收集替换方案,除了其中一种串行垃圾收集器serial GC,其他的都能并行化以提高性能。尽可能降低垃圾收集的开销是非常重要的。这可以在下面的例子中看到。

图1-1中的图表模拟了一个理想的系统,除了垃圾收集之外,它是完全可伸缩的。红线表示在单处理器系统上只花费1%时间进行垃圾收集的应用程序。这意味着在拥有32个处理器的系统上,吞吐量损失超过20%。洋红色线显示,对于垃圾收集时间为10%的应用程序(在单处理器应用程序中,垃圾收集时间不算太长),当扩展到32个处理器时,会损失75%以上的吞吐量。

图1-1 垃圾收集所用时间的百分比对比

该图显示,在小型系统上开发时,可以忽略的吞吐量问题可能会成为扩展到大型系统时的主要瓶颈。然而,在减少这种瓶颈方面的微小改进可以带来巨大的性能提升。对于一个足够大的系统,选择合适的垃圾收集器并在必要时对其进行调整变得很有价值。

串行垃圾收集器对于大多数小型应用已经足够了,尤其是那些堆空间约100兆字节的应用。其他收集器有额外的开销或复杂性,这是高级特性的代价。如果应用程序不需要其他收集器的高级特性,使用串行垃圾收集器就可以了。串行垃圾收集器不是最佳选择的一种情况是运行在具有大量内存和两个或更多处理器的机器上的大型多线程应用程序。当应用程序在这样的服务器级计算机上运行时,默认情况下会选择垃圾优先(G1)收集器;参见工效学

文档中支持的操作系统

本文档及其建议适用于JDK 12支持的所有系统配置,受某些垃圾收集器特定配置实际可用性的限制。请参阅甲骨文JDK认证系统配置

2 工效学

工效学是指Java虚拟机(JVM)使用启发式垃圾收集方法以(如基于行为的试探法)提高应用程序性能的过程。

JVM为垃圾收集器、堆大小和运行时编译器提供了依赖于平台的默认选择。这些选择兼顾不同类型应用程序的需求,同时只需要较少的命令行调整。此外,基于行为的动态堆大小优化,以满足应用程序的特定行为。

本节描述这些默认选择和基于行为的调整。在使用后续章节中描述的更详细控制之前,使用这些默认值。

本节主题:

  • 垃圾收集器、堆和运行时编译器默认选择
  • 基于行为的优化
    • 最大暂停时间
    • 吞吐量
    • 占用量
  • 调优策略

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

下面是重要的垃圾收集器、堆大小和运行时编译器默认选择:

  • 垃圾优先(G1)收集器
  • 垃圾收集线程的最大数量受堆大小和可用CPU资源的限制
  • 堆的初始容量为1/64物理内存
  • 堆的最大容量为1/4物理内存
  • 分层编译器,使用C1和C2

基于行为的优化

Java HotSpot虚拟机垃圾收集器可以配置为优先满足两个目标之一:最大暂停时间和吞吐量。如果达到了首选目标,收集者将尝试最大化另一个目标。当然,这些目标并不总是能够实现的:应用程序需要至少能保存所有存活数据的最小堆,而其他配置可能会阻止实现一些或所有的期望目标。

最大暂停时间

暂停时间是指垃圾收集器停止应用程序并回收非使用空间的持续时间。最大暂停时间目的是限制这些暂停的最长时间。

垃圾收集器维护着暂停时间的平均值和方差。平均值是从应用开始执行时取的,但是它是加权的,所以最近的暂停权重更大。如果暂停时间的平均值加上方差大于最大暂停时间,则垃圾收集器认为目标没有实现。

最大暂停时间是用命令行选项-XX:MaxGCPauseMillis=<nnn>指定的。这被解释为向垃圾收集器提示需要< nnn >毫秒或更少的暂停时间。垃圾收集器会调整Java堆大小和其他与垃圾收集相关的参数,以使垃圾收集暂停时间短于< nnn >毫秒。最大暂停时间的默认值因垃圾收集器而异。这些调整可能会导致垃圾收集的更加频繁,从而降低应用程序的整体吞吐量。然而,在某些情况下,期望的暂停时间目标可能无法实现。

吞吐量

吞吐量目标是以收集垃圾花费的时间来衡量的,垃圾收集之外的时间就是应用程序时间。

吞吐量通过命令选项-XX:GCTimeRatio=nnn指定。垃圾收集时间与应用时间之比为1/ (1+nnn)。例如,-XX:GCTimeRatio=19设置了垃圾收集时间占总时间的1/20或5%。

垃圾收集花费的时间是所有垃圾收集暂停的总时间。如果没有达到吞吐量目标,那么垃圾收集器的一个可能的操作是增加堆的大小,以便在垃圾收集之间的应用程序时间可以更长。

占用量

如果吞吐量和最大暂停时间目标已经达到,那么垃圾收集器会减小堆的大小,直到其中一个目标(总是吞吐量目标)无法达到为止。垃圾收集器可以分别使用-Xms=<nnn >-Xmx=<mmm >设置为最小和最大堆大小。

调优策略

堆增长或收缩到以支持所选吞吐量。了解堆优化策略,如选择最大堆大小和选择最大暂停时间目标。

除非您确定需要大于堆大小默认值的堆,否则不要为堆选择最大值。为您的应用选择一个足够的吞吐量即可。

应用程序行为的改变会导致堆增长或收缩。例如,如果应用程序开始以更高的速率分配内存,那么堆就会增长以保持相同的吞吐量。

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

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

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

3 垃圾收集器实现

Java SE平台的一个优势是它保护开发人员免受内存分配和垃圾收集复杂性的困扰。

然而,当垃圾收集成为主要瓶颈时,理解实现的某些方面是有用的。垃圾收集器对应用程序使用对象的方式进行假设,这些假设反映在可调参数中,这些参数可以在不牺牲抽象能力的情况下进行调整以提高性能。

本节主题:

  • 分代垃圾收集
  • 分代
  • 性能考虑
  • 吞吐量和占用量考量

分代垃圾收集

当运行程序中的任何其他存活对象的任何引用不能再访问一个对象时,它就是一个垃圾,虚拟机可以重用它的内存。

理论上,最简单的垃圾收集算法每次运行时都会遍历每个可到达的对象。其余的对象都被认为是垃圾。这种方法花费的时间与活动对象的数量成正比,这对维护大量存活数据的大型应用程序来说是禁止的。

Java HotSpot虚拟机包含许多不同的垃圾收集算法,这些算法都使用一种称为分代收集的技术。虽然简单的垃圾收集每次都会检查堆中的每个存活对象,但是分代收集利用了在大多数应用程序中观察到的几个经验特性来最小化回收未使用(垃圾)对象所需的工作。这些观察到的特性中最重要的是弱分代假说(weak generational hypothesis),它指出大多数对象只能存活很短的一段时间。

图3-1中的蓝色区域是对象寿命的典型分布。x轴显示的是对象生命周期。y轴显示的是存活对象的字节数。左边的尖峰代表分配后不久可以回收的对象(换句话说,已经“死亡”)。例如,迭代器对象通常只在单个循环期间有效。

图3-1对象生命周期的典型分布

有些对象确实寿命更长,所以分布向右侧延伸。例如,通常有一些在初始化时分配的对象会一直存在,直到虚拟机退出。在这两个极端之间是在运算期间的中间值,在这里被视为初始峰值右侧的块。一些应用程序具有非常不同的分布,但令人惊讶的是,大量应用程序具有这种一般性的形状。通过关注大多数对象“朝生夕死”的事实,有效的收集成为可能。

分代

为了针对这种情况进行优化,内存分几代进行管理(内存池保存不同年龄的对象)。垃圾收集发生在每一代填满时。

绝大多数对象被分配到一个专门用于年轻对象(新生代,young generation)的池中,大多数对象死在那里。当新生代填满时,它会导致一个小规模垃圾收集(minor collection),它只收集新生代;其他分代的垃圾不会被回收。这种收集的成本首先与被收集的存活对象的数量成比例;充满死亡对象的新生代收集的很快。

通常,在每一次小规模的收集过程中,新生代幸存下来的一些对象会被转移到老年代(old generation)。最后,老年代也会填满并且必须被收集,从而产生一个大规模垃圾收集(major collection),它将收集整个堆。大规模垃圾收集的持续时间通常比小规模垃圾收集长得多,因为涉及的对象数量要大得多。图3-2显示了串行垃圾收集器(serial garbage collector)中的默认分代排列:

图3-2串行垃圾收集器(serial garbage collector)中的默认分代排列

启动时,Java HotSpot虚拟机将整个Java堆保留在地址空间中,除非需要,否则不会为其分配任何物理内存。Java堆的整个地址空间在逻辑上分为新生代和老年代。为对象内存保留的完整地址空间也分为新生代和老年代。

新生代由eden和两个survivor空间组成。大多数对象最初分配在eden中。在任何时候都有一个survivor空间是空的,并且在垃圾收集期间充当eden和另一个survivor空间中存活对象的目标空间;垃圾收集后,eden和源survivor空间是空的。在下一次垃圾收集中,两个survivor空间的角色交换。最近填充的survivor空间将会把存活对象复制到另一个survivor空间。对象以这种方式在两个survivor空间之间来回复制,直到它们被复制了一定次数或者没有足够的空间了。这些对象被复制到老年代。这个过程也被称为老化(aging)

性能考虑

垃圾收集的主要指标是吞吐量和时延。

  • 吞吐量是指未花费在垃圾收集上的总时间的百分比(即应用程序所占用的时间,译者注)。吞吐量包括分配内存所花费的时间(但通常不需要调整分配速度)。
  • 时延是应用程序的响应能力。垃圾收集会暂停应用程序进而会影响应用程序的响应能力。

用户对垃圾收集有不同的要求。例如,有些人认为网络服务器的正确度量是吞吐量,因为垃圾收集期间的暂停可能是可以容忍的,或者会因为网络延迟而变得模糊不清。然而,在交互式图形程序中,即使短暂的暂停也会对用户体验产生负面影响。

一些用户对其他考虑很敏感。占用空间是一个进程的工作集,以页面和缓存行来衡量。在物理内存有限或进程众多的系统上,占用空间可能决定可伸缩性。*及时性(Promptness)*是指对象死亡和内存可用之间的时间,对于包括远程方法调用(Remote Method Invocation, RMI)在内的分布式系统来说,这是一个重要的考虑因素。

一般来说,为特定一代选择大小就是在这些因素之间作权衡。例如,非常大的新生代可能最大化吞吐量,但这样做是以空间占用、及时性和暂停时间为代价的。相反,可以通过减小新生代的空间来换取小的垃圾收集暂停时间,但这会牺牲吞吐量。一代的空间不会影响另一代的收集频率和暂停时间。

没有一种通用的方法可以选择一代的最优空间大小。最佳选择取决于应用程序使用内存的方式以及用户需求。因此,虚拟机对垃圾收集器的选择并不总是最佳的,可以通过命令行选项修改;请参见影响垃圾收集性能的因素

吞吐量和占用量考量

吞吐量和占用量最好使用特定于应用的指标来衡量。

例如,可以使用客户端负载生成器测试网络服务器的吞吐量,可以使用pmap命令在Solaris操作系统上测量服务器的占用空间。通过检查虚拟机本身的诊断输出,可以轻松估计垃圾收集导致的暂停时间。

命令行选项-verbose:gc在每次垃圾收集中打印关于堆和垃圾收集的信息。下面是一个例子:

[15,651s][info ][gc] GC(36) Pause Young (G1 Evacuation Pause) 239M->57M(307M) (15,646s, 15,651s) 5,048ms
[16,162s][info ][gc] GC(37) Pause Young (G1 Evacuation Pause) 238M->57M(307M) (16,146s, 16,162s) 16,565ms
[16,367s][info ][gc] GC(38) Pause Full (System.gc()) 69M->31M(104M) (16,202s, 16,367s) 164,581ms

输出显示了两次新生代的垃圾收集,之后是应用程序调用System.gc()启动的完整垃圾收集(Full GC)。这些行以时间戳开始,该时间戳表示应用程序启动的时间。接下来是关于该行的日志级别(信息)和标签(gc)的信息。随后是垃圾收集识别号。在这种情况下,有三个编号为36、37和38的垃圾收集。然后记录的是垃圾收集类型和原因。之后,会记录一些关于内存消耗的信息。该日志使用“在垃圾收集之前使用堆大小”->“在垃圾收集之后使用堆大小”的格式。

在示例的第一行中,是239兆->57M(307兆),这意味着在垃圾收集清除大部分内存之前使用了239兆字节,但是收集之后保留了57兆字节。堆大小为307兆字节。请注意,在此示例中,完整的垃圾收集将堆从307兆字节缩减到104兆字节。在内存使用信息之后,将记录垃圾收集的开始和结束时间以及持续时间(结束-开始)。

-verbose:gc命令是-Xlog:gc的别名。-Xlog是用于HotSpot JVM的通用日志配置选项。这是一个基于标签的系统,其中gc是标签之一。要获得更多关于垃圾收集正在做什么的信息,您可以配置日志来打印任何带有垃圾收集标签和任何其他标签的消息。该命令的命令行选项是-Xlog:gc*

下面是一个用-Xlog:gc*配置的G1收集器新生代垃圾收集的例子:

[10.178s][info][gc,start ] GC(36) Pause Young (G1 Evacuation Pause)
[10.178s][info][gc,task ] GC(36) Using 28 workers of 28 for evacuation
[10.191s][info][gc,phases ] GC(36) Pre Evacuate Collection Set: 0.0ms
[10.191s][info][gc,phases ] GC(36) Evacuate Collection Set: 6.9ms
[10.191s][info][gc,phases ] GC(36) Post Evacuate Collection Set: 5.9ms
[10.191s][info][gc,phases ] GC(36) Other: 0.2ms
[10.191s][info][gc,heap ] GC(36) Eden regions: 286->0(276)
[10.191s][info][gc,heap ] GC(36) Survivor regions: 15->26(38)
[10.191s][info][gc,heap ] GC(36) Old regions: 88->88
[10.191s][info][gc,heap ] GC(36) Humongous regions: 3->1
[10.191s][info][gc,metaspace ] GC(36) Metaspace: 8152K->8152K(1056768K)
[10.191s][info][gc ] GC(36) Pause Young (G1 Evacuation Pause) 391M->114M(508M) 13.075ms
[10.191s][info][gc,cpu ] GC(36) User=0.20s Sys=0.00s Real=0.01s

注意:由-Xlog:gc*生成的输出格式可能会在未来版本中发生变化。

4 影响垃圾收集性能的因素

影响垃圾收集性能的两个最重要的因素是总的可用内存和新生代的比例。

本节内容:

  • 总的可用内存
    • 影响分代大小的选项
    • 堆的默认大小
    • 通过最小化Java堆大小来节省动态占用空间
  • 新生代
    • 新生代大小选项
    • survivor空间大小

总的可用内存

影响垃圾收集性能的最重要因素是总可用内存。因为收集发生在分代空间占满时,吞吐量与可用内存量成反比。

注意:以下关于堆的增长和收缩、堆布局和默认值的讨论以串行垃圾收集器为例。虽然其他收集器使用类似的机制,但这里提供的细节可能不适用于其他收集器。有关其他收集器的类似信息,请参考各自的主题。

影响分代大小的选项

许多选项会影响分代大小。图4-1展示了堆中提交空间和虚拟空间之间的区别。虚拟机初始化时,堆的整个空间都会被保留。保留空间的大小可以用-Xmx选项指定。如果-Xms参数的值小于-Xmx参数的值,则不是所有保留的空间都会立即提交给虚拟机。在此图中,未提交的空间标记为“虚拟”。堆的不同部分,即新生代和老年代,可以根据需要增加到虚拟空间的极限。

一些参数可以设置堆的一部分与另一部分的比率。例如,参数–XX:NewRatio表示老年代与新生代的相对大小。

堆空间

堆的默认大小

默认情况下,虚拟机会在每次垃圾收集时增大或缩小堆,以尝试将每次收集中存活对象的可用空间比例维持在特定范围内。

该范围为选项-XX:MinHeapFreeRatio= <最小值>-XX:MaxHeapFreeRatio= <最大值>设置的百分比,堆的总大小以–Xms <最小值>为界,以–Xmx <最大值>为界。64位Solaris操作系统(SPARC平台版)的默认选项如表4-1所示。

选项 默认值
-XX:MinHeapFreeRatio 40
-XX:MaxHeapFreeRatio 70
-Xms 6656 KB
-Xmx 计算得到

有了这些选项,如果某一代的可用空间低于40%,则会扩展堆空间以保持40%的可用空间,一直到这一代的最大允许大小。类似地,如果空闲空间超过70%,那么这一代空间将收缩,使得只有70%的空间是空闲的,这取决于这一代空间的最小尺寸。

如表4-1所示,默认的最大堆大小是由JVM计算的值。Java SE中用于并行收集器(Parallel collector)的计算现在被用于所有垃圾收集器。计算的一部分是64位平台的最大堆大小上限。请参见大小并行垃圾收集器默认堆。客户端模式的JVM也有类似的计算,只不过最大堆大小小于服务器模式的JVM。

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

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

通过最小化Java堆大小来节省动态占用空间

如果您需要最小化应用程序的动态内存占用(执行期间消耗的最大内存),那么您可以通过最小化Java堆大小来实现这一点。Java SE嵌入式应用程序可能需要这一点。

通过降低命令行选项-XX:MinHeapFreeRatio(默认值为40%)和-XX:MaxHeapFreeRatio(默认值为70%)的值,从而最小化Java堆大小。将-XX:MaxHeapFreeRatio降低到低至10%,-XX:MinHeapFreeRatio已证明能够成功地减小堆大小,而且不会造成太大的性能降低;但是,最终的结果可能取决于你的应用。

此外,您可以指定-XX:-ShrinkHeapInSteps,它会立即将Java堆减小到目标大小(由参数-XX:MaxHeapFreeRatio指定)。此设置可能会导致性能下降。默认情况下,Java运行时会逐渐将Java堆减小到目标大小;这个过程需要多个垃圾收集周期。

新生代

在总可用内存之后,影响垃圾收集性能的第二个最大影响因素是专用于新生代的比例。

新生代越大,小规模收集就越少。然而,对于给定的堆大小,较大的新生代意味着较小的老年代,这将增加大规模收集的频率。最佳选择取决于应用程序对象的生命周期分布。

新生代大小选项

默认情况下,新生代的大小由选项-XX:NewRatio控制。

例如,设置-XX:NewRatio=3意味着新生代和老年代之间的比率是1:3。换句话说,eden和survivor空间总的大小将是总堆大小的四分之一。

选项-XX:NewSize-XX:MaxNewSize指定了新生代的大小下界和上界。将它们设置为相同的值新生代就是固定值,正如将-Xms-Xmx设置为相同的值可以固定总的堆大小一样。这有助于以比-XX:NewRatio更精细的粒度调整新生代。

survivor空间大小

您可以使用选项-XX:SurvivorRatio来调整survivor空间的大小,但这通常对性能并不重要。

例如,-XX:SurvivorRatio=6将eden和survivor空间之间的比率设置为1:6。换句话说,每个survivor空间将是eden大小的六分之一,也就是新生代大小的八分之一(不是七分之一,因为有两个survivor空间)。

如果survivor空间太小,那么将会直接复制到老年代。如果幸存者空间太大,那么将会有很多空间永远不会使用。在每次垃圾收集时,虚拟机都会选择一个阈值,即一个对象在其转移到老年代之前可以复制的次数。选择这个阈值是为了让survivor保持半饱和状态。您可以使用日志配置-Xlog:gcage可以用来显示这个阈值和新生代对象的年龄。它对于观察应用程序的生命周期分布也很有用。

表4-2提供了64位Solaris的默认值。

选项 默认值
-XX:NewRatio 2
-XX:NewSize 1310 MB
-XX:MaxNewSize not limited
-XX:SurvivorRatio 8

新生代的最大大小是根据总堆的最大大小和-XX:NewRatio参数的值计算的。-XX:MaxNewSize参数的“无限制”意味着计算不限制,除非命令行上指定了-XX:MaxNewSize的值。

以下是服务器应用程序的一般指南:

  • 首先决定您可以为虚拟机提供的最大堆大小。然后,根据新生代的规模制定您的性能指标,以找到最佳设置。
    • 请注意,最大堆大小应始终小于机器上安装的内存量,以避免过多的页面错误和抖动。
  • 如果总堆大小是固定的,那么增加新生代的大小需要减少老年代的大小。保持老一代足够大,以容纳应用程序在任何给定时间使用的所有存活数据,加上一定量的富余空间(10%到20%或更多)。
  • 根据前面提到的对老年代的限制:
    • 给予新生代足够的内存。
    • 增加处理器数量的同时增加新生代的规模,因为分配可以并行化。

5 可用收集器

本节主题:

  • 串行收集器(Serial Collector)
  • 并行收集器(Parallel Collector)
  • 主要并发收集器(The Mostly Concurrent Collectors)
  • 选择收集器

串行收集器(Serial Collector)

串行收集器使用单个线程来执行所有垃圾收集工作,这使得它相对高效,因为线程之间没有通信开销。

它最适合单处理器机器,尽管它对于具有小数据集(大约100兆字节)的应用程序在多处理器上很有用,但它不能利用多处理器硬件的优势。默认情况下,串行收集器是在某些硬件和操作系统配置上的默认选择,或者通过选项-XX:+UseSerialGC显式启用。

并行收集器(Parallel Collector)

并行收集器也称为吞吐量收集器,它是与串行收集器相似的一代收集器。串行收集器和并行收集器之间的主要区别是并行收集器有多个线程用于加快垃圾收集。

并行收集器适用于运行在多处理器或多线程硬件上具有大中型数据集的应用程序。您可以通过使用-XX:+UseParallelGC选项来启用它。

并行压缩是一项使并行收集器能够并行执行大规模垃圾收集的功能。如果没有并行压缩,大规模垃圾收集是使用单个线程来执行的,这可能会极大地限制可伸缩性。如果指定了选项-XX:+UseParallelGC,则默认情况下启用并行压缩。您可以通过使用-XX:-UseParallelOldGC选项来禁用它。

主要并发收集器(The Mostly Concurrent Collectors)

并发标记清除(CMS)收集器和垃圾优先(G1)垃圾收集器是两个主要并发收集器。大多数并发收集器并发执行一些代价较高应用程序工作。

  • G1垃圾收集器:这种服务器风格的收集器是为具有大内存、多处理器机器设计的。为了满足垃圾收集暂停时间目标的同时实现高吞吐量。 在某些硬件和操作系统配置上默认选择G1,或者可以使用-XX:+UseG1GC显式启用。
  • CMS收集器:这个收集器是为那些更喜欢较短垃圾收集暂停时间的应用程序设计的,并且能够与垃圾收集共享处理器资源。使用-XX:+UseConcMarkSweepGC启用CMS收集器。

CMS收集器在JDK 9中被标记为弃用。

Z收集器

Z垃圾收集器(ZGC)是一个可扩展的低延迟垃圾收集器。ZGC同时执行所有代价高昂的工作,同时不停止应用程序线程的执行。

ZGC适用于需要低时延(暂停时间不到10 ms)和/或特大堆(几T字节)的应用。您可以通过使用-XX:+UseZGC选项来启用。

从JDK 11开始,ZGC作为一个实验的特性的出现。

选择收集器

除非您的应用程序有相当严格的暂停时间要求,否则首先运行您的应用程序,并允许虚拟机选择收集器。

如有必要,调整堆大小以提高性能。如果性能仍然达不到您的目标,请使用以下准则作为选择收集器的出发点:

  • 如果应用程序有一个小数据集(高达大约100兆字节),则使用选项-XX:+UseSerialGC选择串行收集器。
  • 如果应用程序将在单个处理器上运行,并且没有暂停时间要求,则选择带有选项-XX:+UseSerialGC的串行采集器。
  • 如果(a)应用程序性能峰值是第一优先事项,并且(b)没有暂停时间要求,或者一秒钟或更长的暂停时间是可接受的,则让虚拟机选择收集器或使用-XX:+UseParallelGC选择并行收集器。
  • 如果响应时间比总吞吐量更重要,并且垃圾收集暂停时间必须保持在大约一秒钟以内,则选择一个具有-XX:+UseG1GC-XX:+UseConcMarkSweepGC的主要并发收集器。
  • 如果响应时间是一个高优先级,和/或您正在使用一个非常大的堆,那么选择一个具有-XX:UseZGC的完全并发收集器。

这些准则只是选择收集器的出发点,因为性能取决于堆的大小、应用程序维护的存活数据量以及可用处理器的数量和速度。

如果推荐的收集器没有达到期望的性能,那么首先尝试调整堆和各个分代的大小以满足期望的目标。如果性能仍然不足,请尝试不同的收集器:使用并发收集器减少暂停时间,使用并行收集器增加多处理器硬件上的总吞吐量。

6 并行收集器(The Parallel Collector)

并行收集器(这里也称为吞吐量收集器)是一个类似于串行收集器的分代收集器。串行收集器和并行收集器之间的主要区别是并行收集器有多个线程用于加快垃圾收集。

并行收集器通过命令行选项-XX:+UseParallelGC启用。默认情况下,使用此选项,大规模收集和小规模收集并行运行,以进一步减少垃圾收集开销。

本节主题:

  • 并行收集器垃圾收集器线程的数量
  • 并行收集器中的分代排列
  • 并行收集器工效学
    • 指定并行收集器行为选项
    • 并行收集器指标的优先级
    • 并行收集器各个分代空间调整
    • 并行收集器默认堆大小
      • 并行收集器初始和最大堆大小的规范
  • 过多的并行收集器时间和内存不足错误
  • 并行收集器度量

并行收集器垃圾收集器线程的数量

在硬件线程数N大于8(感觉这里应该说的是CPU的个数,译者加)的机器上,并行收集器使用硬件线程数的固定比例作为垃圾收集器线程数。

对于较大的N值,比例为5/8。当小于8时,线程等于N。在特定的平台上,这一比例降至5/16。垃圾收集器线程的具体数量可以通过命令行选项进行调整(将在后面描述)。在只有一个处理器的主机上,由于并行执行(例如同步)所需的开销,并行收集器的性能可能不如串行收集器。但是,当具有中型到大型堆的应用程序时,运行在具有两个处理器的计算机上,它通常比串行收集器性能略好,并且当有两个以上的处理器可用时,它通常比串行收集器性能好得多。

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

并行收集器中的分代排列

在并行收集器中,分代的排列是不同的。

这种布置如图6-1所示:

并行收集器中的分代排列

并行收集器人机工程学

当使用-XX:+UseParallelGC选择并行收集器时,它启用了一种自动优化方法,允许您指定行为,而不是分代大小和其他低级优化细节。

指定并行收集器行为的选项

您可以指定最大垃圾收集暂停时间、吞吐量和占用空间(堆大小)。

  • 最大垃圾收集暂停时间:最大暂停时间是用命令行选项-XX:MaxGCPauseMillis=<N>指定的。这被解释为需要N毫秒或更少的暂停时间;默认情况下,没有最大暂停时间。如果指定了暂停时间,将调整堆大小和其他与垃圾收集相关的参数,以使垃圾收集暂停时间短于指定值;然而,期望的暂停时间目标可能并不总能实现。这些调整可能会导致垃圾收集器降低应用程序的总吞吐量。
  • 吞吐量:吞吐量目标是根据垃圾收集花费的时间与垃圾收集之外花费的时间(称为应用程序时间)来衡量的。目标由命令行选项-XX:GCTimeRatio=<N>指定,该选项将垃圾收集时间与应用程序时间的比率设置为1 / (1 + N)(感觉应该是垃圾收集与总时间的比率,译者注)。例如,-XX:GCTimeRatio=19设置了垃圾收集总时间的1/20或5%的目标。默认值为99,因此垃圾收集的时间目标为1%。
  • 占用空间:最大堆占用空间是使用选项-Xmx指定的。此外,收集器有一个隐含的目标,只要满足其他目标,就要最小化堆的大小。

并行收集器指标的优先级

目标按最大暂停时间指标、吞吐量指标和最小占用空间指标顺序排列:首先要满足最大暂停时间指标,只有当最大暂停时间指标达到后才会去实现吞吐量的指标,同样只有前两个指标满足后才会考虑占用空间的指标。

并行收集器各个分代空间调整

收集器保存的平均暂停时间等统计信息会在每次收集结束时更新。

进行测试以确定目标是否已经实现,并对一代的空间进行任何必要的调整。例外情况是显式垃圾收集,例如,在保存统计信息和调整代的大小方面,将会忽略System.gc()调用的影响。

各代大小的增加和缩小是通过各代大小的固定百分比的增量来完成的,以便各个分代朝着其期望的大小递增或递减。默认情况下,一代以20%的增量增长,以5%的增量收缩。新生代和老年代的增长比例分别可以通过-XX:YoungGenerationSizeIncrement=<Y>-XX:TenuredGenerationSizeIncrement=<T>指定。收缩比例通过-XX:AdaptiveSizeDecrementScaleFactor=<D>进行调整,如果增长比例是X%,那么收缩比例为X/D%。

如果收集器在启动时增加一代的大小,那么增量中会添加一个额外的百分比。这个百分比将会随着垃圾收集次数的增加而衰减,并不会长期存在。补充的目的是提高启动性能。收缩的百分比没有补充。

如果没有达到最大暂停时间目标,那么一次只能缩小一代的规模。如果两代的暂停时间都超过了目标,那么暂停时间较长的一代的规模将首先缩小。

如果吞吐量目标没有实现,那么两代的规模都会增加。每一个都按其对总垃圾收集时间的贡献比例增加。例如,如果新生代的垃圾收集时间是总收集时间的25%,如果新生代的完整增量是20%,那么新生代将增加5%。

并行收集器默认堆大小

除非命令行中指定了初始堆大小和最大堆大小,否则它们是根据计算机上的内存量计算的。默认的最大堆大小是物理内存的四分之一,而初始堆大小是物理内存的六分之一。分配给新生代的最大空间量是总堆大小的三分之一。

并行收集器初始和最大堆大小的规范

您可以使用选项-Xms(初始堆大小)和-Xmx(最大堆大小)指定初始和最大堆大小。

如果您知道您的应用程序需要多少堆才能正常工作,那么您可以将-Xms-Xmx设置为相同的值。如果您不知道,那么JVM将从使用初始堆大小开始,然后增加Java堆,直到找到堆使用和性能之间的平衡。

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

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

过多的并行收集器时间和内存不足错误

如果在垃圾收集中花费了太多时间,并行收集器将抛出OutOfMemoryError。

如果总时间的98%以上花在垃圾收集上,并且回收的堆少于2%,则抛出OutOfMemoryError。此功能旨在防止应用程序长时间运行,同时由于堆太小而几乎没有进展。如有必要,可以通过向命令行添加选项-XX:-UseGCOverheadLimit来禁用此功能。

并行收集器度量

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

7 主要并发收集器

主要并发收集器对应用程序并发执行收集工作,因此得名。Java HotSpot虚拟机包括两个主要并发的收集器:

  • 并发标记清除(CMS)收集器:该收集器适用于那些更喜欢较短垃圾收集暂停时间并且能够与垃圾收集共享处理器资源的应用程序。
  • 垃圾优先(G1)垃圾收集器:这种服务器风格的收集器适用于具有大量内存的多处理器机器。它旨在满足垃圾收集暂停时间目标,同时实现高吞吐量。

主要并发收集器的开销

主要并发收集器会占用处理器资源(原本应用程序可以使用这些资源),以缩短主要收集暂停时间。

最明显的开销是在收集的并发部分使用一个或多个处理器。在N处理器系统中,垃圾收集的并发部分可用处理器个数为K/N,其中1 <= K <={N/4}。除了在并发阶段使用处理器之外,启用并发还会产生额外的开销。因此,虽然并发收集器的垃圾收集暂停时间通常要短得多,但应用程序吞吐量也往往比其他收集器略低。

在具有多个处理核心的机器上,处理器在收集的并发部分也可用于应用程序线程,因此并发垃圾收集器线程不会暂停应用程序。这通常会导致更短的暂停时间,但是应用程序可用的处理器资源也更少,而且会有一些减速,尤其是在应用程序最大限度地使用所有处理核心的情况下。随着N的增加,由于并发垃圾收集导致的处理器资源减少变得更小,并发收集的好处也增加了。请参阅并发模式故障,其中讨论了这种模式的潜在限制。

因为在并发阶段至少有一个处理器用于垃圾收集,所以并发收集器通常在单处理器(单核)机器上不会提供任何好处。

8 并发标记清除收集器

并发标记清除(CMS)收集器是为那些喜欢较短垃圾收集暂停时间的应用程序设计的,并且能够在应用程序运行时与垃圾收集器共享处理器资源。

典型地,具有相对较大的长寿命数据集(大的老年代)并且运行在具有两个或更多处理器的机器上的应用程序倾向于受益于该收集器的使用。CMS收集器使用-XX:+UseConcMarkSweepGC启用。

不推荐使用CMS收集器。强烈考虑改用垃圾优先收集器。

本节主题:

  • 并发标记清除收集器的性能和结构
  • 并发模式故障
  • 过多的垃圾收集时间过长和内存不足错误
  • 并发标记清除收集器和浮动垃圾
  • 并发标记清除收集器暂停
  • 并发标记清除收集器并发阶段
  • 启动并发收集周期
  • 计划暂停
  • 并发标记清除收集器度量

并发标记清除收集器的性能和结构

与其他可用的收集器相似,CMS收集器是分代的;因此,小规模收集和大规模收集都会发生。CMS收集器试图通过使用单独的垃圾收集线程,在执行应用程序线程的同时,跟踪可到达的对象来减少大规模收集的暂停时间。

在每次大规模收集中,CMS收集器会在收集开始时暂停所有应用程序线程一段时间,在收集期间再次暂停。第二次暂停往往比第一次长。多个线程在两次暂停期间执行收集工作。一个或多个垃圾收集线程完成剩余的收集工作(包括大部分对存活对象的跟踪和对不可达对象的清除)。小规模收集可以与正在进行的大规模收集交叉进行,并且以类似于并行收集的方式完成(特别是小规模收集暂停期间)。

并发模式故障

CMS收集器使用一个或多个垃圾收集器线程,这些线程与应用程序线程同时运行,目的是在老年代变满之前完成老年代的收集。

如前所述,在正常操作中,CMS收集器在应用程序线程仍在运行的情况下执行大部分跟踪和扫描工作,因此应用程序线程只能看到短暂的暂停。但是,如果CMS收集器无法在老年代填满之前回收不可访问的对象,或者如果内存分配不能满足老年代中的可用空闲空间,则应用程序会暂停,并且收集会在所有应用程序线程停止的情况下完成。无法并发完成收集被称为并发模式故障(concurrent mode failure),意味着需要调整CMS收集器参数。如果并发收集被显式垃圾收集(System.gc())或为诊断工具的垃圾收集中断,则报告一个并发模式中断。

过多的垃圾收集时间过长和内存不足错误

如果在垃圾收集中花费了太多时间,如果总时间的98%以上花费在垃圾收集中,并且恢复的堆少于2%,则抛出OutOfMemoryError。

此功能旨在防止应用程序长时间运行,同时由于堆太小而几乎没有进展。如有必要,可以通过向命令行添加选项-XX:-UseGCOverheadLimit来禁用此功能。

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

并发标记清除收集器和浮动垃圾

同Java HotSpot虚拟机中的所有其他收集器一样,CMS收集器是一个跟踪收集器,它需要标识堆中所有可到达的对象。

理查德·琼斯和拉斐尔·林在他们的出版物《垃圾收集:自动动态内存算法》中说,这是一个增量更新收集器。因为应用程序线程和垃圾收集器线程在主要收集过程中同时运行,垃圾收集器线程跟踪的对象可能随后在收集过程结束时变得不可访问。这种尚未被回收的不可达对象被称为浮动垃圾。浮动垃圾的数量取决于并发收集周期的持续时间和应用程序引用更新的频率,也称为突变(mutations)。此外,因为新生代和老年代是独立收集的,彼此互为可达性分析的根(root)。作为一个粗略的指导方针,试着将老年代的空间增加20%,以解决漂浮垃圾的问题。一个并发收集周期结束时堆中的浮动垃圾将在下一个收集周期中收集。

并发标记清除收集器暂停

CMS收集器在并发收集周期内暂停应用程序两次。第一个暂停是将从根(例如,来自应用程序线程堆栈和寄存器的对象引用、静态对象等)和堆中其他地方(例如,新生代)直接可达对象标记为存活对象。

第一次暂停称为初始标记暂停(initial mark pause)。第二次暂停发生在并发跟踪阶段的末尾,并在CMS收集器完成对对象的跟踪后,查找由于应用程序线程更新对象中的引用而被并发跟踪遗漏的对象。第二次暂停称为备注暂停(remark pause)

并发标记清除收集器并发阶段

可达对象图的并发跟踪发生在初始标记暂停和备注暂停之间。

在这个并发跟踪阶段,一个或多个并发垃圾收集器线程可能在使用处理器,原本这些资源对于应用程序是可用的。因此,即使应用程序线程没有暂停,在此阶段和其他并发阶段,应用程序的吞吐量也会相应降低。备注暂停后,并发清理阶段收集不可达的对象。收集周期完成后,CMS收集器等待,几乎不消耗计算资源,直到下一个主要收集周期开始。

启动并发收集周期

对于串行收集器,每当老年代变满,所有应用程序线程都停止,开始一次大规模收集。相比之下,CMS收集器中并发收集的开始时间必须确保收集能够在老年代变满之前完成;否则,由于并发模式故障,应用程序会观察到较长的暂停时间。有几种方法可以开始并发收集。

根据最近的历史记录,CMS收集器会对老年代耗尽之前剩余的时间以及并发收集周期所需的时间进行估计。使用这些动态估计,开始并发收集周期,目的是在老年代耗尽之前完成收集周期。为了安全起见,对这些估计留有余量,因为并发模式故障的代价可能非常高。

如果老年代的占用率超过初始占用率(老年代的百分比),并发收集也会开始。启动并发周期的默认值约为92%,但该值会随版本的不同而变化。该值可以使用命令行选项-XX:CMSInitiatingOccupancyFraction=<N>手动调整,其中N是老年代大小的整数百分比(0到100)。

计划暂停

新生代和老年代的暂停是独立发生的。

它们不会重叠,但可能会快速连续发生,一次收集的暂停,紧接着另一次收集的暂停,看起来可能是一个更长的暂停。为了避免这种情况,CMS收集器试图将备注暂停安排在上一次和下一次新生代代暂停的中间。当前没有为初始标记暂停进行这种调度,初始标记暂停通常比备注暂停短得多。

并发标记清除收集器度量

以下是带有选项-Xlog:gc的CMS收集器的输出:

[121,834s][info][gc] GC(657) Pause Initial Mark 191M->191M(485M) (121,831s, 121,834s) 3,433ms
[121,835s][info][gc] GC(657) Concurrent Mark (121,835s)
[121,889s][info][gc] GC(657) Concurrent Mark (121,835s, 121,889s) 54,330ms
[121,889s][info][gc] GC(657) Concurrent Preclean (121,889s)
[121,892s][info][gc] GC(657) Concurrent Preclean (121,889s, 121,892s) 2,781ms
[121,892s][info][gc] GC(657) Concurrent Abortable Preclean (121,892s)
[121,949s][info][gc] GC(658) Pause Young (Allocation Failure) 324M->199M(485M) (121,929s, 121,949s) 19,705ms
[122,068s][info][gc] GC(659) Pause Young (Allocation Failure) 333M->200M(485M) (122,043s, 122,068s) 24,892ms
[122,075s][info][gc] GC(657) Concurrent Abortable Preclean (121,892s, 122,075s) 182,989ms
[122,087s][info][gc] GC(657) Pause Remark 209M->209M(485M) (122,076s, 122,087s) 11,373ms
[122,087s][info][gc] GC(657) Concurrent Sweep (122,087s)
[122,193s][info][gc] GC(660) Pause Young (Allocation Failure) 301M->165M(485M) (122,181s, 122,193s) 12,151ms
[122,254s][info][gc] GC(657) Concurrent Sweep (122,087s, 122,254s) 166,758ms
[122,254s][info][gc] GC(657) Concurrent Reset (122,254s)
[122,255s][info][gc] GC(657) Concurrent Reset (122,254s, 122,255s) 0,952ms
[122,297s][info][gc] GC(661) Pause Young (Allocation Failure) 259M->128M(485M) (122,291s, 122,297s) 5,797ms

注意:CMS收集器的输出(GC标识657)与小规模收集的输出(GC标识658、659和660)穿插在一起;通常,会有多次小规模收集发生在并发收集周期中。初始标记暂停(Pause Initial Mark)表示并发收集周期的开始。以Concurrent为开头的行表示并发阶段的开始和结束。Pause Remark是备注暂停。前面没有讨论预清洗阶段。预清洗是指在准备备注暂停阶段可以同时完成的工作。最后阶段由并发重置指示(Concurrent Reset),并为下一个并发收集做准备。

初始标记暂停通常相对于小规模收集暂停时间较短。并发阶段(并发标记、并发预清洗和并发清除)通常持续的时间比小规模收集暂停时间长得多,如CMS收集器输出示例所示。但是,请注意,在这些并发阶段,应用程序不会暂停。备注暂停的长度通常相当于一次小规模收集。备注暂停受某些应用程序特征(例如,对象修改率提高会增加暂停)和自上次小规模收集以来的时间(例如,新生代中的更多对象可能会增加暂停)的影响。

9 垃圾优先收集器

本节介绍垃圾优先(G1)收集器。

本节主题:

  • 垃圾优先收集器简介
  • 启用垃圾优先收集器
  • 基本概念
    • 堆布局
    • 垃圾收集周期
  • 深入垃圾优先收集器内部
    • 确定初始堆占用率
    • 标记
    • 堆资源紧张下的行为
    • 大对象
    • 纯新生代收集阶段规模
    • 空间回收阶段收集规模
  • G1的工程学默认值
  • 与其他收集器的对比

垃圾优先收集器简介

垃圾优先(G1)收集器针对具有大量内存的多处理器机器。它试图高概率地达到垃圾收集暂停时间目标,同时无需配置就能实现高吞吐量。G1旨在利用应用程序和环境在延迟和吞吐量之间实现最佳平衡,这些应用程序和环境的功能包括:

  • 堆大小高达几十GB或更大,超过50%的Java堆被存活数据占据。
  • 随着时间的推移,对象分配和升级的速率可能会有很大变化。
  • 堆中有大量碎片。
  • 可预测的暂停时间,不超过几百毫秒,避免长时间的垃圾收集暂停。

G1取代了并发标记清除(CMS)收集器。它也是默认的收集器。

G1采集器实现了高性能,并试图通过以下几节中描述的几种方式来实现暂停时间目标。

启用垃圾优先收集器

垃圾优先垃圾收集器是默认收集器,因此通常您不必执行任何额外的操作。您可以通过在命令行上提供-XX:+UseG1GC来显式启用它。

基本概念

G1是一个分代的、渐进的、并发的、疏散垃圾收集器,它监控每次stop-the-world暂停中的暂停时间目标。与其他收集器一样,G1将这堆分成(虚拟的)新生代和老年代。空间回收工作集中在最有效的新生代,而老年代偶尔才会进行空间回收。

为了提高吞吐量,有些操作总是在stop-the-world时执行。其他操作(如全局标记等整体堆操作)可能需要更多时间将与应用程序并行执行。为了让空间回收时stop-the-world的时间更短,G1逐步并行地进行空间回收。G1通过跟踪关于先前应用程序行为和垃圾收集暂停的信息来建立相关成本的模型,从而实现可预测性。它使用这些信息来调整暂停中完成的工作。例如,G1首先回收最高效区域的空间(即大部分被垃圾填满的区域,因此得名)。

G1主要通过疏散来回收空间:将选定存储区域中的存活对象复制到新的存储区域中,在这个过程中对它们进行压缩。疏散完成后,先前由存活对象占据的空间将重新分配给应用程序。

垃圾优先收集器不是实时收集器。它试图在更长的时间内高概率地达到设定的暂停时间目标,但对于给定的暂停并不总是能达到的。

堆布局

G1将堆分成一组大小相等的堆区域(region),每个区域都是一个连续的虚拟内存范围,如图9-1所示。区域是内存分配和内存回收的单位。在任何给定的时间,这些区域中的每一个都可以是空的(浅灰色),或者分配给特定的一代,新生的或老年的。当内存请求进来时,内存管理器会分配空闲区域。内存管理器将它们分配给一代,然后将它们作为空闲空间返给应用程序,应用程序可以将它们分配给自己。

G1堆布局

新生代包含eden区域(红色)和survivor区域(红色带“S”)。这些区域提供了与其他收集器中连续空间相同的功能,不同之处在于,在G1,这些区域通常在内存中以不连续的模式排列。(浅蓝色)组成了老年代。对于跨越多个区域的对象,老年代区域可能非常大(浅蓝色,带“H”)。

一个应用程序总是先分配给新生代,也就是eden区域,除了那些被直接分配给老年代的巨大对象。

垃圾收集周期

在高层次上,G1收集器在两个阶段之间交替。纯年轻阶段(young-only)包含垃圾收集,这些垃圾收集逐渐用老年代中的对象填充当前可用的内存。空间回收阶段(space-reclamation),G1除了处理新生代的事务外,还逐步回收老年代的空间。然后,循环从一个纯新生代的阶段重新开始。

图9-2给出了这个循环的概述,并举例说明了可能发生的垃圾收集暂停序列:

垃圾收集周期

下面的列表详细描述了G1垃圾收集周期的各个阶段、它们的暂停以及各个阶段之间的过渡:

  1. 纯年轻阶段:这个阶段从几个普通的收集开始,这些收集将对象升级到老年代。纯年轻阶段和空间回收阶段之间的过渡开始于老年代占用率达到某个阈值(启动堆占用率阈值)时。此时,G1计划启动并发新生代收集(Concurrent Start young collection),而不是普通新生代收集(Normal young collection)。
    • 并发开始(Concurrent Start):这种类型的收集除了执行普通的收集之外,还开始标记过程。并发标记决定了老年代中所有当前可达(活动)的对象将被保留到下一个空间回收阶段。普通的新生代收集可能会出现在标记尚未完成时。标记结束时有两个特殊stop-the-world暂停:备注(Remark)和清理(Cleanup)。
    • 备注(Remark):这种暂停完成标记本身,执行全局引用处理和类卸载,回收完全空的区域并清理内部数据结构。在“备注”和“清理”之间,G1计算能够同时回收的选定老年代区域可用空间,这将在“清理”暂停中完成。
    • 清理(Cleanup):这一暂停决定了空间回收阶段是否会真正到来。如果随后是空间回收阶段,则纯年轻阶段将以一个有准备的混合新生代收集来完成。
  2. 空间回收阶段:该阶段包括多个混合收集,除新生代区域外,还疏散老年代区域集合中的活动对象。当G1确定疏散更多的老年代空间性价比不高时,空间回收阶段就结束了。

空间回收后,收集周期从另一个纯年轻阶段重新开始。作为备份,如果应用程序在收集活动信息时耗尽内存,G1会像其他收集器一样执行一次就地完整堆压缩(完整垃圾收集, Full GC)。

垃圾收集暂停和收集集合

G1在stop-the-world暂停时进行垃圾收集和空间回收。存活对象通常从源区域复制到堆中的一个或多个目标区域,并调整对这些移动对象的现有引用。

对于非大型区域,对象的目标区域由该对象的源区域确定:

  • 新生代的对象(eden和survivor区域)被复制到survivor或老年代,这取决于他们的年龄。
  • 老年代区域的对象被复制到其他老年代区域。

巨大区域中的对象被区别对待。G1只决定他们的活跃度,如果它们死掉,收回它们占据的空间。大区域内的物体不会被G1移动。

收集集合是要从中回收空间的源区域集。根据垃圾收集的类型,收集集合由不同类型的区域组成:

  • 在纯年轻阶段,集合仅由新生代中的区域和具有潜在可回收对象的巨大区域组成。
  • 在空间回收阶段,由新生代的区域、具有潜在可回收对象的巨大区域以及收集组候选区域中的一些老年代区域组成。

G1在并发周期中准备垃圾收集的候选区域。在备注暂停期间,G1选择占用空间占用低的区域,这些区域包含大量可用空间。然后,在“备注”和“清理”暂停之间同时准备这些区域,以便以后收集。清理暂停会根据效率对其进行排序。在随后的混合收集中,优先选择包含更多空闲空间的高效区域,这些区域收集时间更少。

深入垃圾优先收集器内部

本节描述了垃圾优先(G1)垃圾收集器的一些重要细节。

确定初始堆占用率

G1在调整Java堆的大小时遵守标准规则,使用-XX:InitialHeapSize作为最小Java堆大小,-XX:MaxHeapSize作为最大Java堆大小,-XX:MinHeapFreeRatio代表最小可用内存比率,-XX:MaxHeapFreeRatio用于确定调整大小后最大可用内存百分比。G1收集器在备注暂停和完整收集中调整Java堆的大小。此过程可能会向操作系统返还内存或从操作系统分配内存。

纯新生代收集规模

G1总是在下一个突变阶段的普通新生代收集结束时对新生代进行评估。通过对实际暂停时间长时间的观察,G1可以达到-XX:MaxGCPauseTimeMillis-XX:PauseTimeIntervalMillis设置的暂停时间目标。它会考虑相似大小的新生代疏散需要多长时间。这包括在收集过程中需要复制多少对象,以及这些对象之间的关联程度等信息。

如果没有其他约束,那么G1自适应地将年轻一代的大小调整在-XX:G1NewSizePercent-XX:G1MaxNewSizePercent设定的暂停时间之间。有关如何修复长暂停的更多信息,请参见垃圾优先垃圾收集器优化

或者,-XX:NewSize-XX:MaxNewSize的组合可以分别用于设置最小和最大新生代大小。

空间回收阶段收集规模

在空间回收阶段,G1试图在一次垃圾收集暂停中最大化老年代回收的空间量。新生代的大小被设置为允许的最小值,通常由-XX:G1NewSizePercent决定。

在此阶段的每个混合收集开始时,G1从候选收集集合中选择一组区域。这组额外的老年代区域由三部分组成:

  • 老年代区域的最小集合,以确保疏散进度。这组老年代区域由候选区域的数量除以空间回收阶段的长度决定,空间回收阶段的长度由-XX:G1MixedGCCountTarget决定。
  • 如果G1预测在收集完上述最小集合后还会有时间,则收集集合中的其他老年代区域将成为候选区域。添加老年代区域,直到预计将使用80%的剩余时间。
  • 一组可选的收集集合区域,在上面两个部分被疏散后,G1会逐渐疏散这些集合区域,如果在此暂停中还有时间。

前两组区域在初始收集过程中收集,如有剩余的暂停时间再收集可选区域。由于可选集合的管理,这种方法确保了空间回收的进展,同时提高了达到暂停时间的概率,并且使开销最小。

当候选区域中的剩余可回收空间小于-XX:G1HeapWastePercent设定的值,空间回收阶段结束。

有关G1将使用多少老年代区域以及如何避免长时间混合收集暂停的更多信息,请参见垃圾优先垃圾收集器优化

定期垃圾收集

如果由于应用程序不活动而长时间没有垃圾收集,虚拟机可能会长时间保留大量未使用的内存,这些内存可能在其他地方使用。为了避免这种情况,G1可能会被迫使用-XX:G1PeriodicGCInterval选项进行常规垃圾收集。此选项确定G1考虑执行垃圾收集的最小时间间隔(毫秒)。如果自上次垃圾收集暂停后经过了这段时间,并且没有正在进行的并发循环,G1会触发额外的垃圾收集,可能会产生以下影响:

  • 在纯年轻阶段:G1使用并发开始暂停(Concurrent Start)来开始并发标记,或者,如果指定了-XX:-G1PeriodicGCInvokesConcurrent,则为完整GC。
  • 在空间回收阶段:G1会触发适合当前进度的垃圾收集暂停类型来继续空间回收阶段。

-XX:G1PeriodicGCSystemLoadThreshold选项可用于优化垃圾收集是否被触发:如果JVM主机系统(例如,容器)上的getloadavg()调用返回的平均一分钟系统负载值高于该值,则不会运行定期垃圾收集。

有关定期垃圾收集的更多信息,请参见JEP 346:立即从G1返回未使用的已提交内存

确定初始堆占用率

初始堆占用率(IHOP,Initiating Heap Occupancy Percent)是触发初始标记收集(Initial Mark)的阈值,定义为老年代大小的百分比。默认情况下,G1通过观察标记需要多长时间以及在标记周期中老年代通常分配多少内存来自动确定最佳IHOP。这一特性被称为自适应IHOP。如果此功能被激活,在没有足够的观察结果来对启动堆占用率阈值进行良好的预测时,选项-XX:InitiatingHeapOccupancyPercent将作为老年代大小的百分比的初始值。使用选项-XX:-G1UseAdaptiveIHOP关闭G1的此行为。在这种情况下,值-XX:InitiatingHeapOccupancyPercent决定这个阈值。

在内部,自适应IHOP尝试设置初始堆占用,以便当老年代占用处于当前最大老年代大小减去作为额外缓冲区的-XX:G1HeapReservePercent值时,开始空间回收阶段的第一次混合垃圾收集。

标记

G1标记使用了一种叫做*开始快照(Snapshot-At-The-Beginning,SATB)*的算法。它在初始标记暂停时获取堆的虚拟快照,只要标记开始时活动的对象在标记的剩余时间都被认为是存活的。这意味着,为了空间回收的目的(除了一些例外),在标记过程中变死(不可到达)的物体仍然被认为是活的。与其他收集器相比,这可能会导致一些额外的内存被错误地保留。然而,这可能会让SATB在备注暂停期间提供更好的时延。在该标记过程中过于保守考虑的活动对象将在下一个标记过程中被回收。有关标记问题的更多信息,请参见主题垃圾优先垃圾收集器优化

堆资源紧张下的行为

当应用程序占据如此多的内存以至于疏散无法找到足够的空间来复制时,就会发生疏散失败。疏散失败意味着G1只复制已经移动的对象到新位置来完成当前的垃圾收集,而不复制任何尚未移动的对象,只调整它们之间的引用。疏散失败可能会产生一些额外的开销,但通常和其他新生代收集一样快。在这次垃圾收集和疏散失败后,G1将恢复正常应用,没有任何其他措施。G1会假设疏散失败发生在垃圾收集接近结束时;也就是说,大多数对象已经被移动,并且还有足够的空间继续运行应用程序,直到标记完成和空间回收开始。

如果这个假设不成立,那么G1最终将安排一个完整垃圾收集。这种类型的收集对整个堆执行就地压缩。这可能很慢。

请参阅垃圾优先垃圾收集器优化,了解有关分配失败或在发出内存不足信号之前发生完全垃圾收集器故障的更多信息。

大对象

大对象是大于或等于半个区域大小的对象。除非使用-XX:G1HeapRegionSize选项进行设置,否则当前区域大小是按照G1工效学默认值部分中所述进行的。

这些大对象有时会被特殊对待:

  • 在老年代中,每个大对象都被分配为一系列连续的区域。对象本身的起点总是位于序列中第一个区域的起点。在整个对象被回收之前,序列最后一个区域中的任何剩余空间都不会被分配。
  • 一般来说,只有在清理暂停(Cleanup)期间标记结束时,或者在完整垃圾收集期间大对象无法访问的情况下,才会回收大对象。然而,对于原始类型数组(例如bool、各种整数和浮点值)的大对象有一个特殊的规定。如果在任何垃圾收集暂停期间,这样的大对象没有被多个对象引用,G1将会回收它。这种行为默认是启用的,你可以使用-XX:G1EagerReclaimHumongousObjects禁用它。
  • 大对象的分配可能会导致垃圾收集过早发生。G1在每个大对象分配中检查初始堆占用率阈值,如果当前占用率超过该阈值,可能会立即强制进行初始的新生代垃圾收集标记。
  • 大对象从不移动,即使在完整的垃圾收集也是如此。这可能会导致过早的慢速完整垃圾收集或大量区域空间碎片而导致的内存不足情况。

G1的工程学默认值

本主题概述了G1特有的最重要的设置及其默认值。他们给出了没有附加选项G1的预期行为和资源使用的粗略概述。

选项和默认值 描述
-XX:MaxGCPauseMillis=200 最大暂停时间
-XX:GCPauseTimeInterval=<ergo> 最大暂停时间间隔。默认情况下,G1没有设定任何值,允许G1在极端情况下连续收集垃圾。
-XX:ParallelGCThreads=<ergo> 垃圾收集期间用于并行工作的最大线程数。规则如下:如果进程可用的CPU线程数少于或等于8,使用相等的线程数。否则,为线程数的5/8。在每次暂停开始时,使用的最大线程数还会受到总的堆大小的限制:每个-XX:HeapSizePerGCThread不会多于一个线程。
-XX:ConcGCThreads=<ergo> 用于并发工作的最大线程数。默认情况下,该值为-XX:ParallelGCThreads除以4。
-XX:+G1UseAdaptiveIHOP
-XX:InitiatingHeapOccupancyPercent=45
控制初始堆占用率的默认值,表示自适应IHOP已打开,并且在最初的几个收集周期中,G1将使用45%作为老年代标记开始的阈值。
-XX:G1HeapRegionSize=<ergo> 堆区域大小。堆包含大约2048个堆区域。堆区域的大小可以从1到32 MB不等,必须是2的幂。
-XX:G1NewSizePercent=5
-XX:G1MaxNewSizePercent=60
新生代总的大小,在这两个值之间,总的堆空间的百分比。
-XX:G1HeapWastePercent=5 候选收集集合未回收空间的百分比。如果候选集合中的可用空间低于该值,G1将停止空间回收阶段。
-XX:G1MixedGCCountTarget=8 空间回收阶段的预期长度。
-XX:G1MixedGCLiveThresholdPercent=85 在空间回收阶段,不会收集存活对象占用率高于这个百分比的老年代区域。

注意:<ergo>意味着实际值是根据环境确定的。

与其他收集器的对比

这里总结一下G1和其他收集齐之间主要区别:

  • 并行收集器将老年代空间作为一个整体来压缩和回收。G1逐渐将这项工作分解为多个更小的垃圾收集工作。这大大缩短了暂停时间,但这可能会牺牲吞吐量
  • 和CMS收集器类似,G1并发的执行老年代空间的收集。然而除非进行完整垃圾收集,CMS不能整理老年代的碎片。
  • 由于其并发性,G1的开销可能高于上述收集器,进而影响吞吐量。
  • ZGC的目标是特大型堆,旨在牺牲更高的吞吐量来提供更短的暂停时间。

由于其工作原理,G1有一些独特的机制来提高垃圾收集效率:

  • 在任何收集过程中,G1都可以回收一些空的老年代空间。这可以避免许多其他不必要的垃圾收集,轻易就可以释放大量空间。
  • G1可以选择性的消除堆上的重复字符串。

从老一代回收空的大对象默认是启用的。您可以使用选项-XX:-G1EagerReclaimHumongousObjects禁用此功能。默认情况下,字符串重复数据消除处于禁用状态。您可以使用选项-XX:+G1EnableStringDeduplication来启用它。

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

本节描述了如何在垃圾优先垃圾收集器(G1垃圾收集器)不符合您的要求的情况下调整它的行为。

本节主题:

  • 对G1的一般性建议
  • 从其他收集器迁移到G1
  • 改进G1的性能
    • 观察完整垃圾收集
    • 大对象碎片
    • 时延优化
      • 异常系统或实时使用
      • 引用对象处理时间过长
      • 纯年轻阶段纯新生代垃圾收集时间过长
      • 混合收集时间过长
      • 更新记忆集和扫描记忆集时间过高
    • 吞吐量优化
    • 堆的大小优化
    • 默认值优化

对G1的一般性建议

一般建议使用G1的默认设置即可,然后给它一个不同的暂停时间设置,并根据需要使用-Xmx设置最大的Java堆大小。

与其他收集器不同,G1的默认值做了不同的平衡。默认配置中,G1的目标既不是最大吞吐量,也不是最低时延,而是在高吞吐量下提供相对较小、均匀的暂停。然而,G1递增式的空间回收机制和暂停时间控制会在应用程序线程和空间回收效率方面产生一些开销。

如果您想要高吞吐量,那么可以使用-XX:MaxGCPauseMillis来放宽暂停时间目标,或者提供一个更大的堆。如果时延是主要要求,则修改暂停时间设置。避免使用-Xmn-XX:NewRatio等选项将新生代的规模限制在特定值,因为新生代的规模是G1用来满足暂停时间的主要手段。将新生代的大小设置为固定值会覆盖并实际上禁用暂停时间控制。

从其他收集器迁移到G1

通常,当从其他收集器(尤其是CMS收集器)迁移到G1时,首先要删除所有影响垃圾收集的选项,只需要设置暂停时间以及使用-Xmx和可选的-Xms设置总堆大小。

对于其他收集器,许多选项对其响应很有用,但它们对于G1起来说,要么根本不起作用,要么甚至降低吞吐量和达到暂停时间目标的可能性。一个例子是设定新生代的规模,这完全阻止了G1调整新生代的规模以达到设置的暂停时间目标。

改进G1性能

G1旨在提供良好的整体性能,而无需指定其他选项。然而,有些情况下,默认的启发式方法或默认配置效果可能不是最优的。本节给出了一些诊断和改善这些情况的指南。本指南仅描述了在给定一组应用程序的情况下,G1在给定指标下提高垃圾收集器性能的可能性。在具体案例上,应用程序级优化可能比试图调整虚拟机性能更好更有效,例如,通过使用寿命较短的对象完全避免一些有问题的情况。

出于诊断目的,G1提供全面的日志。一个好的开始是使用-Xlog:gc*=debug选项,然后在必要时从中提炼输出内容。日志提供了关于垃圾收集活动暂停期间和暂停之外的详细概述。这包括收集的类型和在暂停的特定阶段花费的时间的细节。

以下小节探讨了一些常见的性能问题。

观察完整垃圾收集

*完整堆垃圾收集(Full GC)*通常非常耗时。老年代占用率过高导致的完整收集可以在日志中查找单词"Pause Full (Allocation Failure)"得到。完整收集通常紧跟在一个"to-space exhausted"标签标示的垃圾收集之后。

发生完整垃圾回收的原因是应用程序分配了太多无法快速回收的对象。通常并发标记不能及时完成,以开始空间回收阶段。许多大对象的分配可能会增加进入完整收集的可能性。由于这些对象在G1的分配方式,它们可能会占用比预期多得多的内存。

目标应该是确保并发标记按时完成。这可以通过降低老年代的分配率或者给并发标记更多的时间来完成。

G1给了你几个选项来更好地处理这种情况:

  • 您可以使用gc+heap=info日志来确定Java堆上大对象占据的区域数量。"Humongous regions: X->Y”行中的Y表示大对象占据的区域数量。如果与老年代的数量相比,该数较高,最好的选择是尝试减少区域的数量。您可以通过使用-XX:G1HeapRegionSize选项增加区域大小来实现这一点。当前选择的堆区域大小打印在日志的开头。
  • 增加Java堆的大小。这通常会增加标记完成的时间。
  • 通过显式设置-XX:ConcGCThreads,增加并发标记线程的数量。
  • 迫使G1提前开始标记。G1根据应用程序行为自动确定IHOP阈值。如果应用程序行为改变,这些预测可能是错误的。有两种选择:通过修改-XX:G1ReservePercent来增加自适应IHOP计算中使用的缓冲区,从而降低何时开始空间回收的目标占用率;或者,通过使用-XX:-G1UseAdaptiveIHOP-XX:InitiatingHeapOccupancyPercent手动设置IHOP的阈值。

除了内存分配失败完整收集通常是由应用程序或某个外部工具导致的。如果原因是System.gc(),并且没有办法修改应用程序源码,则可以通过使用-XX:+ExplicitGCInvokesConcurrent或通过设置-XX:+DisableExplicitGC让虚拟机完全忽略它们来减轻完整收集的影响。外部工具可能仍然会强制完整垃圾收集,不用它们的时候就把它们删除。

大对象碎片

为了寻找连续区域,完整垃圾收集可能会在堆内存耗尽之前就进行。一个潜在的选项是提高-XX:G1HeapRegionSize的值,从而降低大对象占用的区域数,或者增加堆的整体大小。在极端情况下,即使可用内存足够G1却找不到充足的连续区域,这将会导致虚拟机退出。因此,除了前面提到的减少大对象分配或者增加堆之外,没有其他选择。

时延优化

本节讨论了在常见时延问题(即暂停时间过高)的情况下如何改善G1行为。

异常系统或实时使用

对于每一次垃圾收集暂停,gc+cpu=info日志输出都包含一行包含来自操作系统的信息,并附有垃圾收集时间明细。这种输出的一个例子是User=0.19s Sys=0.00s Real=0.01s

用户时间(User)是在虚拟机代码中花费的时间,系统时间(Sys)是在操作系统中花费的时间,实时(Real)是在暂停期间经过的绝对时间量。如果系统时间相对较长,那么最常见的原因是环境。

系统时间过高的常见原因:

  • 虚拟机从操作系统内存分配或返还内存可能会导致不必要的时延。通过使用选项-Xms-Xmx将最小和最大堆大小设置为相同的值,并使用-XX:+AlwaysPreTouch预接触所有内存,将此工作移到虚拟机启动阶段,从而避免延迟。
  • 特别是在Linux中,通过透明大页面(THP)功能将小页面合并成大页面往往会拖延随机进程,而不仅仅是在垃圾收集暂停期间。因为虚拟机分配并维护大量内存,所以虚拟机成为长时间停顿的进程的风险比通常情况下要高。请参考操作系统文档,了解如何禁用透明大页面功能。
  • 日志写入输出可能会暂停一段时间,因为一些后台任务会间歇性地占用日志写入的硬盘的输入/输出带宽。考虑为日志或其他存储使用单独的磁盘,例如内存备份文件系统,以避免这种情况。

另一个需要注意的情况是实时比其他情况的总和大得多,这可能表明虚拟机在可能过载的机器上没有获得足够的CPU时间。

引用对象处理时间过长

引用对象处理发生在引用处理阶段。在引用处理阶段,G1根据引用对象的类型更新引用。默认情况下,G1尝试使用以下启发式方法来并发进行引用处理:对于每-XX:ReferencesPerThread个引用对象启动一个线程,最多-XX:ParallelGCThreads个线程。默认情况下,可以通过将-XX:ReferencesPerThread设置为0来禁用此启发式算法,或者通过-XX:-ParallelRefProcEnabled完全禁用并行化。

纯年轻阶段纯新生代垃圾收集时间过长

一般来说,任何新生代收集需要的时间大致与新生代的大小成比例,或者更具体地说,与其中需要复制的存活对象数量成比例。如果疏散收集(Evacuate Collection Set)花费的时间太长,特别是对象复制子阶段(Object Copy),则减少-XX:G1NewSizePercent。这减少了新生代的最小尺寸,停顿的时间可能更短。

如果应用程序性能,特别是幸存的对象数量突然改变,新生代的规模可能会导致垃圾收集暂停时间激增。通过使用-XX:G1MaxNewSizePercent来减小新生代的规模可能是有用的。这限制了新生代的最大尺寸,因此也限制了垃圾收集需要处理的对象数量。

混合收集时间过长

混合收集用来回收老年代的空间。混合收集区域包含新生代和老年代区域。通过启用gc+ergo+cset=trace打印日志输出,您可以获得新生代或老年代区域的疏散时间对暂停时间的影响。分别查看新生代区域和老年代区域的暂停时间。

如果新生代时间太长,则查看上一节纯年轻阶段纯新生代垃圾收集时间过长。否则,为了减少老年代对暂停时间的贡献,G1提供了三种选择:

  • 增加-XX:G1MixedGCCountTarget将垃圾收集扩散到更多的老年代区域
  • 通过使用-XX:G1MixedGCLiveThresholdPercent,避面将占用率高的区域放入候选收集集合中。在许多情况下,高占用率的区域需要大量时间来收集。
  • 尽早停止老年代的空间回收,这样G1就不会收集那么多高度占用的区域。在这种情况下,增加-XX:G1HeapWastePercent百分比。

请注意,后两个选项减少了当前空间回收阶段可回收空间候选区域的数量。这可能意味着G1可能无法在老年代中回收足够的空间用于持续运营。然而,稍后的空间回收阶段可能收集它们。

更新记忆集和扫描记忆集时间过高

为了使G1能够疏散单个老年代区域,G1跟踪跨区域引用(cross-region references)的位置,即从一个区域指向另一个区域的引用。指向给定区域的跨区域引用集称为该区域的记忆集(remembered set)。移动区域内容时,必须更新记忆集。区域记忆集的维护大多是同时进行的。出于性能考虑,当应用程序的两个对象之间建立新的跨区域引用时,G1不会立即更新。记忆集更新请求会被延迟并批量处理以提高效率。

G1需要完整的记忆集进行垃圾收集,因此垃圾收集的*更新记忆集(Update RS)*阶段会处理任何未完成的更新请求。*扫描记忆集(Scan RS)*阶段会搜索记忆集中引用的对象,移动区域的内容,更新到新的引用位置。根据应用程序的不同,这两个阶段可能会花较长的时间。

使用选项-XX:G1HeapRegionSize调整堆区域的大小会影响跨区域引用的数量以及记忆集的大小。处理区域的记忆集可能是垃圾收集工作的一个重要部分,因此这对最大暂停时间有直接影响。较大的区域往往具有较少的跨区域引用,因此处理这些引用所花费的相对工作量会减少,尽管与此同时,较大的区域可能意味着每个区域要疏散更多的存活对象,从而增加了其他阶段的时间。

G1尝试并发处理记忆集的更新,更新阶段花费时间大概是最大暂停时间的-XX:G1RSetUpdatingPauseTimePercent。通过降低该值,G1通常会以更高的并发进行记忆集更新工作。

批量更新记忆集可能会导致记忆集更新与大对象分配合并在一起,从而造成虚假的更新时间过长。如果批处理正好发生在垃圾收集之前,那么就需要处理记忆集更新的所有工作。使用-XX:-ReduceInitialCardMarks禁用这种行为,潜在的避免这种情况。

记忆集扫描时间还取决于G1保存记忆集的压缩量。记忆集在内存中存储得越紧凑,在垃圾收集过程中检索存储值所需的时间就越长。G1自动执行这种压缩,称为记忆集粗化(coarsening),同时根据该区域记忆集的当前大小更新记忆集。特别是在最高压缩级别,检索数据可能会非常慢。使用-XX:G1SummarizeRSetStatsPeriod选项和gc+remset=trace日志级别可以显示是否有粗化发生。如果是这样,那么在Before GC Summary之前部分中的Did <X> coarsenings行中X显示一个高值。增加-XX:G1RSetRegionEntries选项可以显著的降低粗化量。避免在生产环境中使用详细的记忆集日志记录,因为收集此数据可能会花费大量时间。

吞吐量优化

G1的默认策略试图在吞吐量和延迟之间保持平衡;然而,有些情况下需要更高的吞吐量。除了如前所述减少总暂停时间之外,暂停的频率也可以减少。主要思想是通过使用-XX:MaxGCPauseMillis来增加最大暂停时间。分代大小启发式算法将自动调整新生代的大小,这直接决定暂停的频率。如果这没有导致预期的行为,特别是在空间回收阶段,使用-XX:G1NewSizePercent增加新生代的规模将迫使G1这样做。

在某些情况下,-XX:G1MaxNewSizePercent:允许的新生代最大规模,可以通过限制新生代规模来限制吞吐量。这可以通过查看gc+heap=info日志来诊断。在这种情况下,eden区域和survivor区域总的百分比接近于-XX:G1MaxNewSizePercent。在这种情况下,考虑增加-XX:G1MaxNewSizePercent的值。

增加吞吐量的另一个选择是尝试减少并发工作量。特别是,并发更新记忆集通常需要大量的CPU资源。增加-XX:G1RSetUpdatingPauseTimePercent将并发操作挪到垃圾收集阶段。在最糟糕的情况下,通过设置-XX:-G1UseAdaptiveConcRefinement -XX:G1ConcRefinementGreenZone=2G -XX:G1ConcRefinementThreads=0可以完全禁用这种机制,将记忆集更新工作挪到下一次垃圾收集。

通过使用-XX:+UseLargePages启用大页面也可以提高吞吐量。请参考操作系统文档,了解如何设置大页面。

将选项-Xms-Xmx设置为相同的值,您可以通过禁用堆大小调整。此外,您可以使用-XX:+AlwaysPreTouch将操作系统工作放在虚拟机的启动时间,虚拟机使用物理内存支持的虚拟内存。为了使暂停时间更加一致,这两种方法都是特别理想的。

堆的大小优化

和其他收集器一样,G1将调整堆的大小使垃圾收集所花费的时间低于-XX:GCTimeRatio选项所设定的比率。调整此选项,使G1符合您的要求。

默认值优化

本节介绍了默认值和本主题中介绍的命令行选项的一些附加信息。

选项和默认值 描述
-XX:+G1UseAdaptiveConcRefinement
-XX:G1ConcRefinementGreenZone=<ergo>
-XX:G1ConcRefinementYellowZone=<ergo>
-XX:G1ConcRefinementRedZone=<ergo>
-XX:G1ConcRefinementThreads=<ergo>
并发记忆集更新使用这些选项来控制并发线程的工作分配。G1为这些选项选择符合工效学的值,使用-XX:G1RSetUpdatingPauseTimePercent设置垃圾收集暂停中剩余工作花费时间,根据需要自适应的调整。更改时务必小心,因为这可能会导致非常长的暂停时间。
-XX:+ReduceInitialCardMarks 初始对象分配中记忆集并发修改
-XX:+ParallelRefProcEnabled
-XX:ReferencesPerThread=1000
-XX:ReferencesPerThread决定并发的程度:每N个引用有一个线程进行引用处理,线程数最多-XX:ParallelGCThreads。值为0表示始终使用-XX:ParallelGCThreads值所指示的最大线程数。这决定了java.lang.Ref.*实例的处理是否应该由多个线程并发完成。
-XX:G1RSetUpdatingPauseTimePercent=10 这决定了G1在记忆集更新时间占垃圾收集总时间的百分比。G1使用此设置控制记忆集更新并发的数量。
-XX:G1SummarizeRSetStatsPeriod=0 控制多少次垃圾收集生成记忆集摘要报告。将此设置为零以禁用。生成记忆集摘要报告是一项成本很高的操作,因此只有在必要时才应该使用,并且要把值设的高一些。使用gc+remset=trace打印所有内容。
-XX:GCTimeRatio=12 这是垃圾收集上的时间与应用程序的时间之比。用于确定垃圾收集中可以花费的时间的目标分数的实际公式是1 / (1 + GCTimeRatio)。该默认值将会导致有大约8%的时间用于垃圾收集。
-XX:G1PeriodicGCInterval=0 检查G1是否应该触发定期垃圾收集的时间间隔(毫秒)。设置为零禁用。
-XX:+G1PeriodicGCInvokesConcurrent 如果设置,定期垃圾收集会触发并发标记或继续现有收集周期,否则会触发完整垃圾收集。
-XX:G1PeriodicGCSystemLoadThreshold=0.0 触发定期垃圾收集的系统负载阈值,当前系统负载可以通过调用getloadavg()获得。高于此值的系统负载不会进行定期垃圾收集。零值表示此阈值检查被禁用。

注意:<ergo>意味着实际值是根据环境确定的。

11 Z收集器

Z垃圾收集器(ZGC)是一个可扩展的低延迟垃圾收集器。ZGC并发执行所有代价高昂的工作,停止应用程序线程的时间不会超过10ms,这使得它适用于需要低时延和/或特大堆(几T字节)的应用程序。

Z垃圾收集器是一个实验特性,并通过命令行选项-XX:+UnlockExperimentalVMOptions -XX:+UseZGC启用。

设置堆大小

ZGC最重要的调整选项是设置最大堆大小(-Xmx)。由于ZGC是一个并发收集器,因此必须按如下方法选择最大堆大小,1)堆可以容纳应用程序的存活对象,2)即使在垃圾收集运行时也有足够的堆空间分配给应用程序。需要多少空间在很大程度上取决于应用程序的分配速率和存活对象大小。总的来说,你给ZGC的内存越多越好。但与此同时,浪费内存是不可取的,所以这一切都是为了在内存使用和垃圾收集运行频率之间找到平衡。

设置垃圾收集的并发数

第二个可能需要考虑的调整选项是设置并发GC线程的数量(-XX:ConcGCThreads)。ZGC有启发式算法自动选择这个数。这种启发式方法通常运行良好,但是根据应用程序的特点,这可能需要调整。这个选项本质上决定了应该给垃圾收集多少CPU时间。给它太多,垃圾收集器会从应用程序中窃取太多的CPU时间。给它太少,应用程序产生垃圾的速度可能会比垃圾收集速度快。

12 其他情况

本节涵盖影响垃圾收集的其他情况。

本节主题:

  • 弱引用、软引用和幻像引用的终结
  • 显式垃圾收集
  • 软引用
  • 类元数据

弱引用、软引用和幻像引用的终结

一些应用程序通过使用弱引用、软引用和幻像引用的终结(Finalization)来与垃圾收集交互。

这些特性可能在Java编程语言级别造成性能隐患。这方面的一个例子是依赖于终结来关闭文件描述符,这使得外部资源(描述符)依赖于垃圾收集的及时性。依靠垃圾收集来管理内存以外的资源几乎总是一个坏主意。

请参见如何处理Java终结的内存保留问题,其中深入讨论了终结的一些陷阱以及避免它们的技术。

显式垃圾收集

应用程序与垃圾收集交互的另一种方式是使用System.gc()显式调用完整的垃圾收集。

这可能会在不必要的时候强制进行大规模垃圾收集(例如,当小规模收集就足够了),因此通常应该避免。显式垃圾收集的性能影响可以通过使用标志-XX:+DisableExplicitGC禁用它们来衡量,这将导致虚拟机忽略对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虚拟机中有一个内部表示,称为类元数据。

在先前版本的Java Hotspot虚拟机中,类元数据是在所谓的永久代(permanent generation)中分配的。从JDK 8开始,永久代被删除,类元数据被分配到本机内存中。默认情况下,可用于类元数据的本机内存量不受限制。使用选项-XX:MaxMetaspaceSize设置类元数据的上限。

Java Hotspot虚拟机显式地管理元数据空间。从操作系统请求空间,然后将其分成块。类加载器从其块中为元数据分配空间(块绑定到特定的类加载器)。当为类加载器卸载类时,它的块会被循环使用或返回到操作系统。元数据使用mmap而不是malloc分配的空间。

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

当相应的Java类被卸载时,类元数据被回收。垃圾收集可能会导致Java类卸载和元数据被回收。当为类元数据提交的空间达到某个级别(高水位线)时,就会引发垃圾收集。垃圾收集后,高水位线可能会根据从类元数据中释放的空间量而升高或降低。高水位线可能会升高,以免过早引发另一次垃圾收集。高水位线初值为命令行选项-XX:MetaspaceSize的值,并根据选项-XX:MaxMetaspaceFreeRatio-XX:MinMetaspaceFreeRatio升高或降低。如果类元数据的申请空间中可用空间百分比大于-XX:MaxMetaspaceFreeRatio,则高水位线将会降低。如果它小-XX:MinMetaspaceFreeRatio,那么高水位线将会升高。

为选项-XX:MetaspaceSize指定一个高一些的值,以避免引发过早的垃圾收集。为应用程序分配的类元数据的数量取决于应用程序,并且不存在选择-XX:MetaspaceSize的一般准则。-XX:MetaspaceSize的默认大小取决于平台,范围从12 MB到20 MB不等。

关于元数据所用空间的信息包含在堆的日志输出中。以下是典型输出:

[0,296s][info][gc,heap,exit] Heap
[0,296s][info][gc,heap,exit] garbage-first heap total 514048K, used 0K [0x00000005ca600000, 0x00000005ca8007d8, 0x00000007c0000000)
[0,296s][info][gc,heap,exit] region size 2048K, 1 young (2048K), 0 survivors (0K)
[0,296s][info][gc,heap,exit] Metaspace used 2575K, capacity 4480K, committed 4480K, reserved 1056768K
[0,296s][info][gc,heap,exit] class space used 238K, capacity 384K, committed 384K, reserved 1048576K

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

附录:主要中英文名词翻译对照表

英文 中文
Minor Collection 小规模垃圾收集
Major Collection 大规模垃圾收集
Young Generation 新生代
Old Generation 老年代
Ergonomics 工效学
The Mostly Concurrent Collectors 主要并发收集器
Full GC 完整垃圾收集
Young-only Phase 纯年轻阶段
Space-reclamation Phase 空间回收阶段