选择最佳垃圾收集算法以提高Java性能


在本文中,我将解释垃圾回收如何在后台工作以释放内存。在过去的几个Java版本中,Java内存管理已经出现了很多。了解不同的GC算法将帮助您更好地对其进行调整(如果需要),具体取决于我们在许多基于Java的应用程序性能测试中看到的不同性能问题。当您的Java应用程序运行时,它将创建占用内存空间的对象。只要对象被使用(即应用程序在某处引用),它将占用内存。当不再使用该对象时(例如,当您彻底关闭数据库连接时),垃圾收集可以回收该对象占用的空间。

在进行任何基于Java的应用程序性能测试时,如何为您的用例选择最佳的垃圾收集器?在讨论这个问题之前,让我们先谈谈以下一些基本概念。

权衡的 当我们谈论垃圾收集时,我们通常需要考虑三件事。

1.记忆 这是分配给程序的内存量,称为堆内存。请不要将此与占用空间(GC算法运行所需的内存量)混淆。

2.吞吐量 我们需要了解的第二件事是吞吐量。吞吐量是代码运行的时间量与垃圾回收运行的时间量之和。例如,如果吞吐量为99%,则意味着代码运行的时间为99%,垃圾收集运行的时间为1%。对于任何大容量应用程序,我们都希望在运行的任何负载测试中都尽可能获得更高的吞吐量。

3.延迟 我们需要了解的第三个方面是延迟。延迟是指垃圾回收程序运行的时间,即程序停止运行多少时间,垃圾回收程序才能正常运行。所有这些都是以毫秒为单位进行测量的,但是取决于内存的大小和我们为负载测试选择的垃圾收集算法,它们可能长达几秒钟。理想情况下,我们希望延迟尽可能低或尽可能可预测。

垃圾收集的世代假设 这个假设说,大多数被创造的物体都是年轻的。当某个对象无法再访问时,该对象被标记为有资格进行垃圾回收,这可能在对象超出范围时发生。当为对象的参考变量分配一个显式的空值或将其重新初始化时,也会发生这种情况。如果无法访问对象,则意味着任何活动线程都无法通过程序中使用的任何引用变量来访问它。

垃圾回收算法将堆内存大小分为Young Generation和Old Generation。每当我们第一次创建对象时,它们都会被保留在Young代中,并且大多数会死掉,否则它们将很快成为垃圾回收的合格者,这就是为什么我们在Young一代中运行大量Garbage回收并且这个回收是称为次要GC。

如果存在像类级别变量这样的对象(也称为实例级别变量)保留了整个生命周期,并且保持了更长的时间,并且即使在进行了许多次较小的GC收集后,这些对象仍不符合垃圾收集的条件,它们仍会被提升为OLD一代。每当上一代中有很多对象时,就会触发专业GC。

GC算法步骤 任何垃圾收集算法都具有三个基本步骤:

标记 这是第一步,GC从所有引用的根节点开始遍历内存中的对象图,然后将多个对象标记为活动对象。标记阶段结束后,将标记每个活动对象。此标记阶段的持续时间取决于活动对象的数量,并且直接增加堆内存不会影响标记阶段的持续时间。

扫 不论触及到的那些对象中的哪个对象,未使用的对象都将被删除,并回收内存。

压实 压缩是按顺序排列所有内容的过程。此步骤是通过压缩内存来删除内存碎片,以便删除分配的内存区域之间的空白空间。

MARK和COPY算法 在年轻一代中,该空间通常分为EDEN空间和两个幸存者空间,生存空间1和生存空间2

内存中创建的所有新对象都首先分配给EDEN空间。每当次要GC运行时,只会标记此EDEN空间的活动对象并将其复制到幸存者空间,这涉及以下步骤

1.首先将所有对象标记为活动对象,这意味着仍在使用或引用并且不符合垃圾收集条件的对象

2.将所有活动对象复制到S1或S2中的SURVIVOR空间

一旦它复制了所有活动对象,则此EDEN空间将由已复制的对象组成,并且有资格进行垃圾回收的对象将擦除整个EDEN空间。

MARK SWEEP和COMPACT算法 这通常在老一代上运行。假设我们有很多分配的对象,其中一些是活动的,而有些则可以进行垃圾回收。首先,我们将仅标记活动对象。其次,我们将清除并删除所有符合垃圾回收条件的对象,然后将其删除并使其为空白,从技术上讲,我们不会删除这些空间,数据结构本身会更新,并说这些空间为空。第三方面是压缩,我们将所有仍在使用的活动对象移到左侧,并将所有活动对象聚在一起。这种方法的缺点是增加了GC暂停时间,因为我们需要将所有对象复制到新位置并更新对此类对象的所有引用。

压缩的优点是,当我们要分配新对象时,我们所要做的就是保持一个指针和引用,该指针和引用表示左侧的所有内容均已使用,右侧的所有内容均是免费的。

串行垃圾收集器(-XX:+ UseSerialGC) 串行收集器具有所有收集器中最小的占用空间。该垃圾收集器运行所需的数据结构占用的空间非常小。此收集器将单个线程用于次要和主要收集。串行收集器使用了凹凸指针压缩技术,这就是为什么分配要快得多的原因。通常,此收集器最适合在内存非常少的共享CPU上运行的应用程序。

假设我们有一个四核CPU,并且正在运行四个应用程序。如果您的垃圾收集器不是单线程的,而是多线程的,并且在某个时间点,我们的垃圾收集器将在CPU的四个内核上启动所有四个线程,并将整个CPU用作其自己的垃圾收集器,此时CPU上运行的其他应用程序将受到影响。如果在单个CPU上运行多个应用程序,并且我们必须确保垃圾收集器不会影响其他内核或应用程序,那么我们可以使用串行垃圾收集器。

并行/吞吐量收集器(-XX:+ UseParallelGC,-XX:+ UseParallelOldGC) 下一个要了解的收集器称为并行收集器。我们有并行收集器和并行旧收集器。我们通常只使用Parallel Old Collector,它同时为Minor GC和Major GC使用多个线程。该收集器不会与应用程序同时运行。之所以称为Parallel,是因为它本身具有Garbage集合的多个线程,并且所有这些线程并行运行,但是当Garbage收集器正在运行时,所有线程都将停止,并且如果我们的应用程序部署在多核或多处理器系统上,则该收集器将为我们提供最大的吞吐量

在最短的时间内,它将能够收集尽可能多的垃圾。它会停止整个应用程序,并且可能会停止一段时间,并且它是仅适用于批处理应用程序的最佳收集器。在批处理应用程序中,我们不在乎用户的响应时间,因为前端及其批处理应用程序没有用户在后台运行。对于批处理应用程序,Parallel Collector将是最佳的选择。

并发标记和扫描收集器(-XX:+ UseConcMarkSweepGC,-XX:+ UseParNewGC) 这称为并发标记和扫描。该收集器与应用程序同时运行以标记所有活动对象。应用程序必须停止的时间更少,因此应用程序的等待时间也更少。在实际的集合中,它仍然具有STW暂停。STW也称为“停止世界暂停”,这意味着它会在非常短的时间内停止应用程序以进行实际的垃圾收集。与并行收集器相比,此CMS收集器需要更多的占用空间,并且要处理的数据结构更多。它的吞吐量比Parallel Collector少,但优点是停顿时间少于Parallel Collector。该收集器是所有常规Java应用程序中最常用的收集器

G1收集器(-XX:+ UseG1GC)(垃圾优先) CMS收集器的改进称为G1收集器。收集器没有使用特定的年轻一代和老一代堆,而是使用了整个堆并将其划分为多个区域。它具有更多的占用空间,并且此收集器的优点是它具有最可预测的延迟,这是此收集器的最佳功能。当我们启动应用程序时,我们可以传递该变量,例如,我们的应用程序可以承受的最大暂停时间(maxTargetPauseTime)为10毫秒。G1收集器将尝试确保仅在10毫秒内完成垃圾收集,即使剩下一些垃圾,它也会在下一个周期中进行处理。如果我们想要可预测的延迟和暂停时间,则G1收集器将是最佳的收集器。

雪兰多亚收集器(-XX:+ UseShenandoahGC) 还有一个收集器,称为雪兰多(Shenandoah)收集器。该收集器是对G1收集器的改进,在G1收集器中,其占用的空间稍大一些,因此在后台需要更多的数据结构,但其延迟甚至低于G1收集器。

Shenandoah是一个超低的暂停时间垃圾收集器,它通过与正在运行的Java程序同时执行更多的垃圾收集工作来减少GC的暂停时间。CMS和G1都同时执行活动对象的标记。Shenandoah增加了并发压缩。

Epsilon收集器(-XX:+ UseEpsilonGC):JDK的“无所事事”收集器 JDK 11中引入的Epsilon垃圾收集器作为实验性收集器,仅分配内存。它无法释放任何分配的内存,因此应用程序很可能由于OutOfMemoryError崩溃。Epsilon收集器中的GC不会执行任何GC循环,因此不会在乎对象图,对象标记,对象复制等。一旦Java堆耗尽,就无法分配内存,也无法进行内存回收。测试将失败。

最显着的优势是没有GC开销,并且JVM不会暂停以清除内存,因为它甚至不尝试释放任何内存。Epsilon GC已添加为基准,以测试应用程序的性能,内存使用率,延迟和吞吐量改进。Epsilon Collector帮助我们计算Java虚拟机(JVM)耗尽所有内存并关闭所需的时间。Epsilon GC可帮助测试原始应用程序性能,而不会受到GC的干扰,也不会在代码中嵌入GC障碍。Epsilon GC功能在JDK 11中默认为禁用,我们必须启用才能使用此收集器。

对于对超延迟敏感的应用程序,要完全了解内存分配,内存占用量以及了解垃圾回收会对程序的性能有多大影响,Epsilon收集器是最好的选择。

Z垃圾收集器( -XX:+ UseZGC) Z垃圾收集器(ZGC)具有可伸缩性,且延迟低。这是一个全新的GC,是从头开始编写的。它可以同时标记内存,复制和重定位内存,并且可以与堆内存(从KB到大TB内存)一起使用。作为并发的垃圾收集器,ZGC保证即使对于更大的堆大小,也不会超过10毫秒的应用程序延迟。ZGC最初是在Java 11(Linux)中作为实验性GC发布的,随着时间的推移,预计JDK 11、13和14中会有更多更改。

停止世界的暂停仅限于ZGC中的根扫描。它使用带有彩色指针的负载屏障在线程运行时执行并发操作,并用于跟踪堆使用情况。彩色指针是ZGC的核心概念之一,它使ZGC可以查找,标记,定位和重新映射对象。与G1相比,ZGC具有更好的方法来处理非常大的对象分配,这在回收内存和重新分配内存方面是高性能的,并且它是单代GC。

ZGC将内存划分为多个区域,也称为ZPages。这些ZPage可以动态创建和销毁,也可以动态调整大小。与其他GC不同,ZGC的物理堆区域可以映射到更大的堆地址空间(可以包括虚拟内存),这可以避免内存碎片问题。

结论 通常,串行收集器用于小型设备,或者当我们要确保GC不会影响其他应用程序或CPU时,并行收集器最适合批处理应用程序,CMS收集器最适合常规应用程序,G1收集器最适合以下应用程序可预测的延迟,并且Shenandoah收集器是对G1的改进,我们可以将其用作几种Java版本中的默认收集器(来自Java 11)。Epsilon和ZGC收集器是从JDK 11引入的新的实验性收集器,从发行到发行,它们仍在进行很多更改。

感谢您阅读本文,并祝您学习愉快!


原文链接:http://codingdict.com