☕【JVM 技术指南】「理论总结笔记」Java 虚拟机垃圾回收认知和调优的"思南(司南)"【下部】
承接上文
并行收集器
并行收集器(也称为吞吐量收集器)是类似于串行收集器的分代收集器。 串行和并行收集器之间的主要区别是,并行收集器有多个线程,用于加速垃圾回收。
通过命令行选项
-XX:+UseParallelGC
启用并行收集器。 默认情况下,使用此选项,次要(minor)和主要(Major GC)都将并行运行,以进一步减少垃圾回收开销。
并行垃圾收集器线程数
可以使用命令行选项
-XX:ParallelGCThreads=<N>
控制垃圾收集器线程的数量。
并行收集器中分代的排列
在并行收集器中,各代的排列方式是不同的。
并行收集器调优(Parallel Collector Ergonomics)
当使用 -XX:+UseParallelGC 选择并行收集器时,它支持自动调优方法,允许您指定行为,而不是分代大小和其他低级调优细节。
指定并行收集器行为的选项
最大垃圾收集暂停时间: 使用命令行选项
-XX:MaxGCPauseMillis=<N>
指定最大暂停时间目标,这被解释为需要毫秒或更少的暂停时间;默认情况下,没有最大暂停时间目标。如果指定了暂停时间目标,则会调整堆大小和与垃圾收集有关的其他参数,以使垃圾收集暂停时间短于指定值。
可能并不总是能够达到所需的暂停时间目标。
这些调整可能会导致垃圾收集器降低应用程序的总吞吐量。
吞吐量
吞吐量目标是根据执行垃圾回收所花费的时间与垃圾回收之外所花费的时间(称为应用程序时间)来度量的。目标由命令行选项 -XX:GCTimeRatio=<N> 指定,该选项将垃圾收集时间与应用程序时间的比率设置为 1 / (1 + N)。
例如, -XX:GCTimeRatio=19 设置了垃圾收集占总时间的 1/20 或 5%的目标。 默认值为 99,结果是垃圾回收时间的目标为 1%。
内存空间
使用选项 -Xmx<N> 指定最大堆内存占用,此外,收集器还有一个隐式目标,即在满足其他目标的情况下最小化堆的大小。
并行收集器目标的优先级
目标是最大暂停时间目标、吞吐量目标和最小占用空间目标,目标按照这个顺序实现:
首先实现最大暂停时间目标。只有在满足了这个要求之后,吞吐量目标才能实现。 同样,只有在前两个目标已经实现之后,才会考虑内存大小目标。
并行收集器默认堆大小
除非在命令行中指定了初始堆大小和最大堆大小,否则将根据计算机上的内存量计算它们。默认的最大堆大小是物理内存的 1/4,而初始堆大小是物理内存的 1/64。
默认分配给年轻代的最大空间是总堆大小的 1/3。
并行收集器初始和最大堆大小的规范
你可以使用选项和 -Xmx 指定初始堆大小和最大堆大小。
如果您知道应用程序需要多少堆才能正常工作,那么可以将 -Xms 和 -Xmx 设置为相同的值。
如果您不知道,那么 JVM 将开始使用初始堆大小,然后增加 Java 堆,直到找到堆使用量和性能之间的平衡。
要验证默认值,请使用 -XX:+PrintFlagsFinal
选项并在输出中查找 -XX:MaxHeapSize。
例如,在 Linux 上你可以运行以下命令:
过长的并行收集器时间和 OutOfMemoryError
如果在垃圾回收(GC)上花费了太多时间,并行收集器将抛出 OutOfMemoryError 错误。
如果超过 98% 的总时间用于垃圾回收,而回收的堆不到 2%,则抛出 OutOfMemoryError。此特性旨在防止应用程序在较长时间内运行,同时由于堆太小而几乎或根本没有进展。如果需要,可以通过向命令行添加选项
-XX:-UseGCOverheadLimit
来禁用此特性。
G1 垃圾收集器
G1 垃圾收集器的目标是将多处理器机器扩展到大量内存。
它试图以较高的概率满足垃圾收集暂停时间目标,同时实现较高的吞吐量而不需要进行配置。
G1 的目标是使用当前的目标应用程序和环境,在延迟和吞吐量之间提供最佳的平衡。
与吞吐量收集器相比,虽然 G1 收集器的垃圾收集暂停时间通常要短得多,但应用程序吞吐量也往往略低。
G1 是默认收集器。
启用 G1
G1 垃圾回收器是默认回收器,因此通常不需要执行任何其他操作,您可以通过在命令行上提供
-XX:+UseG1GC
来显式启用它。
基本概念
G1 是一个分代的、递增的、并行的、大部分并发的、stop-the-world 和疏散垃圾收集器,它监视每个 stop-the-world 暂停的时间目标。
与其他收集器类似,G1 将堆分为(虚拟的)年轻代和老年代。
空间回收的努力集中在年轻代身上,这样做效率最高,偶尔的空间回收在老年代中。
G1 的设计原则
G1 的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。因此,G1 并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。
G1 采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此 G1 天然就是一种压缩方案(局部压缩);
同时 G1 可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
G1 虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的 survivor(to space)堆做复制准备。G1 只有逻辑上的分代概念,或者说每个分区都可能随 G1 的运行在不同代之间前后切换;
G1 的收集都是 STW 的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。
应用程序停止的其他操作会花费更多时间,比如全局标记之类的整堆操作会与应用程序并行执行。 为了使 stop-the-world 在空间回收方面的停顿时间缩短,G1 逐步并行地进行空间回收。
G1 通过跟踪以前应用程序行为的信息和垃圾收集暂停来构建相关成本的模型,从而实现可预测性。它利用这个信息来计算停顿时所做的工作量。例如,G1 首先在效率最高的区域回收空间(这些区域大部分都是垃圾,因此取名为 G1)。
G1 主要通过撤离来回收空间: 在选定的内存区域内找到的活动对象被复制到新的内存区域,并在处理过程中对其进行压缩。在完成疏散之后,以前被活动对象占用的空间将被应用程序重用以进行分配。
G1 收集器不是实时收集器。它试图在更长的时间内以高概率实现设定的暂停时间目标,但在给定的暂停时间内并不总是绝对确定。
堆布局
G1 将堆划分为一组大小相同的堆区域 Region,每个区域都有一个连续的虚拟内存范围,Region 区域是内存分配和内存回收的单位。
在任何给定的时间,这些区域中的每一个都可以是空的(浅灰色) ,或者分配给特定的一代,年轻的或老年的。
当内存请求进入时,内存管理器分配空闲区域。内存管理器将它们分配给一个代,然后将它们作为可用空间返回给应用程序,应用程序可以将其分配给自己。
年轻代包含伊甸园区域(红色)和幸存者区域(红色带有"S")。这些区域提供了与其他收集器中的相应连续空间相同的功能,不同之处在于,在 G1 中,这些区域通常以非连续的模式布局在内存中。老区域(浅蓝色)组成了老年代。对于跨越多个区域的对象,老年代区域可能非常巨大(浅蓝色带"H")。
应用程序总是分配给年轻代,即伊甸园区域,但直接分配给老年代的大型对象除外。
垃圾回收周期
在较高的水平上,G1 收集器在两个阶段之间交替。只有年轻(young-only)阶段包含垃圾回收,这些垃圾回收会逐渐用老年代中的对象填充当前可用的内存。在空间回收阶段,除了处理年轻代的问题外,G1 逐步收回老年代的空间。然后循环重新开始,只有年轻的阶段。
下面的列表详细描述了 G1 垃圾收集周期的各个阶段,它们之间的停顿和过渡:
纯年轻(Young-only)阶段这个阶段从几个普通(Normal)的年轻代回收开始,将对象升级到老年代。 当老年代占有率达到一定阈值时,即初始堆占有率阈值,纯年轻(young-only)阶段和空间回收(space-reclamation)阶段开始转换。此时,G1 计划一个并发启动(Concurrent Start)年轻代回收,而不是普通(Normal)的年轻代回收。并发启动(Concurrent Start):这种类型的回收除了执行普通年轻代回收之外,还启动标记(marking)过程。重标记(Remark):此暂停将自行确定标记,执行全局引用处理和类卸载,回收完全空的区域并清理内部数据结构。清理(Cleanup):这个暂停决定了是否会真正进入空间回收阶段。空间回收(Space-reclamation)阶段:这一阶段包括多个混合(Mixed)回收,除了年轻代区域,还删除老一代区域的成套活动对象。当 G1 认为删除更多的老年代区域不会产生足够的自由空间时,空间回收阶段就结束了。
在空间回收之后,收集周期从另一个 young-only 的阶段重新开始。作为备份,如果应用程序在收集存活信息时耗尽了内存,G1 会像其他收集器一样执行就地 stop-the-world 的完全堆压缩(Full GC)。
G1 内部细节
Java 堆大小调整
G1 在调整 Java 堆大小时遵循标准规则,使用 -XX:InitialHeapSize 作为最小的 Java 堆空间, -XX:MaxHeapSize 作为最大的 Java 堆空间, -XX:MinHeapFreeRatio 作为最小的可用内存百分比, -XX:MaxHeapFreeRatio 用于确定调整大小后可用内存的最大百分比。 G1 收集器仅在执行重标记(Remark) 和 Full GC 暂停期间考虑调整 Java 堆的大小。 这个过程可以从操作系统释放内存或分配内存。
Young-Only 阶段代调整
G1 总是在下一个突变子阶段的正常年轻代回收结束时测量年轻代的大小。通过这种方式,G1 可以满足使用 -XX:MaxGCPauseTimeMillis 和 -XX:PauseTimeIntervalMillis 设置的暂停时间目标,该目标基于对实际暂停时间的长期观察。它考虑到了同样规模的年轻代需要多长时间才能删除。这包括在回收过程中需要复制多少对象以及这些对象之间的互联程度等信息。
如果没有其他限制,那么 G1 可以在 -XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent 确定的值之间自适应地调整年轻代大小,以满足暂停时间的要求。
或者,可以使用 -XX:NewSize 和 -XX:MaxNewSize 分别设置年轻代的最小值和最大值。
注意: 只指定后面这些选项中的一个,就可以将年轻代大小精确地固定为分别使用 -XX:NewSize 和 -XX:MaxNewSize 传递的值。这将禁用暂停时间控制。
空间回收阶段的代调整
在空间回收阶段,G1 试图在一次垃圾回收暂停中最大化在老年代中回收的空间量。 年轻年代的大小设置为允许的最小值,通常由-XX:G1NewSizePercent
确定。
周期性的垃圾收集
如果由于应用程序不活跃而导致长时间没有垃圾收集,那么虚拟机可能会长时间保留大量未使用的内存,这些内存可以在其他地方使用。
为了避免这种情况,可以强制 G1 使用 -XX:G1PeriodicGCInterval 选项执行常规垃圾收集。此选项确定 G1 考虑执行垃圾回收的最小间隔(毫秒)。
如果自以前任何垃圾收集暂停以来已经过去了这段时间,并且没有正在进行的并发循环,G1 将触发额外的垃圾回收。
确定初始堆占用率
启动堆占用百分比(Initiating Heap Occupancy Percent, IHOP)是触发初始标记回收的阈值,它被定义为老年代大小的百分比。
默认情况下,G1 通过在标记周期中观察标记需要多长时间以及在老年代中通常分配多少内存来自动确定最佳 IHOP。这个特性称为自适应 IHOP。
如果这个特性是活动的,那么选项 -XX:InitiatingHeapOccupancyPercent 确定初始值作为当前老年代代大小的百分比,只要没有足够的观测值来很好地预测启动堆占用阈值。
使用 -XX:-G1UseAdaptiveIHOP 选项关闭 G1 的此行为。 在这种情况下, -XX:InitiatingHeapOccupancyPercent 的值总是决定这个阈值。
标记
G1 标记使用一种称为“初始快照”(Snapshot-At-The-Beginning,SATB)的算法。 它在初始标记暂停时拍摄堆的虚拟快照,此时所有在标记开始时处于活动状态的对象都被认为在标记的剩余时间处于活动状态。这意味着,为了空间回收的目的(除了一些例外) ,在标记期间变为死的(不可到达的)对象仍然被认为是活的。与其他收集器相比,这可能会导致一些额外的内存被错误地保留。但是,SATB 可能在 Remark 暂停期间提供更好的延迟。在这个标记期间过于保守地考虑活动对象将在下一个标记期间被回收。
-XX:MaxGCPauseMillis=200 最大暂停时间的目标
-XX:GCPauseTimeInterval= 最大暂停时间间隔的目标。 默认情况下,G1 不设置任何目标,允许 G1 在极端情况下背靠背地执行垃圾收集。
-XX:ParallelGCThreads= 垃圾回收暂停期间用于并行工作的最大线程数。 这是根据虚拟机以下列方式运行的计算机的可用线程数得出的: 如果进程可用的 CPU 线程数少于或等于 8,则使用该线程。否则,使用线程数的 5/8。
-XX:ConcGCThreads=
-XX:+G1UseAdaptiveIHOP -XX:InitiatingHeapOccupancyPercent=45
-XX:G1HeapRegionSize=
-XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60
-XX:G1HeapWastePercent=5
-XX:G1MixedGCCountTarget=8
-XX:G1MixedGCLiveThresholdPercent=85
与其它收集器的比较
####### 这是 G1 与其他收集器之间主要区别的摘要:
并行 GC 只能作为一个整体压缩和回收老年代中的空间。
G1 增量地将这些工作分配到多个更短的回收中。这大大缩短了暂停时间,但是却降低了吞吐量。
G1 并发执行部分老年代空间回收。
G1 可能比上述收集器显示更高的开销,由于并发性而影响吞吐量。
ZGC 针对非常大的堆,目的是以更高的吞吐量成本提供更小的停顿时间。
由于它的工作原理,G1 有一些独特的机制来提高垃圾回收效率:
在任何回收过程中,G1 都可以回收老年代中一些完全空置的、大的区域。 这可以避免许多其他不必要的垃圾回收,不需要太多努力就可以释放大量空间 G1 可以选择尝试同时对 Java 堆上的重复字符串进行重复数据删除。从老年代回收空的大型对象始终处于启用状态。
您可以使用 -XX:-G1EagerReclaimHumongousObjects 选项禁用此功能。 默认情况下禁用字符串重复数据删除。 您可以使用选项 -XX:+G1EnableStringDeduplication 启用它。
Z 垃圾收集器
Z 垃圾收集器(ZGC)是一个可伸缩的低延迟垃圾收集器。ZGC 并发地执行所有昂贵的工作,而不需要停止应用程序线程的执行超过 10ms,这使得它适合于需要低延迟或使用非常大的堆(TB 级)的应用程序。
Z 垃圾收集器是一个实验性特性,可以通过命令行选项 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC 启用。
设置堆大小
ZGC 最重要的调优选项是设置最大堆大小(-Xmx)。
设置并发 GC 线程数
可能需要考虑的第二个调优选项是设置并发 GC 线程的数量(-XX:ConcGCThreads)。
显式垃圾回收
应用程序与垃圾回收交互的另一种方式是使用 System.gc() 显式调用 full 垃圾回收。
类元数据(Class Metadata)
Java 类在 Java Hotspot 虚拟机中有一个内部表示,称为类元数据。
版权声明: 本文为 InfoQ 作者【李浩宇/Alex】的原创文章。
原文链接:【http://xie.infoq.cn/article/a65b9c3b6aad1e550fa7ebb7e】。文章转载请联系作者。
评论