Java SE 6 HotSpot虚拟机垃圾收集优化
注意:对于Java SE 8,请参阅《Java平台标准版HotSpot虚拟机垃圾收集优化指南》。
|
|
|
|
目录
Java™平台标准版(Java SE™)被用于各种应用程序,从台式机上的小程序到大型服务器上的Web服务。为了支持这种多样化的部署范围,Java HotSpot™虚拟机实现(Java HotSpot™VM)提供了多个垃圾收集器,每个垃圾收集器旨在满足不同的需求。这是满足大型和小型应用程序需求的重要部分。但是,需要高性能的用户,开发人员和管理员要负担额外的步骤,即选择最能满足其需求的垃圾收集器。在J2SE™5.0中,迈出了消除这一负担的重要一步:根据运行应用程序的计算机的类别选择垃圾收集器。
垃圾收集器的这种 更好的选择通常是一种改进,但绝不是总是每种应用程序的最佳选择。具有严格性能目标或其他要求的用户可能需要显式选择垃圾回收器并调整某些参数以实现所需的性能水平。本文档提供了有助于完成这些任务的信息。首先,在串行世界级收集器的上下文中描述了垃圾收集器的一般功能和基本调整选项。然后介绍其他收集器的特定功能以及选择收集器时要考虑的因素。
什么时候选择垃圾收集器很重要?对于某些应用,答案永远是不可能的。也就是说,在存在垃圾收集的情况下,应用程序可以在频率和持续时间适度的暂停下表现良好。但是,对于大类应用程序却不是这种情况,特别是那些具有大量数据(多个千兆字节),许多线程和高事务处理率的应用程序。
Amdahl观察到,大多数工作负载无法完美并行化。某些部分始终是顺序的,不能从并行性中受益。Java™平台也是如此。特别是,Sun Microsystems的J2SE 1.4之前的Java平台虚拟机不支持并行垃圾收集,因此,垃圾收集对多处理器系统的影响相对于其他并行应用程序会增加。
下图模拟了一个理想的系统,除了垃圾回收外,它可以完美地扩展。红线表示应用程序仅在单处理器系统上花费1%的时间进行垃圾回收。这意味着32个处理器系统的吞吐量损失超过20%。在垃圾回收中,只有10%的时间(在单处理器应用程序中,垃圾回收的时间不算太长),当扩展到32个处理器时,将损失超过75%的吞吐量。
这表明,在小型系统上进行开发时,可以忽略的速度问题可能会在扩展到大型系统时成为主要瓶颈。但是,在减少这种瓶颈方面进行小的改进可以提高性能。对于足够大的系统,选择正确的垃圾收集器并在必要时进行调整非常值得。
串行收集器通常适合大多数“小型”应用程序(那些在现代处理器上需要高达100MB的堆)。其他收集器具有额外的开销和/或复杂性,这是专门行为的代价。如果应用程序不需要备用收集器的特殊行为,请使用串行收集器。预计串行收集器不是最佳选择的情况的一个示例是大型应用程序,该应用程序具有大量线程并在具有大量内存和两个或多个处理器的计算机上运行。在此类服务器级计算机上运行应用程序时,默认情况下会选择并行收集器(请参见 下面的人体工程学)。
本文档是使用Solaris™操作系统(SPARC (R) Platform Edition)上的Java SE 6 作为参考而开发的。但是,此处介绍的概念和建议适用于所有受支持的平台,包括Linux,Microsoft Windows和Solaris操作系统(x86平台版本)。此外,尽管某些选项的默认值在每个平台上可能有所不同,但提到的命令行选项在所有受支持的平台上均可用。
J2SE 5.0中引入了此处称为人体工程学的功能 。人体工程学的目标是通过选择以下选项来提供良好的性能,而很少或不需要调整命令行选项。
垃圾收集器,
堆大小
和运行时编译器
在JVM启动时,而不是使用固定的默认值。该选择假定运行应用程序的计算机的类别是有关应用程序特征的提示(即,大型应用程序在大型计算机上运行)。除了这些选择之外,还有一种简化垃圾回收的简化方法。使用并行收集器,用户可以为应用程序指定最大暂停时间和所需吞吐量的目标。这与指定获得良好性能所需的堆大小相反。目的是特别提高使用大堆的大型应用程序的性能。标题为“ 5.0 Java虚拟机中的人体工程学”的文档中介绍了更通用的人体工程学。
本文档中包含的人体工程学功能是并行收集器的自适应大小策略的一部分。这包括用于指定垃圾收集性能目标的选项以及用于微调该性能的其他选项。
J2SE平台的优势之一是,它使开发人员免受内存分配和垃圾回收的复杂性的困扰。但是,一旦垃圾回收成为主要瓶颈,就值得了解此隐藏实现的某些方面。垃圾收集器对应用程序使用对象的方式进行了假设,这些反映在可调整的参数中,可以调整这些参数以提高性能,而又不牺牲抽象的功能。
如果无法从正在运行的程序中的任何指针访问对象,则该对象被视为垃圾。最简单的垃圾回收算法只是简单地遍历每个可到达的对象。剩下的所有对象都被视为垃圾。这种方法所花费的时间与活动对象的数量成正比,这对于维护大量活动数据的大型应用程序是不允许的。
从J2SE 1.2开始,虚拟机合并了许多不同的垃圾收集算法,这些算法使用分代收集进行组合 。当幼稚的垃圾收集检查堆中的每个活动对象时,分代收集利用大多数应用程序的经验观察到的属性,以最大程度地减少回收未使用(“垃圾”)对象所需的工作。这些观察到的特性中最重要的是 弱代假设,该假设指出大多数物体只能存活很短的时间。
下图中的蓝色区域是对象生命周期的典型分布。X轴是对象寿命,以分配的字节为单位。Y轴上的字节数是具有相应生存期的对象中的总字节数。左侧的尖峰表示分配后不久即可回收(即“死亡”)的对象。例如,迭代器对象通常在单个循环期间仍处于活动状态。
有些对象的寿命更长,因此分布向右延伸。例如,通常有一些在初始化时分配的对象,这些对象一直存在,直到进程退出。在这两个极端之间的是在某些中间计算过程中存在的对象,在这里将其视为初始峰右边的肿块。一些应用程序的外观分布非常不同,但是令人惊讶的是,大量应用程序具有这种总体形状。通过关注大多数对象“早逝”这一事实,可以进行有效的收集。
为了针对这种情况进行优化,内存是分代管理的 ,或者是拥有不同年龄对象的内存池。当世代填满时,垃圾收集会在每个世代中发生。绝大多数对象分配在专用于年轻对象(年轻一代)的池中 ,并且大多数对象在那里死亡。当年轻一代填满时,会导致 少量收集只收集年轻一代;不回收其他世代的垃圾。假设弱的世代假设成立并且年轻一代中的大多数对象都是垃圾并且可以回收,则可以优化次要集合。首先,这种收集的费用与所收集的有生命物体的数量成正比;可以很快收集到充满死亡物体的年轻一代。通常,在每个次要收集过程中,来自年轻一代的尚存对象的一部分会移交给有权的 一代。最终,保有权的一代将填满并且必须被收集,从而导致 大量的收集,其中收集了整个堆。主要集合的持续时间通常比次要集合的持续时间长得多,因为涉及的对象数量很多。
如上所述, 人体工程学可以动态选择垃圾收集器,以便在各种应用程序上提供良好的性能。串行垃圾收集器是为具有小型数据集的应用程序设计的,其默认参数被选择为对大多数小型应用程序有效。吞吐量垃圾收集器旨在用于具有中大型数据集的应用程序。通过人机工程学选择的堆大小参数以及自适应大小策略的功能旨在为服务器应用程序提供良好的性能。这些选择在大多数(但不是全部)情况下都能很好地发挥作用。这导致了本文档的中心宗旨:
如果垃圾收集成为瓶颈,您很可能必须自定义总堆大小以及各个世代的大小。检查详细的垃圾收集器输出,然后探索各个性能指标对垃圾收集器参数的敏感性。 |
世代的默认排列(对于所有收集器(并行收集器除外))如下所示。
在初始化时,最大的地址空间实际上是保留的,除非需要,否则不会分配给物理内存。为对象存储器保留的完整地址空间可以分为年轻一代和终身一代。
年轻一代由 伊甸园和两个 幸存者空间组成。大多数对象最初是在eden中分配的。一个幸存者空间随时都是空的,并在下一次复制集合期间用作伊甸园中任何活物的目的地,而另一个幸存者空间则作为目的地。对象以这种方式生存空间之间复制,直到他们都老了,足以被 终身教授(复制到年老代)。
与终身代紧密相关的第三代是永久代,该 代保留虚拟机所需的数据,以描述在Java语言级别上不具有等效性的对象。例如,描述类和方法的对象存储在永久代中。
有两种主要的垃圾收集性能度量:
吞吐量是长时间内未花费在垃圾回收上的总时间的百分比。吞吐量包括分配所花费的时间(但是通常不需要调整分配速度)。
暂停是指由于正在进行垃圾回收而导致应用程序无响应的时间。
用户对垃圾回收有不同的要求。例如,有些人认为Web服务器的正确度量标准是吞吐量,因为垃圾回收期间的暂停可能是可以容忍的,或者只是被网络延迟所掩盖。但是,在交互式图形程序中,即使短暂的暂停也会对用户体验产生负面影响。
一些用户对其他注意事项敏感。 足迹是流程的工作集,以页和缓存行为单位。在具有有限物理内存或许多进程的系统上,占用空间可能决定可伸缩性。 迅速是当一个对象变得死之间的时间当存储器变得可用时,分布式系统,包括远程方法调用(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从第一行开始)分别指示垃圾收集之前和之后的活动对象的组合大小。进行次要收集后,大小包括一些垃圾(不再活动)但无法回收的对象。这些对象包含在使用权的世代中,或从使用权的世代或永久代中引用。
括号中的下一个数字(例如, (776768K)再次从第一行开始)是堆的已提交大小:在无需向操作系统请求更多内存的情况下,可用于Java对象的空间量。请注意,此数字不包括幸存者空间之一,因为在任何给定时间只能使用一个幸存者空间,并且也不包括永久代,该永久代保存虚拟机使用的元数据。
该行的最后一项(例如 0.2300771 secs)表示执行收集所需的时间;在这种情况下,大约需要四分之一秒。
第三行中主要收藏的格式相似。
由产生的输出格式 -verbose:gc可能会在将来的版本中更改。 |
该选项 -XX:+PrintGCDetails使有关集合的其他信息得以打印。-XX:+PrintGCDetails此处显示了使用串行垃圾收集器的输出示例 。
[GC [DefNew: 64575K->959K(64576K), 0.0457646 secs] 196016K->133633K(261184K), 0.0459067 secs]
表示次要收集恢复了约98%的年轻一代, DefNew: 64575K->959K(64576K)并耗时 0.0457646 secs(约45毫秒)。
整个堆的使用率降低到51%左右, 196016K->133633K(261184K)并且收集的最后开销为,稍微增加了一些开销(年轻一代的收集之外) 0.0459067 secs。
该选项 -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 secs大约130毫秒的时间。
与一样 -verbose:gc,产生的输出格式 -XX:+PrintGCDetails在将来的发行版中可能会更改。 |
许多参数会影响世代大小。下图说明了堆中已提交空间和虚拟空间之间的区别。在虚拟机初始化时,将保留堆的整个空间。可以使用该-Xmx选项指定保留空间的大小 。如果 -Xms参数的值小于参数的值,则 -Xmx并非所有保留的空间都会立即提交给虚拟机。在此图中,未使用的空间标记为“虚拟”。堆的不同部分(永久代,终身代和年轻代)可以根据需要增长到虚拟空间的极限。
一些参数是堆的一部分与另一部分的比率。例如,该参数 NewRatio表示保有权代与年轻代的相对大小。这些参数将在下面讨论。
请注意,以下有关增大和缩小堆以及默认堆大小的讨论不适用于并行收集器。(有关并行调整器的堆大小调整和默认堆大小的详细信息,请参见 人体工程学部分。)但是,控制堆的总大小和世代大小的参数确实适用于并行收集器。
由于收集是在世代填满时发生的,因此吞吐量与可用内存量成反比。总可用内存是影响垃圾收集性能的最重要因素。
默认情况下,虚拟机会在每个集合上增加或缩小堆,以尝试将每个集合中活动对象的可用空间比例保持在特定范围内。此目标范围由参数-XX:MinHeapFreeRatio=<minimum>和 设置为百分比 -XX:MaxHeapFreeRatio=<maximum>,总大小由限制为下限 -Xms<min>,之上为 -Xmx<max>。下表显示了32位Solaris操作系统(SPARC平台版本)的默认参数:
参数 | 默认值 |
MinHeapFreeRatio | 40 |
MaxHeapFreeRatio | 70 |
-Xms | 3670k |
-Xmx | 64m |
64位系统上堆大小参数的默认值已增加了大约30%。这种增加是为了补偿64位系统上更大的对象大小。
使用这些参数,如果某代中的可用空间百分比降到40%以下,则该代将被扩展以维持40%的可用空间,直到该代最大允许的大小。同样,如果可用空间超过70%,则将收缩该代,以使只有70%的空间是可用的,这取决于代的最小大小。
大型服务器应用程序在使用这些默认值时通常会遇到两个问题。一种是缓慢的启动,因为初始堆很小,并且必须在许多主要集合中调整其大小。更为紧迫的问题是,对于大多数服务器应用程序,默认的最大堆大小过小。服务器应用程序的经验法则是:
除非您在暂停方面遇到问题,否则请尝试为虚拟机分配尽可能多的内存。默认大小(64MB)通常太小。
通过从虚拟机中删除最重要的大小决策,将-Xms和 设置 -Xmx为相同的值可以提高可预测性。但是,如果选择不当,虚拟机将无法补偿。
通常,由于分配可以并行化,因此随着处理器数量的增加而增加内存。
作为参考,有单独的页面解释了一些可用的 命令行选项。
第二个最有影响的旋钮是专用于年轻一代的堆的比例。年轻一代越大,收藏的次数就越少。但是,对于有限制的堆大小,较大的年轻代意味着较小的终身代,这将增加主要集合的频率。最佳选择取决于应用程序分配的对象的生命周期分布。
默认情况下,年轻一代的大小受的控制 NewRatio。例如,设置 -XX:NewRatio=3意味着年轻一代和终身一代之间的比率为1:3。换句话说,伊甸园空间和幸存者空间的总大小将是堆总大小的四分之一。
这些参数 NewSize和 MaxNewSize边界从下到上的年轻代大小。这些设置为相同的值修正的年轻一代,就像设置 -Xms和 -Xmx相同的值修正总堆大小。这对于以比允许的整数倍更好的粒度调整年轻一代很有用 NewRatio。
如果需要,该参数 SurvivorRatio可用于调整幸存空间的大小,但这对性能通常不那么重要。例如, -XX:SurvivorRatio=6将伊甸园和幸存者空间之间的比例设置为1:6。换句话说,每个幸存者空间将是伊甸园大小的六分之一,因此是年轻一代的八分之一(不是七分之一,因为有两个幸存者空间)。
如果幸存者空间太小,则复制集合将直接溢出到终身代。如果幸存者空间太大,它们将毫无用处。在每次垃圾回收时,虚拟机都会选择一个阈值次数,以限制对象在使用之前可以复制的次数。选择此阈值可使幸存者半满。命令行选项 -XX:+PrintTenuringDistribution可用于显示此阈值和新一代对象的寿命。这对于观察应用程序的生命周期分布也很有用。
这是32位Solaris操作系统(SPARC Platform Edition)的默认值。其他平台上的默认值是不同的。
| 默认值 | |
参数 | 客户端JVM | 服务器JVM |
NewRatio | 8 | 2 |
NewSize | 2228K | 2228K |
MaxNewSize | not limited | not limited |
SurvivorRatio | 32 | 32 |
年轻代的最大大小将根据总堆和的最大大小来计算 NewRatio。“无限制”默认值 MaxNewSize表示MaxNewSize除非MaxNewSize在命令行上指定的值,否则 计算值不受限制 。
服务器应用程序的经验法则是:
首先确定您可以负担得起的虚拟机的最大堆大小。然后针对年轻一代绘制性能指标,以找到最佳设置。
请注意,最大堆大小应始终小于计算机上安装的内存量,以避免过多的页面错误和崩溃。
如果总堆大小是固定的,则增加年轻代的大小需要减小持久代的大小。使使用期限的一代足够大,以容纳应用程序在任何给定时间使用的所有实时数据,以及一定数量的空闲空间(10-20%或更多)。
受制于终身代的上述约束:
给年轻一代留下很多记忆。
随着处理器数量的增加,可以增加年轻代的大小,因为分配可以并行化。
到目前为止,讨论的是串行收集器。Java HotSpot VM包括三个不同的收集器,每个收集器具有不同的性能特征。
该 串行收集器使用一个单独的线程来执行所有的垃圾收集工作,这使得它比较有效,因为没有线程之间没有通信开销。它最适合单处理器计算机,因为它不能利用多处理器硬件,尽管它在多处理器上对于数据集较小(最大约100MB)的应用很有用。默认情况下,在某些硬件和操作系统配置上选择了串行收集器,或者可以使用选项明确启用串行收集器 -XX:+UseSerialGC。
的 并行收集(也被称为 吞吐量集电极)并行进行轻微的集合,其可以减少显著垃圾收集开销。它适用于具有在多处理器或多线程硬件上运行的中型到大型数据集的应用程序。默认情况下,在某些硬件和操作系统配置上选择了并行收集器,或者可以使用选项显式启用并行收集器 -XX:+UseParallelGC。
新增内容: 并行压缩是J2SE 5.0 Update 6中引入的功能,并在Java SE 6中得到了增强,该功能允许并行收集器并行执行主要收集。如果没有并行压缩,则使用单个线程执行主要集合,这会大大限制可伸缩性。通过-XX:+UseParallelOldGC在命令行中添加选项来启用并行压缩 。
在 并发收集器执行其工作的同时多数(即当应用程序仍在运行),以确保垃圾收集短暂停。它设计用于具有中型到大型数据集的应用程序,对于这些应用程序,响应时间比整体吞吐量更重要,因为用于最小化暂停的技术会降低应用程序性能。并发收集器通过选项启用 -XX:+UseConcMarkSweepGC。
除非您的应用程序有非常严格的暂停时间要求,否则请先运行您的应用程序并允许VM选择收集器。如有必要,请调整堆大小以提高性能。如果性能仍然不能达到您的目标,请使用以下准则作为选择收集器的起点。
如果应用程序的数据集较小(最大约100MB),则
使用选择串行收集器 -XX:+UseSerialGC。
如果应用程序将在单个处理器上运行,并且没有暂停时间要求,则
让VM选择收集器,或者
使用选择串行收集器 -XX:+UseSerialGC。
如果(a)峰值应用程序性能是第一要务,并且(b)没有暂停时间要求或一秒钟或更长时间的暂停是可接受的,则
让VM选择收集器,或者
使用-XX:+UseParallelGC和选择并行收集器, 并使用(可选)启用并行压缩 -XX:+UseParallelOldGC。
如果响应时间比整体吞吐量更重要,并且垃圾收集暂停时间必须保持在不到一秒钟左右,那么
选择并发收集器 -XX:+UseConcMarkSweepGC。如果只有一个或两个处理器可用,请考虑使用 以下所述的增量模式。
这些准则只是选择收集器的起点,因为性能取决于堆的大小,应用程序维护的实时数据量以及可用处理器的数量和速度。暂停时间对这些因素特别敏感,因此上述提到的一秒的阈值仅是近似值:在许多数据大小和硬件组合上,并行收集器的暂停时间将超过一秒。相反,在某些组合上,并发收集器可能无法使暂停时间短于一秒钟。 |
如果推荐的收集器没有达到期望的性能,请首先尝试调整堆和世代大小以满足期望的目标。如果仍然不成功,请尝试使用其他收集器:使用并发收集器减少暂停时间,并使用并行收集器增加多处理器硬件上的总体吞吐量。
并行收集器(在此也称为吞吐量收集器)是类似于串行收集器的分代收集器。主要区别在于使用多个线程来加速垃圾回收。并行收集器通过命令行选项启用 -XX:+UseParallelGC。默认情况下,只有次要集合可以并行执行。主要集合是通过单个线程执行的。但是,可以使用该选项启用并行压缩, -XX:+UseParallelOldGC以便次要和主要收集都可以并行执行,以进一步减少垃圾收集的开销。
在具有N个处理器的计算机上 ,并行收集器使用 N个垃圾收集器线程。但是,可以使用命令行选项调整此数字(请参见下文)。在具有一个处理器的主机上,由于并行执行(例如,同步)所需的开销,并行收集器的性能可能不如串行收集器。但是,当运行具有中型到大型堆的应用程序时,在具有两个处理器的计算机上,它通常比串行收集器的性能要适度,并且在可用两个以上处理器的情况下,其性能通常明显好于串行收集器。
垃圾收集器线程的数量可以通过命令行选项控制 -XX:ParallelGCThreads=<N>。如果使用命令行选项对堆进行显式调整,则为使并行收集器具有良好性能而需要的堆大小首先要与串行收集器所需的堆大小相同。启用并行收集器只会使次要收集的暂停时间缩短。因为次要收集中有多个垃圾收集器线程,所以在收集期间从年轻一代升级到终身代的可能性很小。每个垃圾收集线程都保留使用权的一代中的一部分用于提升,而将可用空间划分为这些“提升缓冲区”会导致碎片效应。
如前所述,并行收集器中的世代排列是不同的。下图显示了这种安排。
从J2SE 5.0开始,默认情况下在服务器级机器上选择并行收集器,如文档 垃圾收集器人体工程学中所述。另外,并行收集器使用一种自动调整的方法,该方法允许指定所需的行为,而不是生成大小和其他低级调整详细信息。可以指定的行为是:
最大垃圾收集暂停时间
通量
占地面积(即堆大小)
最大暂停时间目标是通过命令行选项指定的 -XX:MaxGCPauseMillis=<N>。这被解释为需要<N>毫秒或更短的暂停时间的提示;默认情况下,没有最大暂停时间目标。如果指定了暂停时间目标,则会调整堆大小和其他与垃圾回收相关的参数,以使垃圾回收的暂停时间短于指定值。请注意,这些调整可能导致垃圾收集器降低应用程序的整体吞吐量,并且在某些情况下无法满足所需的暂停时间目标。
吞吐量目标是根据进行垃圾收集所花费的时间与在垃圾收集之外所花费的时间(称为应用程序时间)来衡量的。该目标由命令行选项指定,该选项 -XX:GCTimeRatio=<N>将垃圾回收时间与应用程序时间之比设置为 1 / (1 + <N>)。
例如, -XX:GCTimeRatio=19将垃圾收集的目标设置为总时间的1/20或5%。默认值为99,导致垃圾回收的目标时间为1%。
使用现有选项指定最大堆占用空间 -Xmx<N>。另外,收集器还有一个隐含的目标,即只要满足其他目标,就将堆的大小最小化。
目标按以下顺序解决
最大暂停时间目标
吞吐量目标
最小足迹目标
首先达到最大暂停时间目标。仅在达到目标后,才能解决吞吐量目标。同样,只有在达到前两个目标后,才会考虑足迹目标。
收集器保留的统计信息(例如平均暂停时间)在每个收集结束时更新。然后进行确定目标是否达到的测试,并对世代大小进行任何必要的调整。唯一的例外是,System.gc()在保留统计信息和调整世代大小方面,会忽略显式垃圾回收(例如对的调用 )。
增长和缩小世代的大小是通过增加作为世代大小的固定百分比的增量来完成的,以便使世代朝其期望的大小递增或递减。生长和收缩以不同的速率进行。默认情况下,世代以20%的增量增长,而以5%的增量缩减。成长百分比由-XX:YoungGenerationSizeIncrement=<Y>年轻一代和 -XX:TenuredGenerationSizeIncrement=<T>终身代的命令行标志控制 。世代收缩的百分比由命令行标志调整 -XX:AdaptiveSizeDecrementScaleFactor=<D>。如果增长增量为X百分比,则收缩的增量 为 X / D百分比。
如果收集器决定在启动时增加一代,则将增加一个补充百分比。该补充剂随收集数量的增加而衰减,并且该补充剂没有长期影响。补充的目的是提高启动性能。缩小百分比没有补充。
如果未达到最大暂停时间目标,则一次仅缩小一代的大小。如果两个世代的暂停时间都超过目标,则首先缩减具有较大暂停时间的世代大小。
如果未达到吞吐量目标,则两代产品的大小都会增加。每一个都按其对总垃圾收集时间的贡献成比例增加。例如,如果年轻一代的垃圾收集时间是总收集时间的25%,并且如果年轻一代的完全增量将增加20%,则年轻一代将增加5%。
如果未在命令行上另外设置,则根据计算机上的内存量来计算初始堆大小和最大堆大小。用于堆的内存比例由命令行选项DefaultInitialRAMFraction和 来控制 DefaultMaxRAMFraction,如下表所示。(在表中, memory表示机器上的内存量。)
| 式 | 默认 |
初始堆大小 | memory / DefaultInitialRAMFraction | memory / 64 |
最大堆大小 | MIN(memory / DefaultMaxRAMFraction, 1GB) | MIN(memory / 4, 1GB) |
请注意1GB,无论计算机上安装了多少内存,默认的最大堆大小都不会超过 。
如果在垃圾回收上花费了太多时间,则并行收集器将抛出OutOfMemoryError:如果在垃圾回收中花费了总时间的98%以上,而回收的堆少于2%,则将抛出OutOfMemoryError。此功能旨在防止应用程序长时间运行,而由于堆太小而几乎没有进展,甚至没有进展。如有必要,可以通过-XX:-UseGCOverheadLimit在命令行中添加选项来禁用此功能 。
并行收集器输出的详细垃圾收集器与串行收集器的输出基本相同。
并发收集器是为那些希望较短的垃圾收集暂停并且可以在应用程序运行时与垃圾收集器共享处理器资源的应用程序而设计的。通常,具有相对较长的长期数据集(大量使用期限)并在具有两个或多个处理器的计算机上运行的应用程序往往会受益于此收集器的使用。但是,对于暂停时间要求较低的任何应用程序,都应考虑使用该收集器。例如,对于在单个处理器上具有适度大小的持久代的交互式应用程序,已经观察到良好的结果,尤其是在使用 增量模式的情况下。并发收集器通过命令行选项启用 -XX:+UseConcMarkSweepGC。
与其他可用的收集器类似,并发收集器是世代的。因此,次要收藏和主要收藏都发生了。并发收集器尝试通过使用单独的垃圾收集器线程在执行应用程序线程的同时并发地跟踪可访问对象,来减少由于主要收集而导致的暂停时间。在每个主要的收集周期中,并发收集器将在收集开始时暂停所有应用程序线程一小段时间,并在收集中途再次暂停。第二个暂停往往是两个暂停中的较长者,并且在该暂停期间使用多个线程来执行收集工作。收集的其余部分(包括大量活动对象的跟踪和不可达对象的清除)是由一个或多个与应用程序同时运行的垃圾收集器线程完成的。次要收藏可以与正在进行的主要周期交织在一起,并且以类似于 并行收集器(特别是在次要收集期间停止了应用程序线程)。
并发收集器使用的基本算法在技术报告A世代的大部分并发垃圾收集器中进行了描述 。请注意,随着收集器从一个发行版增强到另一个发行版,确切的实现细节可能会略有不同。
并发收集器将处理器资源(否则将可用于应用程序)交易以缩短主要收集的暂停时间。最明显的开销是在集合的并发部分使用一个或多个处理器。在 N处理器系统上,集合的并发部分将使用 可用处理器的K / N,其中 1 <= K <= ceiling {N / 4}。(请注意,对K的精确选择和范围 可能会发生变化。)除了在并发阶段使用处理器外,还会产生额外的开销来实现并发。因此,虽然并发收集器的垃圾收集暂停通常要短得多,但应用程序吞吐量也往往比其他收集器要低一些。
在具有多个处理核心的机器上,在收集的并发部分中有可用于应用程序线程的处理器,因此并发垃圾收集器线程不会“暂停”应用程序。这通常会导致更短的暂停,但是再次有较少的处理器资源可用于应用程序,并且应该会出现一定的速度下降,尤其是在应用程序最大程度地利用所有处理核心的情况下。达到极限时,随着 N增加,由于并发垃圾收集而导致的处理器资源减少将变得更小,并且并发收集的收益也会增加。下一节 并发模式故障讨论了这种扩展的潜在限制。
由于在并发阶段使用至少一个处理器进行垃圾收集,因此并发收集器通常不会在单处理器(单核)计算机上提供任何好处。但是,有一种单独的模式可以在只有一个或两个处理器的系统上实现低暂停。有关详细信息,请参见 下面的增量模式。
并发收集器使用一个或多个垃圾收集器线程,这些线程与应用程序线程同时运行,目的是在任一个和永久的世代都变满之前完成对终身代和永久代的收集。如上所述,在正常操作中,并发收集器在应用程序线程仍在运行的情况下执行其大部分跟踪和清除工作,因此应用程序线程仅会看到短暂的暂停。但是,如果并发收集器在使用权产生的一代填满之前无法完成回收无法访问的对象,或者如果使用权能生成的可用空闲空间块无法满足分配要求,则暂停应用程序,并使用所有应用程序线程均已停止。 并发模式失败,表示需要调整并发收集器参数。
如果在垃圾回收上花费了太多时间,则并发收集器将抛出OutOfMemoryError:如果在垃圾回收中花费了总时间的98%以上,而回收的堆少于2%,则将抛出OutOfMemoryError。此功能旨在防止应用程序长时间运行,而由于堆太小而几乎没有进展,甚至没有进展。如有必要,可以通过-XX:-UseGCOverheadLimit在命令行中添加选项来禁用此功能 。
该策略与并行收集器中的策略相同,除了执行并发收集所花费的时间不计入98%的时间限制。换句话说,只有在应用程序停止时执行的收集才计入过多的GC时间。此类收集通常是由于并发模式故障或显式收集请求(例如对的调用 System.gc())引起的。 |
与HotSpot中的所有其他收集器一样,并发收集器是一个跟踪收集器,它至少标识堆中的所有可访问对象。用 Jones和Lins的话来说,它是一个 增量更新收集器。因为应用程序线程和垃圾收集器线程在主收集期间同时运行,所以垃圾收集器线程跟踪的对象随后可能会在收集完成时变得不可访问。尚未回收的此类无法访问的对象称为 浮动垃圾。浮动垃圾的数量取决于并发收集周期的持续时间以及引用更新的频率,也称为 突变,由应用程序决定。此外,由于年轻一代和终身一代是独立收集的,因此每个人都是彼此的根源。作为一个粗略的经验法则,请尝试将永久代的大小增加20%,以解决浮动垃圾的问题。在一个并发收集周期结束时,将在下一个收集周期中收集堆中的浮动垃圾。
并发收集器在并发收集周期中两次暂停应用程序。第一个暂停是将可直接从根(例如,来自应用程序线程堆栈和寄存器的对象引用,静态对象等)以及从堆中其他位置(例如,年轻代)访问的对象标记为活动状态。此第一个停顿称为 初始标记停顿。第二次暂停在并发跟踪阶段结束时进行,并在并发收集器完成跟踪该对象之后,查找由于对象中引用的应用程序线程进行更新而导致并发跟踪遗漏的对象。此第二个停顿称为 备注停顿。
可达对象图的并发跟踪发生在初始标记暂停和注释暂停之间。在此并发跟踪阶段,一个或多个并发垃圾回收器线程可能正在使用处理器资源,否则这些资源将可供应用程序使用,结果,在此阶段和其他并发阶段,计算绑定应用程序的应用程序吞吐量可能会相应下降。即使应用程序线程没有暂停。备注暂停后,存在并发清除阶段,该阶段收集被标识为不可访问的对象。收集周期完成后,并发收集器将等待,几乎不消耗任何计算资源,直到下一个主要收集周期开始。
使用串行收集器时,只要保有期限的代满,并且在完成收集过程中所有应用程序线程都停止时,就会发生主要收集。相反,并发收集需要一次启动,这样收集才能在保有期限的代满之前完成。否则,由于并发模式故障,应用程序将观察到更长的暂停 。有多种方式可以启动并发收集。
根据最近的历史记录,并发收集器将保留对权属生成耗尽之前的剩余时间以及并发收集周期所需时间的估计。基于这些动态估计,将开始并发的收集周期,目的是在使用权产生之前用完收集周期。由于并发模式故障的代价可能很高,因此为了安全起见,对这些估计进行了填充。
如果年老代的占用率超过了一并发收集也将开始 发起占用,年老代的百分比。此初始占用阈值的默认值约为92%,但是该值可能会因版本而异。可以使用命令行选项手动调整此值
| -XX:CMSInitiatingOccupancyFraction = <N> |
其中,<N>是使用权的世代大小的整数百分比(0-100)。
年轻一代收藏和终身一代收藏的暂停独立发生。它们不会重叠,但是可能会快速连续发生,因此一个集合的暂停,紧接着是另一个集合的暂停,可能看起来像是一个较长的暂停。为了避免这种情况,并发收集器尝试在上一代和下一个年轻暂停之间的大致中间时间安排注释暂停。当前未针对初始标记暂停执行此计划,该时间通常比标记暂停短得多。
并发收集器可以在并发阶段以增量方式完成的模式下使用。回想一下,在并发阶段,垃圾收集器线程正在使用一个或多个处理器。增量模式旨在通过定期停止并发阶段以将处理器交还给应用程序来减轻长时间的并发阶段的影响。此模式在这里称为“ i-cms”,它将收集器同时完成的工作分成小块时间,这些时间块安排在年轻一代收集之间。当需要并发收集器提供的低暂停时间的应用程序在具有少量处理器(例如1或2)的计算机上运行时,此功能很有用。
并发收集周期通常包括以下步骤:
停止所有应用程序线程并标识从根可访问的对象集,然后恢复所有应用程序线程
在应用程序线程正在执行时,使用一个或多个处理器同时跟踪可访问对象图
使用一个处理器并同时回溯自上一步中的跟踪以来修改的对象图的各个部分
停止所有应用程序线程并回溯自上次检查以来可能已修改的根和对象图的部分,然后恢复所有应用程序线程
使用一个处理器同时将无法访问的对象清除到用于分配的空闲列表中
使用一个处理器同时调整堆大小并为下一个收集周期准备支持数据结构
通常,并发收集器会在整个并发跟踪阶段使用一个或多个处理器,而不会自愿放弃它们。同样,一个处理器用于整个并发扫描阶段,而不会放弃它。对于具有响应时间限制的应用程序而言,这种开销可能会造成太多破坏,否则可能会利用处理核心,尤其是在仅具有一个或两个处理器的系统上运行时。增量模式通过将并发阶段分解为短暂的活动突发来解决此问题,这些突发活动计划在较小的暂停之间进行。
i-cms使用 占空比来控制并发收集器在自愿放弃处理器之前可以完成的工作量。占空比是允许并发收集器运行的年轻代收集之间的时间百分比。i-cms可以根据应用程序的行为自动计算占空比(推荐的方法,称为 自动定步),也可以在命令行上将占空比设置为固定值。
以下命令行选项控制i-cms(请参阅以下有关初始选项集的建议):
选项 | 描述 | 默认值 | |
J2SE 5.0及更早版本 | Java SE 6及更高版本 | ||
-XX:+CMSIncrementalMode | 启用增量模式。请注意,还必须启用并发收集器(使用-XX:+ UseConcMarkSweepGC),此选项才能起作用。 | 残障人士 | 残障人士 |
-XX:+CMSIncrementalPacing | 启用自动定步。增量模式占空比根据JVM运行时收集的统计信息自动调整。 | 残障人士 | 已启用 |
-XX:CMSIncrementalDutyCycle=<N> | 允许并发收集器运行的次要收集之间的时间百分比(0-100)。如果启用了CMSIncrementalPacing,则这只是初始值。 | 50 | 10 |
-XX:CMSIncrementalDutyCycleMin=<N> | 百分比(0-100),是启用CMSIncrementalPacing时占空比的下限。 | 10 | 0 |
-XX:CMSIncrementalSafetyFactor=<N> | 计算占空比时用于增加保守性的百分比(0-100)。 | 10 | 10 |
-XX:CMSIncrementalOffset=<N> | 在次要收集之间的时间内,增量模式占空比向右移动的百分比(0-100)。 | 0 | 0 |
-XX:CMSExpAvgFactor=<N> | 计算并发集合统计信息的指数平均值时,用于加权当前样本的百分比(0-100)。 | 25 | 25 |
要在Java SE 6中使用i-cms,请使用以下命令行选项:
| -XX:+ UseConcMarkSweepGC -XX:+ CMSIncrementalMode \
-XX:+ PrintGCDetails -XX:+ PrintGCTimeStamps
|
前两个选项分别启用并发收集器和i-cms。不需要最后两个选项。它们只是使有关垃圾回收的诊断信息写入stdout,以便可以看到垃圾回收行为并在以后进行分析。
请注意,在J2SE 5.0和更早版本中,我们建议将以下内容作为i-cms的初始命令行选项集:
| -XX:+ UseConcMarkSweepGC -XX:+ CMSIncrementalMode \
-XX:+ PrintGCDetails -XX:+ PrintGCTimeStamps \
-XX:+ CMSIncrementalPacing -XX:CMSIncrementalDutyCycleMin = 0
-XX:CMSIncrementalDutyCycle = 10
|
这些与Java SE 6的建议相同,增加了控制i-cms自动起搏的三个选项。附加选项仅指定在Java SE 6中成为默认值的值。
i-cms自动调整功能使用程序运行时收集的统计信息来计算占空比,以便并发收集在堆变满之前完成。但是,过去的行为并不是未来行为的完美预测器,并且估计值可能并不总是足够准确以防止堆变满。如果出现太多完整集合,请尝试以下步骤,一次尝试一个:
步 | 选件 |
1.增加安全系数: | -XX:CMSIncrementalSafetyFactor=<N> |
2.增加最小占空比: | -XX:CMSIncrementalDutyCycleMin=<N> |
3.禁用自动起搏,并使用固定的占空比: | -XX:-CMSIncrementalPacing -XX:CMSIncrementalDutyCycle=<N> |
以下是并发收集器的输出,其中包含options -verbose:gc -XX:+PrintGCDetails,除去了一些次要细节。注意,并发收集器的输出散布在次要收集的输出中。通常在并发收集周期中会发生许多次要收集。的 CMS初始标记:指示并发收集周期的开始。的 CMS并发标记: 指示并发标记阶段的结束,并且 CMS并发扫描:标志着并发清除阶段的结束。之前未讨论过的预清洁阶段由 CMS-concurrent-preclean:。预清洁代表可以在准备备注阶段时同时进行的工作 CMS备注。最后阶段由 CMS并发重置: 并为下一个并发收集做准备。
[GC [1 CMS初始标记:13991K(20288K)] 14103K(22400K),0.0023781秒]
[GC [DefNew:2112K-> 64K(2112K),0.0837052秒] 16103K-> 15476K(22400K),0.0838519秒]
...
[GC [DefNew:2077K-> 63K(2112K),0.0126205秒] 17552K-> 15855K(22400K),0.0127482秒]
[CMS-concurrent-mark:0.267 / 0.374 secs]
[GC [DefNew:2111K-> 64K(2112K),0.0190851秒] 17903K-> 16154K(22400K),0.0191903秒]
[CMS-concurrent- preclean :0.044 / 0.064 secs]
[GC [1 CMS备注: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秒]
...
[GC [DefNew:2111K-> 0K(2112K),0.0035190秒] 17527K-> 15479K(22400K),0.0036052秒]
[CMS-concurrent-sweep:0.291 / 0.662 secs]
[GC [DefNew:2048K-> 0K(2112K ),0.0013347秒] 17527K-> 15479K(27912K),0.0014231秒]
[CMS并发重置:0.016 / 0.016秒]
[GC [DefNew:2048K-> 1K(2112K),0.0013936秒] 17527K-> 15479K(27912K) ),0.0014814秒]
相对于次要收集暂停时间,初始标记暂停通常较短。如以上示例输出所示,并发阶段(并发标记,并发预清理和并发扫描)通常持续的时间明显长于次要收集暂停。但是请注意,在这些并发阶段中不会暂停应用程序。备注停顿的长度通常可与次要收藏相媲美。备注暂停受某些应用程序特性的影响(例如,对象修改的高速率可能会增加此暂停)以及自上次次要收集以来的时间(即,年轻一代中的更多对象可能会增加此暂停)。
永久代大小
对于大多数应用程序,永久生成不会对垃圾收集器的性能产生明显影响。但是,某些应用程序会动态生成并加载许多类。例如,JavaServer Pages(JSP)页面的某些实现。这些应用程序可能需要更大的永久代来保留其他类。如果是这样,可以使用命令行选项来增加最大永久代大小 -XX:MaxPermSize=<N>。
完成;弱,软和幻像引用
一些应用程序通过使用终结处理和弱引用,软引用或幻像引用与垃圾回收进行交互。这些功能可以在Java编程语言级别上创建性能工件。这样的一个示例是依靠终结来关闭文件描述符,这使得外部资源(描述符)依赖于垃圾回收的及时性。依靠垃圾回收来管理内存以外的资源几乎总是一个坏主意。
该 参考资料部分包括了一篇文章,深入讨论了一些定型的陷阱和技术,以避免他们。
显式垃圾回收
应用程序可以与垃圾回收进行交互的另一种方式是通过调用显式调用完整的垃圾回收 System.gc()。这可能会强制在没有必要时(例如,当次要收集就足够时)进行主要收集,因此通常应避免使用。可以通过使用标志禁用显式垃圾回收来评估显式垃圾回收对性能的影响 -XX:+DisableExplicitGC,这会导致VM忽略对的调用 System.gc()。
RMI的分布式垃圾收集(DGC)是显式垃圾收集最常遇到的用途之一。使用RMI的应用程序引用其他虚拟机中的对象。如果不偶尔收集本地堆,就无法在这些分布式应用程序中收集垃圾,因此RMI会定期强制执行完整收集。这些收集的频率可以通过属性进行控制。例如,
| java -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 ... |
指定每小时一次的显式收集,而不是默认的每分钟一次的速率。但是,这也可能导致某些对象需要更长的时间才能被回收。Long.MAX_VALUE如果不希望DGC活动的及时性有上限,可以将这些属性设置为高, 以使显式收集之间的时间有效地无限。
软参考
在服务器虚拟机中,软引用的生存期比在客户端中更长。清除速率可以使用命令行选项控制,该选项 -XX:SoftRefLRUPolicyMSPerMB=<N>指定对于堆中每兆字节的可用空间,软引用将保持活动状态的毫秒数(一旦不再严格可达)。默认值为每兆字节1000毫秒,这意味着对于堆中的每兆字节可用空间,软引用(在收集到对对象的最后一个强引用之后)将保留1秒钟。请注意,这是一个大概的数字,因为仅在垃圾回收期间才清除软引用,这可能会偶尔发生。
Solaris 8备用libthread
Solaris 8操作系统支持线程库的替代版本libthread,该版本将线程直接绑定到轻量级进程(LWP)。使用此替代libthread可以使某些应用程序受益匪浅,这对于任何线程化应用程序都是潜在的好处。以下命令将为Java加载备用libthread(显示了Bourne shell语法):
| LD_PRELOAD = / usr / lib / lwp / libthread.so.1 导出LD_PRELOAD Java的 |
仅在Solaris 8上才需要上述操作,因为备用libthread是Solaris 9操作系统中的默认libthread,并且是从Solaris 10开始唯一可用的libthread。
GC输出示例描述了如何解释来自不同收集器的输出。
如何处理Java Finalization的“内存保留问题”介绍了Finalization陷阱以及避免它们的方法。
理查德·琼斯(Richard Jones)和拉斐尔·林斯(Rafael Lins),《垃圾收集:自动动态内存管理的算法》,威利和桑斯(1996),国际标准书号0-471-94148-4
在网站上使用的术语“ Java虚拟机”和“ JVM”表示Java平台的虚拟机。