原文:https://docs.oracle.com/javase/9/gctuning/JSGCT.pdf
从桌面小程序applet到大型服务器上的Web服务,有各种各样的应用程序在使用Java平台,标准版(Java SE)。为了支持这些不同种类的部署,Java HotSpot虚拟机实现(Java HotSpot VM)提供了多种垃圾收集器,每个垃圾收集器都旨在满足不同的需求。
Java SE根据运行应用程序的计算机种类选择最合适的垃圾收集器。但是,这种选择对于每个应用程序可能都不是最佳的。具有严格性能目标或其他要求的用户,开发人员和管理员可能需要明确选择垃圾收集器并调整某些参数以达到所需的性能级别。
本文档提供了有助于完成这些任务的信息。首先,在串行Stop-the-world收集器的上下文中描述了垃圾收集器的一般功能和基本调整选项。然后介绍其他收集器的特定功能以及选择收集器时要考虑的因素。
垃圾收集器(GC)是一种内存管理工具,它通过以下操作实现自动内存管理:
• 将对象分配给年轻代并将老对象移到老年代。
• 通过并发(并行)标记阶段在老年代中查找活动对象。当Java堆总占用率超过默认阈值时,Java HotSpot VM将触发标记阶段。请参阅Concurrent Mark Sweep(CMS)收集器和Garbage-First垃圾收集器部分。
• 通过并行拷贝,压缩活动对象来恢复可用内存。请参阅并行收集器和Garbage-First垃圾收集器部分。
垃圾收集器的选择在什么时候很重要?对于某些应用程序,答案是永远不重要。也就是说,垃圾收集得很好,收集的频率适度,程序停顿的时间也可以接受。但是,对于大型应用程序来说情况并非如此,特别是那些具有大量数据(GB级以上),多个线程和高事务处理率的应用程序。
Amdahl定律(给定问题的并行加速受到问题的串行部分的限制)意味着大多数工作负载无法完美并行化; 某些部分总是串行的,不会受益于并行性。对于Java平台也是如此。特别是Oracle Java 1.4之前的Java平台的虚拟机不支持并行垃圾收集,因此相对于其他并行应用程序而言垃圾收集对多处理器系统的影响更大。
图1-1中的图表“比较垃圾收集中花费的时间百分比”模拟了一个理想的系统,除了垃圾收集(GC)之外,它是完全可扩展的。红线是在单处理器系统上仅花费1%的时间用于垃圾收集的应用程序,而具有32个处理器的系统上,吞吐量损失超过20%。洋红色线显示,对于花费10%的时间用于垃圾收集的应用程序(不考虑在单处理器应用程序中垃圾收集时间过多),当扩展到32个处理器时,超过75%的吞吐量会丢失。
图1-1比较垃圾收集时间的百分比
“图1-1比较垃圾收集时间百分比”的描述
这表明在小型系统上开发时可忽略不计的速度问题可能成为扩展到大型系统时的主要瓶颈。然而,在减少这种瓶颈方面的微小改进可以在性能上产生很大的提高。对于足够大的系统,选择正确的垃圾收集器并在必要时进行调整是值得的。
串行收集器通常适用于大多数“小型”应用程序(大约100MB堆的现代处理器上)的应用程序。其他收集器具有额外的开销或复杂性,这是其实现特殊行为的代价。如果应用程序不需要备用收集器的特殊行为,那么请使用串行收集器。串行收集器不应该是最佳选择的一种情况是在具有大量内存和两个内存的机器上运行的大型高度线程化的应用程序,当应用程序在此类服务器级计算机上运行时,默认情况下会选择并行收集器。请参阅“人机工程学”一节。
本文档是在使用Solaris操作系统(SPARC Platform Edition)上的Java SE 8的基础上编写的。但是,此处提供的概念和建议适用于所有受支持的平台,包括Linux,Microsoft Windows,Solaris操作系统(x64平台版)和OS X。此外,所有支持的平台上都提供了所提到的命令行选项,某些选项的默认值在每个平台上可能不同。
Ergonomics是Java虚拟机(JVM)和垃圾收集调优(例如基于行为的调优)提高应用程序性能的过程。JVM为垃圾收集器,堆大小和运行时编译器提供与平台相关的默认选择。这些选择符合不同类型应用程序的需求,同时需要较少的命令行调整。此外,基于行为的调优动态调整堆的大小以满足应用程序的指定行为。
本节介绍这些默认选项和基于行为的调整。在使用后续部分中描述的更详细控制之前,请先使用这些默认值。
服务器级机器的配置如下:
• 2个或更多物理处理器
• 2 GB以上的物理内存
在服务器级计算机上,默认情况下选择以下内容:
• 吞吐量垃圾收集器
• 初始堆大小为1/64的物理内存,最大为1 GB
• 最大堆大小为1/4物理内存,最大为1 GB
• Server runtime编译器
有关64位系统的初始堆和最大堆大小,请参阅并行收集器中的“默认堆大小”一节。
服务器类机器的定义适用于所有平台,但运行Windows操作系统版本的32位平台除外。表2-1“默认运行时编译器”显示了针对不同平台的运行时编译器所做的选择。
表2-1默认运行时编译器
平台 | 操作系统 | 默认 | 服务器类默认 |
---|---|---|---|
i586 | Linux | 客户端 | 服务器 |
i586 | 视窗 | 客户 | 客户 |
SPARC(64位) | Solaris | 服务器 | 服务器3 |
AMD(64位) | Linux的 | 服务器 | 服务器3 |
AMD(64位) | 视窗 | 服务器 | 服务器3 |
1客户端表示使用客户端运行时编译器。服务器意味着使用服务器运行时编译器。
2选择策略即使在服务器类机器上也使用客户端运行时编译器。之所以做出这样的选择是因为历史上客户端应用程序(例如,交互式应用程序)在平台和操作系统的这种组合上运行得更频繁。
3仅支持服务器运行时编译器。
对于并行收集器,Java SE提供了两个基于实现应用程序的指定行为的垃圾收集调整参数:最大停顿时间目标和应用程序吞吐量目标; 请参阅并行收集器一节。(这两个选项在其他收集器中不可用。)请注意,不能始终满足这些行为。应用程序需要一个足够大的堆来至少保存所有活着的数据。此外,最小堆大小可能妨碍达到这些期望的目标。
停顿时间是垃圾收集器停止应用程序并恢复不再使用的空间的持续时间。最大停顿时间目标的意图是限制这些停顿的最长时间。垃圾收集器维护平均停顿时间和平均值的方差,平均值是从执行开始时获得的,但是是加权的,越近期的停顿权值越大。如果平均值加上停顿时间的方差大于最大停顿时间目标,则垃圾收集器认为目标未得到满足。
使用命令行选项指定最大停顿时间目标-XX:MaxGCPauseMillis=。是需要停顿毫秒。垃圾收集器将调整Java堆大小和与垃圾收集相关的其他参数,以尝试使垃圾收集停顿时间短于毫秒。默认情况下,没有最大停顿时间目标。这些调整可能会导致垃圾收集器更频繁地发生,从而降低了应用程序的整体吞吐量。垃圾收集器尝试在吞吐量目标之前满足任何停顿时间目标。但是,在某些情况下,无法满足所需的停顿时间目标。
吞吐量目标是根据收集垃圾所花费的时间和垃圾收集之外所花费的时间(称为应用程序时间)来衡量的。目标由命令行选项指定-XX:GCTimeRatio=。垃圾收集时间与应用时间的比率为1 /(1 + )。例如,-XX:GCTimeRatio=19设置垃圾收集总时间的1/20或5%的目标。
垃圾收集所花费的时间是年轻代和老年代收集的总时间。如果未满足吞吐量目标,则增加代的大小以努力增加应用程序在收集之间运行的时间。
如果已满足吞吐量和最大停顿时间目标,则垃圾收集器会减小堆的大小,直到无法满足其中一个目标(总是吞吐量目标),然后解决未达到的目标。
除非您知道需要的堆大于默认的最大堆大小,否则不要为堆选择最大值。选择足以满足您的应用程序的吞吐量目标。
堆将增大或缩小到支持所选吞吐量目标的大小。应用程序行为的更改可能导致堆增长或缩小。例如,如果应用程序开始以更高的速率分配,则堆将增长以保持相同的吞吐量。
如果堆增长到其最大大小并且未满足吞吐量目标,则最大堆大小对于吞吐量目标而言太小。将最大堆大小设置为接近平台上的总物理内存但不会导致应用程序交换的值。再次执行应用程序。如果仍未达到吞吐量目标,则应用程序时间的目标对于平台上的可用内存来说太高。
如果可以满足吞吐量目标,但停顿时间过长,则选择最大停顿时间目标。选择最大停顿时间目标可能意味着您的吞吐量目标将无法满足,因此请选择对应用程序而言可接受的折衷的值。
当垃圾收集器试图满足竞争目标时,堆的大小通常会振荡。即使应用程序已达到稳定状态,也是如此。实现吞吐量目标(可能需要更大的堆)的压力是与最大停顿时间的目标和最小占用空间的目标(两者都可能需要小堆)相竞争。
Java SE平台的一个优势是它可以保护开发人员免受内存分配和垃圾收集复杂性的影响。但是,当垃圾收集是主要瓶颈时,了解此隐藏实现很有用。垃圾收集器对应用程序使用对象的方式做出假设,这些可以反映在可调参数中,可以调整这些参数以提高性能,而不会牺牲抽象的功能。
当一个对象无法再从正在运行的程序中的任何指针访问时,它被认为是垃圾。最直接的垃圾收集算法是迭代每个可到达的对象。剩下的任何物体都被认为是垃圾。这种方法所花费的时间与活动对象的数量成正比,这对于维护大量实时数据的大型应用程序来说是不可行的。
虚拟机包含许多不同的垃圾收集算法,这些算法使用世代集合进行组合。简单的垃圾收集检查堆中的每个活动对象,而分代收集利用几个经验观察的大多数应用程序的属性,以使回收未使用(垃圾)对象所需的工作最小化。所观察的属性中最重要的是弱世代假说:大多数对象只能存活很短的时间。
图3-1中的蓝色区域是对象生命周期的典型分布。x轴是以分配的字节为单位测量的对象生命周期。y轴上的字节数是具有相应生命周期的对象中的总字节数。左边的尖峰表示在分配后不久可以回收的物体(换句话说,已经“死亡”)。例如,迭代器对象在单个循环的持续时间内通常是活动的。
图3-1对象生命周期的典型分布
“图3-1对象生命周期的典型分布”的描述
有些对象的生命周期很长,因此分布向右延伸。例如,通常在初始化时分配一些对象,这些对象一直存在直到进程退出。在这两个极端之间是在一些中间计算期间存活的对象,在这里看作是初始峰值右侧的块。一些应用具有非常不同的分布,但是大多数具有这种一般形状。通过关注大多数对象“年轻时死亡”这一事实,可以实现高效收集。
为了优化这种情况,内存是在世代中管理的(内存池包含不同年龄的对象)。当某个世代填满时,会发生垃圾收集。绝大多数对象都分配在年轻对象(年轻代)池中,并且大多数对象都在那里死亡。当年轻代填满时,它会导致一个小小的收集只收集年轻代;其他世代的垃圾没有回收。
假设弱世代假说成立,并且年轻代中的大多数物体都是垃圾并且可以被回收,则可以优化为minor收集。这些收集的成本,按照第一顺序,与收集的活动对象的数量成比例;很快就会收集到充满死亡对象的年轻代。通常,来自年轻代的幸存对象的一部分在每次minor收集期间被移动到老年代。最终,老年代将填满并必须收集,从而产生一个major收集,即收集整个堆。major收集通常比minor收集持续更长时间,因为涉及的对象数量明显更多。
如Ergonomics部分所述,Ergonomics动态选择垃圾收集器,以在各种应用程序上提供良好的性能。串行垃圾收集器专为具有小型数据集的应用程序而设计,其默认参数被选择为对大多数小型应用程序有效。并行或吞吐量垃圾收集器旨在用于具有中到大数据集的应用程序。由Ergonomics选择的堆大小参数加上自适应大小策略的功能旨在为服务器应用程序提供良好的性能。这些选择适用于大多数情况,但不是所有情况,这导致了本文档的核心原则:
注意:
如果垃圾收集成为瓶颈,您很可能必须自定义总堆大小以及各代的大小。检查详细的垃圾收集器输出,然后探索各个性能指标对垃圾收集器参数的敏感性。
图3-2,“除了并行收集器和G1之外,世代的默认排列”
“图3-2世代默认排列,并行收集器和G1除外”的说明
在初始化时,除非需要,否则实际上保留了最大地址空间但未分配给物理内存。为对象存储器保留的完整地址空间可以分为年轻和老年代。
年轻代由Eden和两个幸存者空间组成。大多数对象最初都是在Eden中分配的。一个幸存者空间在任何时候都是空的,并且作为Eden中任何活动对象的目的地;另一个幸存者空间是下一个复制收集的目的地。以这种方式在幸存者空间之间复制对象,直到它们足够老到老年代(复制到老年代)。
垃圾收集性能有两个主要指标:
• 吞吐量是未垃圾收集总时间的百分比。吞吐量包括分配所花费的时间(但通常不需要调优分配速度)。
• 停顿是应用程序显示无响应的时间,因为正在进行垃圾收集。
用户对垃圾收集有不同的要求。例如,有些人认为Web服务器的正确度量标准是吞吐量,因为垃圾收集期间的停顿可能是可以容忍的,或者只是被网络延迟所掩盖。但是,在交互式图形程序中,即使短停顿顿也可能对用户体验产生负面影响。
一些用户对其他考虑因素很敏感。占用空间是一个进程的工作集,以页和缓存行来衡量。在具有有限物理内存或许多进程的系统上,占用空间可能会影响可伸缩性。
及时性是指对象变为死亡和内存可用之间的时间,这是分布式系统的一个重要考虑因素,包括远程方法调用(RMI)。
通常,选择特定世代的尺寸以权衡这些因素。例如,非常大的年轻代可以最大化吞吐量,但这样做是以占用空间,及时性和停顿时间为代价的。年轻代的停顿可以通过使用小型年轻代来最小化,如此会影响吞吐量。一世代的大小不会影响另一代的收集频率和停顿时间。
没有一种正确的方法可以选择世代大小。最佳选择取决于应用程序使用内存的方式以及用户要求。因此,虚拟机对垃圾收集器的选择并不总是最佳的。
使用应用程序特定的目标值是测量吞吐量和占用空间最好方法。例如,可以使用客户端负载生成器测试Web服务器的吞吐量,而在Solaris操作系统上可以使用pmap命令测量服务器的占用空间。通过检查虚拟机本身的诊断输出,可以轻松估计由于垃圾回收导致的停顿。
命令行选项-verbose:gc会在每次收集中打印有关堆和垃圾回收的信息。例如,从大型服务器应用程序输出:
[GC 325407K-> 83000K(776768K),0.2300771 secs]
[GC 325816K-> 83372K(776768K),0.2454258 secs]
[Full GC 267628K-> 83769K(776768K),1.8479984 secs]
输出显示两个minor收集,和一个major收集。箭头前后的数字(例如,第一行的325407K->83000K)分别表示垃圾收集之前和之后的活动对象的组合大小。在minor收集之后,大小包括一些垃圾(不再存活)但无法回收的对象。这些对象要么包含在老年代中,要么被老年代引用。
括号中的数字(例如,(776768K))是堆可用大小:可用于Java对象的空间量,而无需从操作系统请求更多内存。请注意,此数字仅包括一个幸存者空间。除了在垃圾收集期间,在任何给定时间仅使用一个幸存者空间来存储对象。
该行的最后一项(例如0.2300771 secs)表示执行收集所花费的时间,在这种情况下大约是四分之一秒。
第三行中major收集的格式类似。
注意: 产生的输出格式-verbose:gc在未来版本中可能会有所变化。
命令行选项-XX:+PrintGCDetails会输出有关要打印的其他信息,此处显示了使用串行垃圾收集器的输出示例。
[GC [DefNew:64575K-> 959K(64576K),0.0457646秒] 196016K-> 133633K(261184K),0.0459067秒]
这表明minor收集恢复了约98%的年轻一代,DefNew: 64575K->959K(64576K)并且0.0457646 secs(约45毫秒)。
整个堆的使用减少到大约51%(196016K->133633K(261184K)),并且收集(在年轻代收集)有一些轻微的额外开销,如最终时间所示0.0459067 secs。
注意: 产生的输出格式-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秒。minor收集大约在同一时间开始。此外,还显示了由老年代描绘的主要收集信息。老年代使用量减少到约10%(18154K->2311K(24576K))并花费0.1290354 secs(约130毫秒)。
许多参数会影响世代的大小。图4-1“堆参数”说明了堆中的已保留的空间和虚拟空间之间的差异。在初始化虚拟机时,将保留堆的整个空间。可以使用-Xmx选项指定保留空间的大小。如果-Xms参数的值小于-Xmx参数的值,则不会立即将所有保留的空间提交给虚拟机。未保留的空间在此图中标记为“虚拟”。堆的不同部分(老年代和年轻代)可以根据需要增长到虚拟空间的极限。
一些参数是堆的一部分与另一部分的比率。例如,参数NewRatio表示老年代与年轻代的相对大小。
图4-1堆参数
“图4-1堆参数”的说明
总堆
以下关于堆和默认堆大小的增长和收缩的讨论不适用于并行收集器。(见在确定世代大小并行收集Ergonomics节,有关于并行收集器调整堆大小和堆缺省大小的详细信息。)然而,控制堆的总大小和世代的尺寸的参数确实适用于并行收集。
影响垃圾收集性能的最重要因素是总可用内存。由于收集发生在内存被填满时,因此吞吐量与可用内存量成反比。
默认情况下,虚拟机在每次收集中增大或缩小堆,以尝试将可用空间的比例保持在特定范围内以保存活动的对象。此目标范围设定为由参数百分比-XX:MinHeapFreeRatio=和-XX:MaxHeapFreeRatio=,并且总尺寸下限是-Xms及上限是-Xmx。表4-1“64位Solaris操作系统的缺省参数”中显示了64位Solaris操作系统(SPARC Platform Edition)的缺省参数:
表4-1 64位Solaris操作系统的缺省参数
参数 | 默认值 |
---|---|
MinHeapFreeRatio | 40 |
MaxHeapFreeRatio | 70 |
-Xms | 6656k |
-Xmx | calculated |
使用这些参数,如果一代中的可用空间百分比低于40%,则生成将扩展为维持40%的可用空间,直到生成的最大允许大小。类似地,如果自由空间超过70%,那么将收缩这一代,使得只有70%的空间是免费的,受制于该代的最小尺寸。
如表4-1“64位Solaris操作系统的默认参数”中所述,默认的最大堆大小是由JVM计算的值。Java SE中用于并行收集器和服务器JVM的计算现在用于所有垃圾收集器。部分计算是32位平台和64位平台上最大堆大小的上限。请参阅并行收集器中的默认堆大小部分。客户端JVM有类似的计算,这导致最大堆大小小于服务器JVM。
以下是有关服务器应用程序的堆大小的一般准则:
• 除非您遇到停顿问题,否则请尽量为虚拟机授予尽可能多的内存。默认大小通常太小。
• 将Xms和-Xmx设置为相同的值可关闭虚拟机的大小决策,以提高可预测性。但是,如果你做出糟糕的选择,则虚拟机无法补救。
• 通常,在增加处理器数量时增加内存,因为分配可以并行化。
除了总可用内存,影响垃圾收集性能的第二个最有影响的因素是专用于年轻代堆的比例。年轻代越大,发生的minor收集就越少。但是,对于有限的堆大小,较大的年轻代意味着较小的老年代,这将增加major收集的频率。最佳选择取决于应用程序分配的对象的生命周期分布。
默认情况下,年轻代的大小由参数NewRatio控制。例如,设置-XX:NewRatio=3意味着年轻和老年代之间的比例是1:3。换句话说,Eden和幸存者空间的组合大小将是总堆大小的四分之一。
参数NewSize和MaxNewSize限制年轻代的大小。将这些设置为相同的值可以固定年轻代的大小,就像设置-Xms和-Xmx为相同的值一样。以这种更细的粒度调优年轻代,比用NewRatio以整数倍调整更有帮助。
您可以使用该参数SurvivorRatio来调整幸存者空间的大小,但这通常对性能不重要。例如,-XX:SurvivorRatio=6将eden和幸存者空间之间的比率设置为1:6。换句话说,每个幸存者空间将是eden大小的六分之一,因此是年轻代的八分之一(不是七分之一,因为有两个幸存者空间)。
如果幸存者空间太小,则复制收集会直接溢出到老年代。如果幸存者空间太大,它们将毫无用处。在每次垃圾收集时,虚拟机会选择一个阈值编号,该阈值编号是对象在终止之前可以复制的次数。选择此阈值是为了使幸存者空间保持半满。命令行选项-XX:+PrintTenuringDistribution(不是所有的垃圾收集器都可用)可用于显示此阈值和新世代中对象的年龄。它对于观察应用程序的生命周期分布也很有用。
表4-2“Survivor Space Sizing的默认参数值”提供了64位Solaris的默认值:
表4-2 Survivor Space Sizing的默认参数值
参数 服务器JVM默认值
NewRatio | 2 |
---|---|
NewSize | 1310M |
MaxNewSize | 不限 |
SurvivorRatio | 8 |
年轻代的最大值将根据总堆的最大值和NewRatio参数的值来计算。MaxNewSize参数的“不受限制”默认值意味着计算值不受MaxNewSize限制,除非MaxNewSize在命令行中指定了值。
以下是服务器应用程序的一般准则:
• 首先确定您可以为虚拟机提供的最大堆大小。然后根据年轻代尺寸绘制性能指标,以找到最佳设置。
o 请注意,最大堆大小应始终小于计算机上安装的内存量,以避免过多的页面错误(page faults)和抖动。
• 如果总堆大小是固定的,那么增加年轻代的大小会减少老年代大小。保持老年代足够大,以容纳应用程序在任何给定时间使用的所有实时数据,加上一些余量空间(10到20%或更多)。
• 根据先前对老年代的约束:
o 给年轻代留下充足的内存。
o 随着处理器数量的增加,增加年轻代的大小,因为分配可以并行化。
Java HotSpot VM包括三种不同类型的收集器,每种收集器具有不同的性能特征。
• 串行收集器使用单个线程来执行所有垃圾收集工作,这使得它相对有效,因为线程之间没有通信开销。它最适合单处理器机器,因为它无法利用多处理器硬件,尽管它对于具有小数据集(最大约100 MB)的应用程序的多处理器非常有用。默认情况下,在某些硬件和操作系统配置上选择串行收集器,或者可以使用该选项显式启用串行收集器-XX:+UseSerialGC。
• 并行收集器(也称为吞吐量收集器)并行执行minor收集,这可以显着减少垃圾收集开销。它适用于在多处理器或多线程硬件上运行的具有中型到大型数据集的应用程序。默认情况下,在某些硬件和操作系统配置上选择并行收集器,或者可以使用该选项显式启用并行收集器-XX:+UseParallelGC。
o 并行压缩是一种使并行收集器能够并行执行major收集的功能。如果没有并行压缩,major收集将使用单个线程执行,这可能会限制可伸缩性。如果-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秒的停顿。
如果推荐的收集器未达到所需性能,请首先尝试调整堆和生成大小以满足所需目标。如果性能仍然不足,那么尝试使用不同的收集器:使用并发收集器来减少停顿时间,并使用并行收集器来提高多处理器硬件的总吞吐量。
并行收集器(这里也称为吞吐量收集器)是类似于串行收集器的新代收集器; 主要区别在于多个线程用于加速垃圾收集。使用命令行选项启用并行收集器-XX:+UseParallelGC。默认情况下,使用此选项,并行执行minor和major收集,以进一步减少垃圾收集开销。
有N个硬件线程,其中N大于8的机器上,并行收集器使用的一个固定分数N作为垃圾收集线程的数量。对于较大的N值,该分数约为5/8。在N值低于8时,使用的数字是N.。在一些选定的平台上,分数降至5/16。可以使用命令行选项(稍后描述)调整垃圾收集器线程的特定数量。在具有一个处理器的主机上,由于并行执行所需的开销(例如,同步),并行收集器的性能可能不如串行收集器。但是,在运行具有中型到大型堆的应用程序时,它通常在具有两个处理器的计算机上的性能优于串行收集器,并且当有两个以上处理器可用时,通常比串行收集器表现更好。
可以使用命令行选项控制垃圾收集器线程的数量 -XX:ParallelGCThreads=。如果使用命令行选项对堆进行显式调整,那么并行收集器的良好性能所需的堆大小与串行收集器所需的大小相同。但是,启用并行收集器应使收集停顿时间更短。因为多个垃圾收集器线程正在参与minor收集,所以在从年轻代升级到老年代期间,可能存在一些碎片。minor收集中每个垃圾收集线程保留一部分用于升级的老年代,并且为这些“升级缓冲区” 划分空间可能导致碎片效应。减少垃圾收集器线程的数量并增加老年代的大小可以减少这种碎片效应。
如前所述,并行收集器中世代的排列是不同的。如图6-1“并行收集器中的生成安排”所示:
图6-1并行收集器中的世代排列
“图6-1并行收集器中的世代排列”的描述
默认情况下,在服务器级计算机上选择并行收集器。此外,并行收集器使用自动调整方法,允许您指定特定行为,而不是世代大小和其他低级调整细节。您可以指定最大垃圾收集停顿时间,吞吐量和占用空间(堆大小)。
• 最大垃圾收集停顿时间:使用命令行选项指定最大停顿时间目标-XX:MaxGCPauseMillis=。这被解释为需要停顿毫秒或更短的停顿时间的提示; 默认情况下,没有最大停顿时间目标。如果指定了停顿时间目标,则会调整堆大小和与垃圾回收相关的其他参数,以尝试使垃圾回收停顿时间短于指定值。这些调整可能导致垃圾收集器降低应用程序的总吞吐量,并且不能始终满足所需的停顿时间目标。
• 吞吐量:吞吐量目标是根据垃圾收集所花费的时间与垃圾收集之外所花费的时间(称为应用程序时间)来衡量的。目标由命令行选项指定,该选项-XX:GCTimeRatio=设置垃圾收集时间与应用程序时间的比率1 / (1 + )。
例如,-XX:GCTimeRatio=19将目标设置为垃圾收集中总时间的1/20或5%。默认值为99,垃圾回收的则目标为1%。
• 占用空间:最大堆占用空间使用选项指定-Xmx。此外满足其他目标,收集器有一个隐含的目标,即最小化堆的大小。
目标按以下顺序解决:
首先满足最大停顿时间目标。只有在满足后才能解决吞吐量目标。同样,只有在满足前两个目标之后才考虑占用空间目标。
收集器保留的平均停顿时间等统计信息将在每个收集的末尾更新,然后确定是否满足目标的测试,并对世代的尺寸进行任何所需的调整。一个例外是,显式垃圾收集(例如,调用System.gc())不保持统计数据和调整世代大小。
增长和缩小世代的尺寸是通过增量来完成的,这些增量是世代的尺寸的固定百分比,以便世代增长减少以达到其期望的尺寸。增长和缩小以不同的速度完成。默认情况下,世代以20%的增量增长,并以5%的增量缩小。年轻代增长百分比由-XX:YoungGenerationSizeIncrement=控制,XX:TenuredGenerationSizeIncrement=命令行选项控制老年代。通过命令行标志调整世代收缩的百分比-XX:AdaptiveSizeDecrementScaleFactor=。如果增长增量为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 -version | grep MaxHeapSize
如果在垃圾收集(GC)中花费了太多时间,那么并行收集器会抛出OutOfMemoryError:如果在垃圾收集中花费了超过98%的总时间并且恢复了不到2%的堆,则抛出OutOfMemoryError。此功能旨在防止应用程序由于堆太小长时间运行,同时没有什么进展。如有必要,可以通过-XX:-UseGCOverheadLimit在命令行中添加选项来禁用此功能。
并行收集器的详细垃圾收集器输出与串行收集器的输出基本相同。
Java Hotspot VM在JDK 8中有两个主要是并发的收集器:
• 并发标记扫描(CMS)收集器:此收集器适用于垃圾收集停顿较短,并可以与垃圾收集共享处理器资源的应用程序。
• Garbage-First垃圾收集器:这种服务器式收集器适用于具有大内存的多处理器机器。它以高概率满足垃圾收集停顿时间目标,同时实现高吞吐量。
大多数并发收集器交换处理器资源(否则可供应用程序使用)以缩短major收集停顿时间。最明显的开销是在收集的并发部分期间使用一个或多个处理器。在N处理器系统上,收集的并发部分将使用可用处理器的K / N,其中1 <= K <= ceiling { N / 4}。(K的精确选择和界限可能会发生变化。)除了在并发阶段使用处理器之外,还会产生额外的开销以实现并发。因此,虽然并发收集器的垃圾收集停顿通常要短得多,但应用程序吞吐量也往往略低于其他收集器。
在具有多个处理内核的计算机上,在收集的并发部分期间处理器可用于应用程序线程,因此并发垃圾收集器线程不会“停顿”应用程序。这通常会导致停顿时间缩短,但应用程序可用的处理器资源也会减少,并且应该会有一些减速,特别是如果应用程序最大限度地使用所有处理内核。随着N的增加,由于并发垃圾收集变得更小会减少处理器资源的使用,并发收集的益处增加。并行模式失败在并发标记扫描(CMS)收集器部分会讨论这种扩展的潜在限制。
因为在并发阶段期间至少有一个处理器用于垃圾收集,所以并发收集器通常不会在单处理器(单核)机器上提供任何好处。但是,CMS(不是G1)有一个单独的模式,可以在只有一个或两个处理器的系统上实现低停顿; 参考增量模式在并发标记扫描(CMS)收集器的详细信息。此功能在Java SE 8中已弃用,可能会在以后的主要版本中删除。
Garbage-First垃圾收集器:http://www.oracle.com/technetwork/java/javase/tech/g1-intro-jsp-135488.html
Garbage-First收集器调优:http://www.oracle.com/technetwork/articles/java/g1gc-1984535.html
并发标记扫描(CMS)收集器专为需要较短垃圾收集停顿,且能够在应用程序运行时与垃圾收集器共享处理器资源的应用程序而设计的。通常,具有相对大的长寿命数据集(大型老年代)并在具有两个或更多处理器的机器上运行的应用程序倾向于从该收集器的使用中受益。但是,对于任何停顿时间要求较低的应用程序,都应考虑使用此收集器。使用命令行选项启用CMS收集器-XX:+UseConcMarkSweepGC。
与其他可用的收集器类似,CMS收集器是世代的,因此会发生minor和major收集。CMS收集器尝试通过使用单独的垃圾收集器线程在执行应用程序线程的同时跟踪可访问的对象来减少由于major收集而导致的停顿时间。在每个major收集周期中,CMS收集器会在收集开始时停顿所有应用程序线程,并到收集的中间位置再次停顿。第二次停顿往往是较长。在两个停顿期间,多个线程用于执行收集工作。
CMS收集器使用一个或多个与应用程序线程同时运行的垃圾收集器线程,目标是在堆满之前完成老年代的收集。如前所述,在正常操作中,CMS收集器在应用程序线程仍在运行时执行大部分跟踪和扫描工作,因此应用程序线程只能看到短暂的停顿。但是,如果CMS收集器无法在老年代填满之前完成对无法访问的对象的回收,或者如果无法使用老年代中的可用空闲块满足分配,则应用程序将停顿,直到收集完成。
无法同时完成收集称为并发模式失败,这时需要调整CMS收集器的参数。如果并发收集被显式垃圾收集(System.gc())中断或者为诊断工具提供信息所需的垃圾收集,则报告并发模式中断。
如果在垃圾收集中花费了太多时间,CMS收集器会抛出OutOfMemoryError:如果超过98%的总时间用于垃圾收集,并且回收的堆少于2%,则抛出一个OutOfMemoryError。此功能旨在防止应用程序长时间运行,同时由于堆太小而进展很少或没有。如有必要,可以通过-XX:-UseGCOverheadLimit在命令行中添加选项来禁用此功能。
该策略与并行收集器中的策略相同,只是执行并发收集所花费的时间不计入98%的时间限制。换句话说,只有在应用程序停止时执行的收集才会计入过多的GC时间。此类收集通常是由于并发模式失败或显式收集请求(例如,调用System.gc)造成的。
与Java HotSpot VM中的所有其他收集器一样,CMS收集器是一个跟踪收集器,至少可以识别堆中的所有可访问对象。在Richard Jones和Rafael D. Lins的出版物“ Garbage Collection:Algorithms for Automated Dynamic Memory”中,它是一个增量更新收集器。由于应用程序线程和垃圾收集器线程在major收集期间并发运行,因此垃圾收集器线程跟踪的对象随后可能在收集过程结束时变得不可访问。这些尚未回收的无法到达的对象被称为浮动垃圾。
浮动垃圾量取决于并发收集周期的持续时间以及被应用程序引用(reference)更新的频率(也称为突变)。此外,由于年轻代和老年代是独立收集的,因此每世代都是另一方的root源。一个粗略的指导方针是,尝试将老年代增加20%来保存浮动垃圾。这些浮动垃圾会在下一次的收集周期期间收集。
CMS收集器在并发收集周期中停顿两次应用程序。第一个停顿是将根中可直接访问的对象(例如,来自应用程序线程栈和寄存器的对象引用,静态对象等)以及堆中的其他位置(例如,年轻代)标记为活动对象。第一次停顿被称为初始标记停顿。第二次停顿在并发跟踪阶段结束时,查找并发跟踪遗漏的对象。这些对象是因为在CMS收集器完成跟踪之后,应用程序线程更新了对象的引用而致的。第二次停顿被称为备注停顿。
可达对象图的并发跟踪发生在初始标记停顿和备注停顿之间。在该并发跟踪阶段期间,一个或多个并发垃圾收集器线程可能正在使用本供应用程序使用的处理器资源。因此,即使应用程序线程未停顿,应用程序吞吐量也会相应下降。备注停顿后,并发扫描阶段会收集标识为无法访问的对象。收集周期完成后,CMS收集器将处于等待,几乎不消耗任何计算资源,直到下一个major收集周期开始。
使用串行收集器时,只要老年代满就会发生major收集,并且在收集完成之前时停止所有应用程序线程。相反,并发集合的开始必须定时,以便收集可以在老年代变满之前完成; 否则,由于并发模式失败,应用程序将会有更长的停顿。
有几种方法可以启动并发收集。
CMS收集器根据最近的历史记录,维护对老年代用尽之前剩余时间的估计,以及并发收集周期所需的时间。使用这些动态估计,开始并发收集周期,目的是在老年代用尽之前完成收集周期。这些估计值是为了安全起见而记录的,因为并发模式失败可能非常昂贵。
如果老年代的占用率超过初始占用率(老年代的百分比),则也开始并发收集。此初始占用阈值的默认值约为92%,但该值可能会随发行版的不同而有所变化。可以使用命令行选项手动调整此值-XX:CMSInitiatingOccupancyFraction=,其中是老年大小的整数百分比(0到100)。
年轻代收集和老年代收集的停顿独立发生。它们不重叠,但可能快速连续发生,使得来自一个收集的停顿,紧接着来自另一个收集,可以看起来是单个较长的停顿。为避免这种情况,CMS收集器会尝试大致在上一次和下一次年轻代停顿之间中途安排备注停顿。目前没有对初始标记停顿进行此调度,因为它通常比备注停顿短得多。
请注意,增量模式在Java SE 8中已弃用,可能会在将来的主要版本中删除。
CMS收集器可以在以递增方式完成并发阶段的模式中使用。回想一下,在并发阶段,垃圾收集器线程正在使用一个或多个处理器。增量模式旨在通过周期性地停止并发阶段以使处理器返给应用程序,来减少长并发阶段的影响。这种模式,在此称为i-cms,将收集器并发完成的工作划分为在年轻代收集之间的小块时间。当需要CMS收集器提供的低停顿时间的应用程序,并具有少量处理器(例如,1或2)的计算机上运行时,此功能非常有用。
并发收集周期通常包括以下步骤:
• 停止所有应用程序线程,标识可从根访问的对象集,然后恢复所有应用程序线程。
• 在应用程序线程执行时,使用一个或多个处理器同时跟踪可访问的对象图。
• 使用一个处理器同时回溯自上一步中的跟踪以来修改的对象图的部分。
• 停止所有应用程序线程并回溯自上次检查后可能已修改的根和对象图的部分,然后恢复所有应用程序线程。
• 并发的,使用一个处理器将无法访问的对象扫描到用于分配的空闲列表。
• 并发的,使用一个处理器调整堆大小,并为下一个收集周期准备数据结构。
通常,CMS收集器在整个并发跟踪阶段使用一个或多个处理器,而不会主动放弃它们。类似地,一个处理器用于整个并发扫描阶段,同样不放弃它。对于具有响应时间限制的应用程序而言,这种开销可能会造成太多中断,尤其是在只有一个或两个处理器的系统上运行时。增量模式通过将并发阶段分解为短暂的活动突发来解决此问题,这些活动计划在minor停顿之间发生。
i-cms模式使用占空比来控制CMS收集器在主动放弃处理器之前允许执行的工作量。该占空比是年轻代收集之间的时间百分比。i-cms模式可以根据应用程序的行为自动计算占空比(推荐的方法,称为自动调步),或者可以在命令行上将占空比设置为固定值。
表8-1“i-cms的命令行选项”列出了控制i-cms模式的命令行选项。“推荐选项”部分提供了一组初始选项。
表8-1 i-cms的命令行选项
选项 | 描述 | 默认值,Java SE 5和更早版本 | 默认值,Java SE 6和更高版本 |
---|---|---|---|
-XX:+CMSIncrementalMode | 启用增量模式。请注意,还必须启用CMS收集器(使用-XX:+UseConcMarkSweepGC)才能使此选项生效。 | disabled | disabled |
-XX:+CMSIncrementalPacing | 启用自动调步。根据JVM运行时收集的统计信息自动调整增量模式占空比。 | disabled | disabled |
-XX:CMSIncrementalDutyCycle= | 允许CMS收集器运行的minor收集之间的时间百分比(0到100)。如果CMSIncrementalPacing启用,那么这只是初始值。 | 50 | 10 |
-XX:CMSIncrementalDutyCycleMin= | CMSIncrementalPacing启用时占空比下限的百分比(0到100)。 10 0 | ||
-XX:CMSIncrementalSafetyFactor= | 用于在计算占空比时添加保守性的百分比(0到100) | 10 | 10 |
-XX:CMSIncrementalOffset= | 增量模式占空比在minor收集之间的时间段内向右移动的百分比(0到100)。 | 0 | 0 |
-XX:CMSExpAvgFactor= | 在计算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自动起搏功能故障”中的步骤,一次尝试一个。
表8-2排除i-cms自动调步功能故障
步骤 | 选项 |
---|---|
1.增加安全系数。 | -XX:CMSIncrementalSafetyFactor= |
2.增加最小占空比。 | -XX:CMSIncrementalDutyCycleMin= |
3.禁用自动调步并使用固定的占空比。 | -XX:-CMSIncrementalPacing -XX:CMSIncrementalDutyCycle= |
例8-1“CMS收集器的输出”是CMS收集器的输出,带有选项-verbose:gc和-XX:+PrintGCDetails,删除了一些小细节。请注意,CMS收集器的输出中散布着minor收集的输出。
通常,许多minor收集在并发收集周期中发生。CMS-initial-mark表示并发收集周期的开始,CMS-concurrent-mark表示并发标记阶段的结束,CMS-concurrent-sweep表示并发扫描阶段的结束。之前未讨论的是由CMS-concurrent-preclean表示的预清洁阶段。预清理代表可以同时完成的工作,以准备备注阶段CMS-remark。CMS-concurrent-reset表示最后阶段,并为下一个并发集合做准备。
示例8-1 CMS收集器的输出
[GC [1 CMS-initial-mark:13991K(20288K)] 14103K(22400K),0.0023781 secs]
[GC [DefNew:2112K-> 64K(2112K),0.0837052秒] 16103K-> 15476K(22400K),0.0838519秒]
...
[GC [DefNew:2077K-> 63K(2112K),0.0126205 secs] 17552K-> 15855K(22400K),0.0127482 secs]
[CMS-并发标记:0.267 / 0.374秒]
[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秒] 18177K-> 17382K(22400K),0.0718204秒]
[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秒]
[GC [DefNew:2048K-> 0K(2112K),0.0013347 secs] 17527K-> 15479K(27912K),0.0014231 secs]
[CMS-concurrent-reset:0.016 / 0.016秒]
[GC [DefNew:2048K-> 1K(2112K),0.0013936秒] 17527K-> 15479K(27912K),0.0014814秒
]
初始标记停顿通常相对于minor收集停顿时间较短。并发阶段(并发标记,并发预扫描和并发扫描)通常持续时间明显长于minor集合停顿,如例8-1“CMS收集器的输出”所示。但请注意,在这些并发阶段期间不会停顿应用程序。备注停顿的长度通常与minor收集相当。备注停顿受某些应用程序特征的影响(例如,高速率的对象修改可以增加此停顿)以及自上次minor收集以来的时间(例如,年轻代中的更多对象可能会增加此停顿)。
Garbage-First(G1)垃圾收集器是服务器式垃圾收集器,针对具有大内存的多处理器机器。它尝试以高概率满足垃圾收集(GC)停顿时间目标,同时实现高吞吐量。全堆操作(例如全局标记)与应用程序线程同时执行。这可以防止与堆或实时数据大小成比例的中断。
G1收集器通过多种技术实现了高性能和停顿时间目标。
堆被分区为一组大小相等的堆区域,每个堆区域都是连续的虚拟内存范围。G1执行并发全局标记阶段以确定整个堆中对象的活跃度。在标记阶段完成之后,G1知道哪些区域会基本上是空的。它首先收集这些区域,这通常会产生大量的自由空间。这就是为什么这种垃圾收集方法称为Garbage-First。顾名思义,G1将其收集和压缩活动集中在充满可回收对象(即垃圾)的区域。G1使用停顿预测模型来满足用户定义的停顿时间目标,并根据指定的停顿时间目标选择要收集的区域数。
G1将对象从堆的一个或多个区域复制到堆上的单个区域,并且在此过程中压缩并释放内存。这种操作在多处理器上并行执行,以减少停顿时间并提高吞吐量。因此,随着每次垃圾收集,G1不断努力减少碎片。这超出了以前两种方法的能力。CMS(Concurrent Mark Sweep)垃圾收集不进行压缩。并行压缩仅执行整堆压缩,这会导致相当长的停顿时间。
值得注意的是,G1不是实时收集器。它以高概率但不是绝对确定性满足设定的停顿时间目标。根据以前收集的数据,G1估计在目标时间内可以收集多少个区域。因此,收集器具有收集区域的成本的相当准确的模型,并且它使用该模型来确定在停留在停顿时间目标内时要收集哪些区域和多少区域。
G1的重点是为要求有限GC延迟,并具有大堆的应用程序用户提供解决方案。这意味着堆大小约为6GB或更大,并且稳定且可预测的停顿时间低于0.5秒。
如果应用程序具有以下一个或多个特征,那么正在使用CMS或并行压缩运行的应用程序将从切换到G1中受益。
• 超过50%的Java堆被活动数据占用。
• 对象分配率或升级率差异很大。
• 该应用程序正在经历不希望的长垃圾收集或压缩停顿(超过0.5到1秒)。
G1计划作为并发标记扫描收集器(CMS)的长期替代品。将G1与CMS进行比较揭示了G1将成为更好的解决方案。一个区别是G1是压缩收集器,此外,G1提供比CMS收集器更可预测的垃圾收集停顿,并允许用户指定所需的停顿目标。
与CMS一样,G1专为需要较短GC停顿的应用而设计。
G1将堆分成固定大小的区域(灰色框),如图9-1“堆分区G1”所示。
图9-1 G1的堆分区
“图9-1按G1分类堆”的描述
G1在逻辑意义上是世代的。一组空区域被指定为逻辑年轻代。在图中,年轻一代是浅蓝色的。
分配的逻辑是,当年轻代充满时,这组地区被垃圾收集(一个年轻代收集)。在某些情况下,年轻区域之外的区域(深蓝色的旧区域)可以同时进行垃圾收集。这被称为混合收集。在该图中,收集的区域用红色框标记。该图说明了一个混合收集,因为正在收集年轻地区和旧地区。垃圾收集是一个压缩集合,它将活动对象复制到选定的最初为空的区域。基于幸存物体的年龄,可以将物体复制到幸存者区域(标记为“S”)或旧区域(未具体示出)。标有“H”的区域包含大于半个区域且经过特殊处理的巨大物体; 请参见
Garbage-First垃圾收集器中的Humongous对象和Humongous分配。
与CMS一样,G1收集器在应用程序继续运行时运行其收集的一部分,并且存在一个风险,即应用程序分配对象的速度快于垃圾收集器可以恢复可用空间。与并发标记扫描(CMS)收集器的并发模式失败类似。在G1中,G1正在将活动数据从一个区域(撤离)复制到另一个区域时发生失败(Java堆耗尽)。复制以压缩活动数据。如果在收集垃圾期间无法找到空闲区域,则会发生分配失败(因为没有空间来分配来自撤离区域的活动对象),这时会stop-the-world( STW)进行全收集。
对象可能在G1收集期间死亡而不被收集。G1使用一种名为snapshot-at-the-beginning(SATB)的技术来保证垃圾收集器找到所有活动对象。SATB声明在并发标记开始时存在的任何对象(整个堆上的标记)被认为是用于收集目的的活动对象。SATB允许浮动垃圾的方式类似于CMS增量更新。
G1停顿应用程序以将活动对象复制到新区域。这些停顿可以是只收集年轻区域的年轻收集停顿,也可以是年轻人和旧区域被撤离的混合收集停顿。与CMS一样,在应用程序停止时,有一个最终标记或备注停顿以完成标记。虽然CMS也有初始标记停顿,但G1会将初始标记工作作为撤离停顿的一部分。G1在收集的末尾有一个清理阶段,部分是STW,部分是并发的。清理阶段的STW部分识别空区域并确定作为下一个集合的候选的旧区域。
如果垃圾收集器不收集整个堆(增量集合),则垃圾收集器需要知道从堆的未收集部分到正在收集的堆的部分中的指针。这通常用于分代垃圾收集器,其中堆的未收集部分通常是老年代,并且堆的收集部分是年轻代。用于保存此信息的数据结构(老年代指针指向年轻代对象)是一个记忆集。卡表是一个特定类型的记忆集。Java HotSpot VM使用字节数组作为卡表。每个字节称为一个卡。卡对应于堆中的一系列地址。Dirtying一个卡意味着将字节值更改为脏值,脏值可能包含一个新指针,该指针是卡所包含的从老年代到年轻代的地址范围。
处理一个卡意味着查看卡,以查看是否存在旧代到年轻代指针并且可能对该信息做某事,例如将其转移到另一数据结构。
G1具有并发标记阶段,标记从应用程序中找到的活动对象。并发标记从撤离停顿(初始标记工作完成)结束延伸到备注。并发清理阶段将收集清空的区域添加到空闲区域列表中,并清除这些区域的记忆集。此外,根据需要运行并发清洗线程处理已被应用程序写入弄脏并且可能具有跨区域引用的卡表条目。
如前所述,年轻和老年区域会被混合收集。为了收集老年区域,G1对堆中的活动对象进行了完整标记。这种标记是通过并行标记阶段完成的。当整个Java堆的占用率达到参数值InitiatingHeapOccupancyPercent时,将启动并发标记阶段。使用命令行选项设置此参数的值-XX:InitiatingHeapOccupancyPercent=。默认值为45。
使用标志MaxGCPauseMillis设置G1的停顿时间目标。G1使用预测模型来确定在该目标停顿时间内可以完成多少垃圾收集工作。在收集的最后,G1选择要在下一个收集区域(收集组)。收集组将包含年轻区域(其大小的总和决定了逻辑年轻代的大小)。G1控制GC停顿的长度部分是通过选择收集组中的年轻区域的数量来实现的。您可以像在其他垃圾收集器中一样在命令行上指定年轻代的大小,但这样做可能会妨碍G1达到目标停顿时间的能力。除停顿时间目标外,您还可以指定停顿时间段的长度。您可以指定此时间跨度的最小mutator使用量(GCPauseIntervalMillis)以及停顿时间目标。MaxGCPauseMillis默认值为200毫秒。GCPauseIntervalMillis的默认值(0)相当于时间间隔上没有要求。
本节介绍如何调整和调优Garbage-First垃圾收集器(G1 GC)以进行评估,分析和性能。
如Garbage-First垃圾收集器一节所述,G1 GC是区域化和世代垃圾收集器,这意味着Java对象堆(heap)被分成许多大小相等的区域。启动时,Java虚拟机(JVM)设置区域大小。区域大小可以从1 MB到32 MB不等,具体取决于堆大小,不超过2048个区域。eden,幸存者和老年代是这些区域的逻辑集合,并不是连续的。
G1 GC尝试满足停顿时间目标(软实时)。在年轻代收集期间,G1 GC调整其年轻代(eden和幸存者大小)以满足软实时目标。请参阅章节停顿和停顿时间目标在Garbage-First垃圾收集有关信息,了解G1 GC为什么需要停顿,以及如何设置停顿时间目标。
在混合收集期间,G1 GC根据混合垃圾收集的目标数量,每个区域中活动对象的百分比,以及总体可接受的堆废弃百分比,来调整老年代区域的收集数量。
G1 GC通过将活动对象从一组或多组区域(称为收集集(CSet))增量并行复制到一个或多个不同的新区域来减少堆碎片,以实现压缩。目标是尽可能多地回收堆空间,从包含最多可回收空间的区域开始,同时尝试不超过停顿时间目标(Garbage first)。
G1 GC使用独立记忆集(RSets)来跟踪对区域的引用。独立RSet支持并行和独立的区域收集,因为只有区域RSet必须为引用被扫描以进入该区域,而不是整个堆。G1 GC使用写后屏障(post-write barrier)来记录对堆的更改并更新RSets。
除了疏散停顿(请参见Garbage-First垃圾收集的分配(疏散)失败)组成stop-the-world(STW)年轻和混合垃圾回收,G1 GC还具有平行,并发,和多阶段标识周期。G1 GC使用snapshot-at-the-beginning(SATB)算法,该算法在标记周期开始时逻辑上获取堆中活动对象集的快照。活动对象集还包括自标记周期开始以来分配的对象。G1 GC标记算法使用预写屏障(pre-write barrier)来记录和标记逻辑快照的对象。
G1 GC满足大多数来自添加到eden区域集的区域的分配请求。在年轻代的垃圾收集过程中,G1 GC从之前的垃圾收集中收集eden区域和幸存区域。来自eden和幸存者区域的活动对象被复制或撤离到一组新的区域。特定对象的目标区域取决于对象的年龄; 对象的年龄足够老后将其疏散(升级)到老年代地区;否则,该对象会撤离到幸存者区域,并将包括在下一个年轻或混合垃圾收集的CSet中。
成功完成并发标记周期后,G1 GC将从执行年轻垃圾收集切换到执行混合垃圾收集。在混合垃圾收集中,G1 GC可选地将一些老年代区域添加到将被收集的eden区域和幸存区域中。添加的老年代区域的确切数量由标志控制(请参阅“驯服混合垃圾收集器”中的“建议”部分)。G1 GC收集足够数量的老年代区域(通过多个混合垃圾收集)后,G1将恢复执行年轻代垃圾收集,直到下一个标记周期完成。
标记周期包括以下阶段:
• 初始标记阶段:G1 GC在此阶段标记根。这个阶段在正常(STW)年轻代垃圾收集上捎带执行。
• 根区扫描阶段:G1 GC扫描在初始标记阶段标记的幸存区域,以引用老年代,并标记引用的对象。此阶段与应用程序同时运行(而不是STW),并且必须在下一个STW年轻代垃圾收集开始之前完成。
• 并发标记阶段:G1 GC在整个堆中查找可到达(活动)对象。此阶段与应用程序同时发生,并且可以被STW年轻代垃圾收集中断。
• 备注阶段:此阶段是STW收集,帮助完成标记周期。G1 GC排出SATB缓冲区,跟踪未访问的活动对象,并执行引用处理。
• 清理阶段:在最后阶段,G1 GC执行accounting和RSet清理的STW操作。在accounting期间,G1 GC识别完全自由区域和混合垃圾收集候选者。清理阶段在重置并将空区域返回到空闲列表时,是部分并发的。
G1 GC是一个自适应垃圾收集器,默认设置无需修改即可高效工作。表10-1“G1垃圾收集器的重要选项的默认值”列出了Java HotSpot VM,build 24中的重要选项及其默认值列表。您可以通过输入选项来调整和调整G1 GC以满足应用程序性能需求表10-1“G1垃圾收集器的重要选项的默认值”,其中JVM命令行上的设置已更改。
表10-1 G1垃圾收集器重要选项的缺省值
选项和默认值 | 选项 |
---|---|
-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。如果有超过八个逻辑处理器,则将n的值设置为逻辑处理器的大约5/8。这在大多数情况下都有效,除了较大的SPARC系统,其中n的值可以是逻辑处理器的大约5/16。 |
-XX:ConcGCThreads=n | 设置并行标记线程的数量。设置n为并行垃圾回 |
-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将尝试满足它。作为副作用,吞吐量可能受到影响。请参阅本节停顿Garbage-First垃圾收集器中的时间目标以获取更多信息。
• 驯服混合垃圾收集:调整混合垃圾收集时,请尝试以下选项。有关这些选项的信息,请参阅重要默认值部分:
o -XX:InitiatingHeapOccupancyPercent:用于更改标记阈值。
o -XX:G1MixedGCLiveThresholdPercent和-XX:G1HeapWastePercent:用于更改混合垃圾收集决策。
o -XX:G1MixedGCCountTarget和-XX:G1OldCSetRegionThresholdPercent:用于调整老年区域的CSet。
当您在日志中看到空间溢出或空间耗尽的消息时,表明G1 GC没有足够的内存用于幸存者或(和)升级的对象,因为已经达到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,任何超过区域大小一半的对象都被视为一个大对象。这样的对象直接在老年代的大区域。这些大区域是一个连续的区域。StartsHumongous标记连续集的开始,ContinuesHumongous标记连续集的延续。
在分配任何大区域之前,检查标记阈值,如有必要,启动并发周期。
在清理阶段期间以及在完整的垃圾收集循环期间,在标记循环结束时释放死的大对象。
为了减少复制开销,任何疏散停顿都不包括大对象。完整的垃圾收集周期可以压缩大对象。
因为每个单独的一组StartsHumongous和ContinuesHumongous区域仅包含一个大对象,所以大对象的末端与对象跨越的最后一个区域的末尾之间的空间未被使用。对于仅略大于堆区域倍数大小的对象,此未使用的空间可能导致堆碎片化。
如果你看到由于大对象分配而启动的背靠背并发周期,并且如果这样的分配正在破坏你的老年代,那么增加-XX:G1HeapRegionSize值,这样之后以前的大对象不再是大的,并且将遵循常规分配路径的值。
本节介绍影响垃圾收集的其他情况。
某些应用程序通过使用finalization和weak,soft或phantom引用与垃圾收集进行交互。这些功能可以在Java编程语言级别创建性能组件。一个例子是依靠finalization来关闭文件描述符,这使得外部资源(描述符)依赖于垃圾收集的快速性。依靠垃圾收集来管理内存以外的资源几乎总是一个坏主意。
前言中的相关文档部分包括一篇文章,深入讨论了最终确定的一些缺陷和避免它们的技术。
应用程序与垃圾收集交互的另一种方法是通过显式调用垃圾收集System.gc()。这种强制执行major收集可能是没有必要的(例如,当minor收集已经足够时),因此通常应该避免。显式垃圾收集的性能影响可以通过使用标志-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=,它指定软引用为堆中可用空间的每兆字节保持活动(直到不再可以访问)的毫秒数(ms)。默认值为每兆字节1000毫秒,这意味着对于堆中可用空间的每兆字节,软引用将(最后一个强引用已经被收集之后)存活1秒钟。这是一个近似数字,因为软件引用仅在垃圾收集期间被清除,这可能偶尔发生。
Java类在Java Hotspot VM中具有一个内部表示,并称为类元数据。在以前的Java Hotspot VM版本中,类元数据是在所谓的永久代中分配的。在JDK 8中,永久代被删除,类元数据在本机内存中分配。默认情况下,可用于类元数据的本机内存量是无限制的。使用该选项MaxMetaspaceSize可以为用于类元数据的本机内存量设置上限。
Java Hotspot VM显式管理用于元数据的空间。从操作系统请求空间,然后分成块。类加载器从其块中为元数据分配空间(块被绑定到特定的类加载器)。为类加载器卸载类时,其块将被回收以供重用或返回到操作系统。元数据使用分配的空间mmap,而不是malloc。
如果UseCompressedOops打开并使用UseCompressedClassesPointers,则将两个逻辑上不同的本机内存区域用于类元数据。UseCompressedClassPointers使用32位偏移量来表示64位进程中的类指针,就像UseCompressedOopsJava对象引用一样。为这些压缩类指针(32位偏移)分配区域。CompressedClassSpaceSize默认情况下,区域的大小可以设置为1千兆字节(GB)。压缩类指针的空间保留为mmap初始化时分配的空间,并根据需要提交。将MaxMetaspaceSize适用于压缩类空间之和为其他类的元数据的空间。
卸载相应的Java类时,将释放类元数据。由于垃圾收集而卸载Java类,并且可能会导致垃圾收集以卸载类并释放类元数据。当为类元数据提交的空间达到一定水平(高水位线)时,会引发垃圾收集。在垃圾收集之后,可以根据从类元数据中释放的空间量来升高或降低高水位标记。将提高高水位标记,以免过早引起另一次垃圾收集。高水位标记最初设置为命令行选项的值MetaspaceSize。这是上升或下降基础上,选择MaxMetaspaceFreeRatio和MinMetaspaceFreeRatio。如果类元数据的可用空间占类元数据总承诺空间的百分比大于MaxMetaspaceFreeRatio,则高水位标记将降低。如果小于MinMetaspaceFreeRatio,则高水位标记将被提升。
为选项指定更高的值,MetaspaceSize以避免为类元数据引发早期垃圾收集。为应用程序分配的类元数据量取决于应用程序,并且不存选择MetaspaceSize的一般准则。默认MetaspaceSize大小取决于平台,范围从12 MB到大约20 MB。
有关用于元数据的空间的信息包含在堆的打印输出中。典型输出如例11-1“典型堆打印输出”所示。
例11-1典型的堆打印输出
Heap
PSYoungGen 总计10752K,使用4419K
[0xffffffff6ac00000,0xffffffff6b800000,0xffffffff6b800000)
Eden space 9216K,使用率47%
[0xffffffff6ac00000,0xffffffff6b050d68,0xffffffff6b500000)
frame space 1536K,使用0%
[0xffffffff6b680000,0xffffffff6b680000,0xffffffff6b800000)
space 1536K,使用0%
[0xffffffff6b500000,0xffffffff6b500000,0xffffffff6b680000)
ParOldGen 总计20480K,使用20011K
[0xffffffff69800000,0xffffffff6ac00000,0xffffffff6ac00000)
对象空间20480K,使用率97%
[0xffffffff69800000,0xffffffff6ab8add8,0xffffffff6ac00000)
Metaspace使用2425K,capacity 4498K,committed 4864K,reserved 1056768K
类空间 使用262K,capacity 386K,committed 512K,reserved 1048576K
在以Metaspace开头的行中,used值是用于加载类的空间量。capacity值是当前分配的块中可用于元数据的空间。committed值是可用于块的空间量。reserved值是元数据保留(但不一定是已提交)的空间量。以行开头的class space行包含压缩类指针的元数据的相应值。