☕【JVM 技术指南】「理论总结笔记」Java 虚拟机垃圾回收认知和调优的"思南(司南)"【上部】
优化目标与策略(Ergonomics)
垃圾回收器、堆和运行时编译器默认选择
G1(Garbage First)收集器
GC 线程的最大值受限于堆大小和可用的 CPU 资源
初始堆空间(Xms)为物理内存的 1/64
最大堆空间(Xmx)为物理内存的 1/4
分层编译器,同时使用 C1 和 C2
可以将 Java HotSpot VM 垃圾收集器配置为优先满足两个目标之一:最大暂停时间和应用吞吐量,如果首选目标得到满足,收集器将尝试最大化其他目标。
最大暂停时间目标(Maximum Pause-Time Goal)
暂停时间是垃圾收集器停止应用程序并恢复不再使用的空间的持续时间。 最大暂停时间目标的意图是限制这些暂停的最长时间。
使用命令行选项
-XX:MaxGCPauseMillis=<nnn>
指定最大暂停时间目标。这被解释为向垃圾回收器提示,需要的暂停时间为 nnn 毫秒或更短。垃圾收集器调整 Java 堆大小和其他与垃圾收集相关的参数,以使垃圾收集暂停时间小于 n 毫秒。
最大暂停时间目标的缺省值随收集器的不同而变化。
这些调整可能会导致垃圾收集更频繁地发生,从而降低应用程序的总吞吐量。但是,在某些情况下,暂停时间的预期目标无法实现。
吞吐量目标(Throughput Goal)
吞吐量目标是根据收集垃圾所花费的时间来度量的,而垃圾收集之外所花费的时间是应用程序时间。
目标由命令行选项
-XX:GCTimeRatio=n
指定。垃圾收集时间与应用程序时间的比值为 1/ (1+nnn)。 例如, -XX:GCTimeRatio=19 设置了垃圾收集总时间的 1/20 或 5% 的目标。
用于垃圾收集的时间是所有垃圾收集引起的暂停的总时间。
如果吞吐量目标没有达到,那么垃圾收集器可能采取的一个行动是增加堆的大小,以便应用程序在收集暂停之间花费的时间可以更长。
使用空间(Footprint)
如果吞吐量和最大停顿时间目标已经达到,那么垃圾收集器就会减少堆的大小,直到其中一个目标(总是吞吐量目标)无法达到为止。
垃圾收集器可以使用的最小和最大堆大小可以分别使用 -Xms=<n> 和 -Xmx=<m> 来设置最小和最大堆大小。
垃圾收集器实现(Garbage Collector Implementation)
分代垃圾收集(Generational Garbage Collection)
一个对象被认为是“Garbage Object”,当无法从正在运行的程序中的任何其他活跃对象的引用访问到它时,JVM 可以重用它的内存。
可达性分析标记
最简单的垃圾收集算法在每次运行时遍历每个可达对象。任何剩下的东西都被认为是垃圾。
这种方法花费的时间与活跃对象的数量成正比,这对于维护大量活跃数据的大型应用程序来说是不应该选择的。
Java HotSpot 虚拟机合并了许多不同的垃圾收集算法,除了 ZGC 回收器之外,这些算法都使用一种称为分代收集的技术。
虽然简单的垃圾收集每次都会检查堆中的每个活动对象,分代收集利用了大多数应用程序的一些经验观察到的属性,以最小化回收未使用(垃圾对象)所需的工作。
这些被观察到的性质中最重要的是弱世代假说,即大多数对象只能存活很短的时间。
此外还有一些在初始化时分配的对象会一直存在直到 JVM 结束退出。
通过关注大多数对象“早逝”这一事实,高效的收集成为可能。
世代说分析(Generations)
为了对此场景进行优化,对内存进行分代管理(存放不同年龄段对象的内存池)。垃圾回收在每一代填满时发生。
绝大多数对象分配在一个专门用于年轻对象的池中(年轻代) ,大多数对象死在那里。
当年轻代的垃圾填满时,触发 Minor GC,只有年轻代的垃圾会被回收,而其他代的垃圾则不会被回收。这种收集的成本,在第一阶段,与被收集的活对象数量成正比-> 年轻代回收垃圾非常快。
通常,在每次 Minor GC 期间,年轻代幸存(在幸存者区)的对象中的一部分被移动到老年代。
最终,老年代将被填满并且必须被回收,从而造成 Major GC(Full GC),在这个回收中将收集整个堆(包括方法区)。
Major GC 通常比 Minor GC 持续时间长得多,因为涉及的对象数量要大得多。
在启动时,Java HotSpot VM 将整个 Java 堆保留在地址空间中,但除非需要,否则不为其分配任何物理内存。
覆盖 Java 堆的整个地址空间在逻辑上被划分为年轻代和老年代。保留给对象存储的完整地址空间可以分为年轻代和老年代。
年轻代
年轻代由伊甸园(eden)和两个幸存者(survivor)空间组成。
Minor GC 的介绍
大多数对象最初是在伊甸园中分配的。一个幸存者空间在任何时候都是空的,我们称之为 To 幸存者区,并且在垃圾收集过程中作为伊甸园和另一个 From 幸存者空间中活动对象的目的地,在垃圾回收之后,伊甸园和 From 幸存者空间都是空的。
在下一次垃圾收集中,将交换两个幸存者空间的用途。最近填充的一个 FROM 幸存者区是将活动对象复制到 To 幸存者空间的。对象以这种方式在这两个幸存者空间之间复制,直到它们被复制了一定次数,或者那里没有足够的空间,这些对象被复制到老年区域中。这个过程也被称为“衰老”(和人生很像!)。
性能考虑因素
垃圾收集的主要度量指标是吞吐量和延迟。
吞吐量是在长时间内没有花在垃圾收集总时间的百分比。吞吐量包括分配所花费的时间(但通常不需要对分配速度进行调优)。
延迟是应用程序的响应能力。垃圾收集暂停会影响应用程序的响应能力。
用户对垃圾回收有不同的要求。
吞吐量和占用空间测量(Throughput and Footprint Measurement)
吞吐量和占用空间最好使用特定于应用程序的指标来度量。
例如,web 服务器的吞吐量可以使用一个客户端负载生成器进行测试。 但是,通过检查虚拟机本身的诊断输出,很容易估计垃圾收集引起的暂停。命令行选项 -verbose:gc 打印有关堆和垃圾收集的信息。
下面是一个例子:
输出显示了两个年轻代的回收,接着是应用程序通过调用 System.gc()启动的 full 回收。
这些行以一个时间戳开始,表示从应用程序启动时开始的时间。
接下来是关于这一行的日志级别(info)和标记(gc)的信息。
然后是 GC 标识号, 在本例中,有三个 GC,分别为 36、37 和 38。
然后记录 GC 的类型和声明 GC 的原因。
在此之后,将记录有关内存消耗的一些信息。该日志使用的格式:“GC 之前使用的堆空间” -> “GC 后使用的堆空间”。
示例的第一行是 239M->57M(307M),这意味着在 GC 前使用 239MB,并且 GC 清除了大部分内存,但是 57MB 保留了下来,堆大小为 307 MB。
注意,在这个示例中,full GC 将堆从 307 MB 缩小到 104 MB。在内存使用信息之后,记录 GC 的开始和结束时间以及持续时间(end-start)。
-verbose:gc 命令是 -Xloggc 的别名。
-Xlog 是 HotSpot JVM 中日志记录的通用日志记录配置选项。
这是一个基于标记的系统,其中 gc 是标记之一。要获得有关 GC 正在做什么的更多信息,可以配置日志记录,以打印包含 GC 标记和任何其他标记的任何消息。此选项的命令行选项是-Xloggc*。
下面是一个用-Xlog:gc* 记录的 G1 年轻代回收的示例:
影响垃圾收集性能的因素
影响垃圾收集性能的两个最重要的因素是总可用内存和专用于年轻代的堆的比例。
总堆(Total Heap)
影响垃圾收集性能的最重要因素是总可用内存。 由于收集发生在代填满时,因此吞吐量与可用内存量成反比。
影响生成代大小的堆选项
说明了堆中提交的空间和虚拟空间之间的区别。
在初始化虚拟机时,将保留堆的整个空间。可以使用 -Xmx 选项指定保留空间的大小。 如果 -Xms 参数的值小于 -Xmx 参数的值,那么并非所有保留的空间都立即提交给虚拟机。
未提交的空间在这个图中被标记为“virtual”。堆的不同部分,即老年代和年轻代,可以根据需要增长到虚拟空间的极限。
其中一些参数是堆的一部分与另一部分的比率。例如,参数 –XX:NewRatio 表示老年代与年轻代的相对大小。
堆大小的默认选项值
默认情况下,虚拟机在每次回收中增加或缩小堆,以便将每次回收中的可用空间与活动对象的比例保持在特定范围内。
此目标范围由选项
-XX:MinHeapFreeRatio=<minimum>
和-XX:MaxHeapFreeRatio=<maximum>
设置为百分比,总大小限制在–Xms<min>
和–Xmx<max>
之间。
使用命令行选项-XX:MaxHeapFreeRatio(默认值为 70%) 和 -XX:MinHeapFreeRatio (默认值为 40%)降低相关比例,从而最小化 Java 堆大小。
空间参数介绍
如果可用空间比例低于 40% ,那么这一代将扩展到保持 40%的可用空间,直到这一代的最大允许空间大小。
如果可用空间超过 70% ,那么这一代就会收缩,以便只有 70%的空间是可用的,这取决于这一代的最小空间大小。
Java SE 中用于并行收集器的计算现在用于所有的垃圾收集器。计算的一部分是 64 位平台的最大堆大小的上限。对于客户端 JVM 也有类似的计算,这会导致堆的最大空间小于服务器 JVM。
以下是关于服务器应用程序堆大小的一般准则:
除非你有暂停问题,否则请尝试向虚拟机授予尽可能多的内存。 默认大小通常太小。
将 -Xms 和 -Xmx 设置为相同的值可以从虚拟机中删除最重要的大小调整决策,从而提高可预测性。
如果你需要最小化应用程序的动态内存占用(执行过程中消耗的最大 RAM) ,那么可以通过最小化 Java 堆大小来实现这一点。
年轻代
除了总的可用内存之外,影响垃圾收集性能的第二个最重要的因素是专用于年轻代的堆的比例。
年轻代规模的选择
默认情况下,年轻代的大小由选项 -XX:NewRatio 控制。
例如,设置 -XX:NewRatio=3 意味着年轻代和老年代之间的比例为 1:3。 换句话说,伊甸园 和 幸存者空间的总和将是堆总大小的四分之一。
选项 -XX:NewSize 和 -XX:MaxNewSize 设置了年轻代的下限和上限。
这有助于以比 -XX:NewRatio 所允许的整数倍更细的粒度调优年轻代。
将这些值设置为相同的值可以固定年轻代,就像将 -Xms 和 -Xmx 设置为相同的值可以固定堆总大小一样。
幸存者空间调整
你可以使用选项 -XX:SurvivorRatio 来调整幸存者空间的大小,但这通常对性能并不重要。
例如, -XX:SurvivorRatio=6 将伊甸园和幸存者空间之间的比率设置为 1:6。 换句话说,每个幸存者的空间是伊甸园的 1/6,也就是年轻代的 1/8(不是 1/7,因为存在两个幸存者的空间)。
如果幸存者空间太小,那么复制收集将直接溢出到老年代中。
如果幸存者空间太大,那么它们就是无用的空。
在每次垃圾收集时,虚拟机都会选择一个阈值数字,这是一个对象在老化之前可以复制的次数。选择这个门槛是为了让幸存者保持半满状态。
你可以使用日志配置 -Xloggc、-verbose:gc,age 可用于显示此阈值以及新生成的对象的年龄。这对于观察应用程序的生命周期分布也很有用。
年轻代的最大空间是根据总堆的最大空间和 -XX:NewRatio 参数的值计算出来的。-XX:MaxNewSize 参数的默认值"not limited" 意味着计算值不受 -XX:MaxNewSize 的限制,除非在命令行上指定了 -XX:MaxNewSize 的值。
可用的收集器(Available Collectors)
Java HotSpot 虚拟机包含 3 种不同类型的收集器,每种收集器具有不同的性能特征。
串行收集器(Serial Collector)
并行收集器(Parallel Collector)
G1 收集器(Garbage-First Garbage Collector)
ZGC 收集器(Z Garbage Collector)
串行收集器(Serial Collector)
串行收集器使用单个线程执行所有垃圾收集工作,这使得它相对高效,因为线程之间没有通信开销。
它最适合于单处理器机器,因为它不能利用多处理器硬件,尽管它可以在多处理器上用于具有小数据集(大约 100MB)的应用程序。
在某些硬件和操作系统配置上,串行收集器是默认选择的,或者可以使用选项 -XX:+UseSerialGC 显式启用串行收集器。
并行收集器(Parallel Collector)
并行收集器也称为吞吐量收集器,它是一个类似于串行收集器的分代收集器。 串行和并行收集器之间的主要区别是,并行收集器有多个线程,用于加速垃圾收集。
并行收集器用于在多处理器或多线程硬件上运行的具有中等到大型数据集的应用程序。 您可以使用 -XX:+UseParallelGC 选项启用它。
并行压缩是使并行收集器能够并行执行 Major GC 的一个特性。
如果不进行并行压缩,Major GC 将使用单个线程执行,这将极大地限制可伸缩性。如果指定了-XX:+UseParallelGC 选项,则默认情况下启用并行压缩。
您可以使用 -XX:-UseParallelOldGC 选项禁用它。
通过命令行选项 -XX:+UseParallelGC 启用并行收集器。 默认情况下,使用此选项,次要(minor)和主要(major)回收都将并行运行,以进一步减少垃圾回收开销。
并行垃圾收集器线程数
可以使用命令行选项 -XX:ParallelGCThreads=<N> 控制垃圾收集器线程的数量。
G1 收集器(Garbage-First Garbage Collector)
G1 主要是一个并发收集器。大多数并发收集器并发执行一些代价高昂的工作到应用程序。 此收集器设计用于从小型机器扩展到大型具有大量内存的多处理器机器。 它提供了以高概率满足停顿时间目标的能力,同时实现高吞吐量。
在大多数硬件和操作系统配置中,默认选择 G1,或者可以使用 -XX:+UseG1GC 显式启用 G1。
Z 收集器(The Z Garbage Collector)
Z 垃圾收集器(ZGC)是一个可伸缩的低延迟垃圾收集器。ZGC 并发地执行所有昂贵的工作,而不停止应用程序线程的执行。
ZGC 适用于需要低延迟(少于 10 毫秒的暂停) 或 使用非常大的堆(TB 级)的应用程序。 可以通过使用 -XX:+UseZGC 选项启用。ZGC 是一个实验性的特性,从 JDK 11 开始。
选择收集器
如果需要,调整堆大小以提高性能。如果性能仍然不能达到你的目标,那么使用下面的准则作为选择收集器的起点:
如果应用程序有一个小的数据集(大约 100 MB) ,那么使用选项 -XX:+UseSerialGC 选择串行收集器,如果应用程序将在单处理器上运行,并且没有暂停时间要求,那么使用选项 -XX:+UseSerialGC 选择串行收集器。
如果(a)峰值应用程序性能是第一优先级,并且(b)没有暂停时间要求或者一秒或更长的暂停是可以接受的,那么让 虚拟机 选择收集器或者用 -XX:+UseParallelGC 选择并行收集器。
如果响应时间比总吞吐量更重要,并且垃圾收集暂停时间必须更短,那么选择主要并发的收集器,使用 -XX:+UseG1GC。
如果响应时间是一个高优先级,或者你正在使用一个非常大的堆,那么选择一个完全并发的收集器,使用 -XX:UseZGC。
这些准则只是选择收集器的起点,因为性能取决于堆的大小、应用程序维护的实时数据量以及可用处理器的数量和速度。
如果推荐的收集器没有达到预期的性能,那么首先尝试调整堆和分代大小,以满足预期的目标。 如果性能仍然不足,那么尝试另一个收集器: 使用并发收集器来减少暂停时间,并使用并行收集器来增加多处理器硬件上的总吞吐量。
小结:
如果应用程序是小数据集或是单处理器上运行,选择串行收集器。
如果吞吐量是第一优先级,而没有暂停时间要求,选择并行收集器。
如果响应时间比吞吐量更重要,选择 G1 收集器。
如果最关注响应时间,或者堆非常大(TB 级),则使用 Z 收集器。
版权声明: 本文为 InfoQ 作者【李浩宇/Alex】的原创文章。
原文链接:【http://xie.infoq.cn/article/71dcccbd882885795765a7aca】。文章转载请联系作者。
评论