Java垃圾收集调优的逐步指南

原始地址:https://dev.to/sematext/a-step-by-step-guide-to-java-garbage-collection-tuning-2m1g

使用Java应用程序的优点

使用Java应用程序有很多好处,特别是与C/C++等语言相比。在大多数情况下,您可以在操作系统和各种环境之间实现互操作性。您可以将应用程序从服务器移动到服务器,从操作系统移动到操作系统,而无需大量工作或在极少数情况下进行轻微更改。
运行基于JVM的应用程序最有趣的好处之一是自动内存处理。当您在代码中创建一个对象时,它会分配到一个堆上,并保留在那里,直到从代码中引用它。当它不再需要时,它需要从内存中移除,为新对象腾出空间。在C或C++等编程语言中,内存的清理是由我们程序员在代码中手动完成的。在Java或Kotlin等语言中,我们不需要关心这一点- JVM会通过它的垃圾回收器自动完成。

垃圾回收调优是什么?

垃圾回收调优 是调整基于JVM的应用程序的启动参数以匹配所需结果的过程。不多不少。调整堆大小(即 -Xmx-Xms 参数)可能会很简单。这也是您应该从中开始的地方。或者,调优所有高级参数以调整不同的堆区域可能会更复杂。一切都取决于情况和您的需求。

为什么垃圾回收调优很重要?

清理我们的应用程序的JVM进程堆内存是不免费的。垃圾回收器需要分配资源来执行其工作。您可以想象,与处理应用程序的业务逻辑相比,CPU可能在处理扫描堆并删除无用数据时忙碌。
这就是为什么垃圾回收器尽可能高效地工作非常重要。垃圾回收过程可能是繁重的。在我们作为开发人员和顾问的工作中,我们曾见过垃圾回收器在60秒的时间窗口内工作了20秒的情况。这意味着33%的时间应用程序没有完成其工作-它在做清理工作。
我们可以预期线程会停止很短的时间。它们经常发生:
2019-10-29T10:00:28.879-0100: 0.488: 应用程序线程停止的总时间:0.0001006秒,停止线程所花的时间:0.0000065秒
然而,真正危险的是应用程序线程在很长一段时间内完全停止-比如几秒钟,甚至在极端情况下甚至是几分钟。这可能导致用户无法正常使用应用程序。由于元素无法及时响应,您的分布式系统可能会崩溃。
为了避免这种情况,我们需要确保运行在我们JVM应用程序上的垃圾回收器已经配置良好,并且尽可能地完成了它的工作。

什么时候进行垃圾回收调优?

首先,您应该知道调优垃圾回收应该是您所做的最后一项操作之一。除非您绝对确定问题出在垃圾回收上,否则不要从更改JVM选项开始。坦率地说,有很多情况下垃圾回收器的工作只是突显了更大的问题。
如果您的JVM内存利用率良好且垃圾回收器工作正常而不会引起问题,您不应该花时间进行垃圾回收调优。您很可能会通过重构代码使其更有效果,效率更高。
那么,如何判断垃圾回收器工作良好?我们可以查看我们的监控,例如我们自己的Sematext Cloud。它可以为您提供有关JVM内存利用率,垃圾回收器工作以及应用程序的整体性能的信息。例如,请查看以下图表:
在此图表中,您可以看到一种称为“鲨鱼牙齿”的现象。通常,它是JVM堆的一个健康迹象。内存的最大部分(称为旧代)被填满,然后由垃圾回收器清理。如果将此与垃圾回收器的时间配合使用,我们将看到整个情况。了解所有这些,我们可以判断出垃圾回收的工作是否令人满意,或者是否需要进行调优。
您还可以查看我们在了解Java GC日志博客文章中讨论的垃圾回收日志,或使用jstat等工具。它们将提供有关JVM的详细信息,特别是在涉及堆内存和垃圾回收方面发生情况时。
在考虑垃圾回收性能调优时,还有一点需要考虑。默认的Java垃圾回收器设置可能不适合您的应用程序。换句话说,与其购买更多的硬件或更强大的机器,您可能更希望查看内存是如何被管理的。有时候调优可以降低操作成本,降低开支,并在不增加环境的情况下实现增长。
一旦确定垃圾回收器有问题并希望开始优化它的参数,我们可以开始处理JVM启动参数。

垃圾回收调优过程:如何调优Java GC

在讨论调优垃圾回收器的过程时,您必须记住JVM世界中有多种可用的垃圾回收器。在处理较小的堆和旧版JVM(例如版本7、8或9)时,您很可能会使用良好且传统的Concurrent Mark Sweep垃圾回收器。对于较新的JVM版本(例如11),您可能正在使用G1GC。如果您喜欢尝试新事物,那么您可能正使用最新的JVM版本以及ZGC。您必须记住,每个垃圾回收器的工作方式都不同。因此,它们的调优过程也会有所不同。
使用具有不同垃圾回收器的基于JVM的应用程序是一回事,进行实验是另一回事。Java垃圾回收的调优需要进行大量的实验和尝试。通常情况下,在第一次尝试中,您不太可能获得期望的结果。您将希望逐一引入更改,并观察每个更改后应用程序和垃圾回收器的行为如何。
无论您对GC调优的动机是什么,我想要明确一件事。为了能够调优垃圾回收器,您需要能够看到它是如何工作的。这意味着您需要了解GC度量或GC日志,或者两者都有,这将是最好的解决方案。

开始GC调优

首先要观察应用程序的行为,了解填充内存空间的事件以及填充的空间。请记住:

  • 被分配到堆的Eden代的对象被移动到幸存者空间。
  • 被分配到幸存者空间的对象被移动到旧代,如果对象的计数足够高或增加了计数。
  • 被分配到旧代的对象将被忽略,不会被回收。
    您需要确保充分了解应用程序堆内部发生的情况,以及导致垃圾回收事件发生的原因。这将有助于了解应用程序的内存需求以及如何改进垃圾回收。
    那我们就开始调整吧。

堆大小

令人惊讶的是,当涉及到设置正确的堆大小时,经常会被忽视。作为咨询师,我们见过一些这样的情况,请相信我们。首先,请检查您的堆大小是否设置得很好。
在设置应用程序的堆时,应该考虑哪些因素?这当然取决于很多因素。有一些系统,例如Apache Solr或Elasticsearch,它们非常依赖I/O,并且可以共享操作系统文件系统缓存。在这种情况下,您应该尽量为操作系统保留尽可能多的内存,特别是如果您的数据很大的话。如果您的应用程序处理大量数据或执行大量解析操作,可能需要更大的堆。
无论如何,您应该记住,直到32GB的堆大小,您将从所谓的压缩普通对象指针中受益。普通对象指针或OOP是指向内存的64位指针。它们指向堆上的对象,让JVM能够引用这些对象。至少在不了解内部细节的情况下,它是如何工作的。
在32GB的堆大小之内,JVM可以压缩这些OOP,从而节省内存。这是您可以将JVM世界中的压缩普通对象指针想象为以下方式:
前32位用于实际内存引用,并存储在堆上。32位足以在32GB的堆上寻址每个对象。我们如何计算这个呢?我们有232-一个32位指针可以寻址的空间。由于指针尾部有三个零,我们有232+3,得到235,可以寻址的内存空间为32GB。这是可以使用压缩普通对象指针的最大堆大小。
超过32GB的堆将导致JVM使用64位指针。在某些情况下,从32GB增加到35GB的堆,您可能期望有更多或更少相同数量的可用空间。这取决于您的应用程序内存使用情况,但您需要考虑到这一点,可能需要将堆大小增加到超过35GB才能看到差异。
最后,如何选择适当的堆大小?通过设置最小和最大大小。使用*-Xms* JVM参数设置最小大小,使用*-Xmx参数设置最大大小。例如,要将应用程序的堆大小设置为2GB,我们将在应用程序启动参数中添加-Xms2g -Xmx2g*。在大多数情况下,我还会将它们设置为相同的值,以避免在堆大小调整时发生碎片化。此外,我还会添加**-XX:+AlwaysPreTouch**标志以在应用程序启动时加载内存页面。
我们还可以使用*-Xmn属性来控制幸存者堆空间的大小,就像-Xms-Xmx*一样。这使我们可以在需要时明确定义幸存者堆空间的大小。

串行垃圾回收器

串行垃圾回收器是最简单的单线程垃圾回收器。您可以通过将**-XX:+UseSerialGC标志添加到JVM应用程序的启动参数中来启用**串行垃圾回收器。我们不会重点讨论调整串行垃圾回收器。

并行垃圾回收器

并行垃圾回收器与串行垃圾回收器在根本上相似,但使用多个线程对应用程序堆进行垃圾回收。您可以通过将**-XX:+UseParallelGC标志添加到JVM应用程序的启动参数中来启用并行垃圾回收器。要完全禁用它,请使用-XX:-UseParallelGC**标志。

调整并行垃圾回收器

如前所述,并行垃圾回收器使用多个线程来执行清理任务。垃圾回收器可以使用的线程数量由使用**-XX:ParallelGCThreads标志设置,该标志添加到我们的应用程序启动参数中。
例如,如果我们希望有4个线程执行垃圾回收,则可以将以下标志添加到我们的应用程序参数中:
-XX:ParallelGCThreads=4。请记住,您为清理任务分配的线程越多,它就越快。但是,拥有更多的垃圾回收线程也会有缺点。每个参与次要垃圾回收事件的GC线程都会为提升保留一部分旧代堆空间。这将创建空间和碎片化方面的分区。线程越多,碎片化越高。如果出现碎片化问题,减少Parallel垃圾回收线程的数量并增加旧代的大小将有助于解决碎片化问题。
可以使用
-XX:MaxGCPauseMillis进行第二次调优。它指定了两个连续垃圾回收事件之间的最大暂停时间目标。它以毫秒为单位进行定义。例如,使用标志-XX:MaxGCPauseMillis=100**,我们告诉并行垃圾回收器我们希望垃圾回收之间的最大暂停时间为100毫秒。垃圾回收之间的间隔越长,可以在堆上留下更多垃圾,使得下一次垃圾回收变得更加昂贵。另一方面,如果该值过小,则应用程序将花费大部分时间在垃圾回收上,而不是执行业务逻辑。
可以使用**-XX:GCTimeRatio标志设置最大吞吐量目标**。它定义了在GC中花费的时间与在GC之外花费的时间之间的比率。它被定义为1 /(1 + GC_TIME_RATIO_VALUE),并且是垃圾回收所花费时间的百分比。
例如,设置**-XX:GCTimeRatio=9意味着应用程序的工作时间中可能花费10%的时间用于垃圾回收。这意味着应用程序应该比分配给垃圾回收的时间多9倍。
默认情况下,
-XX:GCTimeRatio**标志的值由JVM设置为99,这意味着应用程序将比垃圾回收花费的时间多99倍,对于服务器端应用程序来说,这是一个不错的折衷方案。
您还可以控制并行垃圾回收器的各代调整。并行垃圾回收器的目标是:

  • 当达到暂停时间时,实现最大暂停时间
  • 当达到暂停时间时,达到吞吐量(仅当实现了暂停时间时)
  • 只有达到前两个目标后才实现占用堆(footprint)
    并行垃圾回收器通过增长和缩减各代堆来实现上述目标。每个堆都有自己的配置。增长和缩减堆通过固定百分比的增量进行。默认情况下,堆的增长以20%的增量进行,缩减以5%的增量进行。每个堆的增长百分比由**-XX:YoungGenerationSizeIncrement标志控制。旧代的增长由-XX:TenuredGenerationSizeIncrement标志控制。
    缩减部分可以通过
    -XX:AdaptiveSizeDecrementScaleFactor标志来控制。例如,对于幸存者代进行收缩的增量百分比由将-XX:YoungGenerationSizeIncrement标志值除以-XX:AdaptiveSizeDecrementScaleFactor标志值得到。
    如果暂停时间目标未实现,将逐个缩小各代堆。如果两个世代的暂停时间都高于目标,则首先缩减停止线程时间较长的世代。如果未达到吞吐量目标,则Young和Old两个世代都会增加。
    如果垃圾回收所花费的时间过长,且内存几乎没有或几乎没有被回收,则并行垃圾回收器可能会抛出OutOfMemory异常。默认情况下,如果花费的时间超过98%,而回收的堆空间不到2%,则会抛出此异常。如果要禁用此行为,可以添加标志
    -XX:-UseGCOverheadLimit**。但请注意,垃圾回收器长时间工作并且几乎不释放或几乎没有释放任何内存通常意味着您的堆大小太低,或者您的应用程序存在内存泄漏的问题。
    一旦您了解了所有这些,我们可以开始查看垃圾回收器日志。它们将告诉我们并行垃圾回收器执行的事件。这应该让我们基本了解何处开始调优以及堆的哪个部分不健康或可以改进。

并发标记扫描垃圾回收器

并发标记扫描垃圾回收器是一种几乎并行的实现,它与应用程序共享用于垃圾回收的线程。您可以通过在JVM应用程序启动参数中添加**-XX:+UseConcMarkSweepGC**标志来启用它。

调优并发标记扫描垃圾回收器

与JVM世界中的其他可用收集器一样,CMS垃圾回收器也是分代的,这意味着可以预期发生两种类型的事件-次要和主要回收。在正常工作期间,大部分垃圾回收是在不停止应用程序线程的情况下完成的。只有在运行的开始和中间期间,CMS才会停止线程以进行很短的时间。次要回收与并行垃圾回收器的方式非常相似-在垃圾回收期间停止了所有应用程序线程。
CMS垃圾回收器需要调优的信号之一是间歇性模式故障。这表明并发标记扫描垃圾回收器无法在旧代填满之前或在堆的旧代中没有足够碎片空间来提升对象时回收所有不可达对象。
但是并发性呢?让我们再花点时间来研究一下暂停。在并发阶段,CMS垃圾回收器有两个暂停时机。首个被称为初始标记暂停。它用于标记直接从根和堆中的其他位置直接可达的活动对象。第二个暂停称为再标记暂停,在并发追踪阶段结束时进行。它查找在初始标记暂停期间被忽略的对象,主要是因为在此期间进行了更新。并发跟踪阶段在这两个暂停之间进行。在此阶段,一个或多个垃圾回收器线程可以工作以清理垃圾。在整个周期结束后,并发标记扫描垃圾回收器在等待下一个周期时几乎不占用资源。然而,请注意,在并发阶段期间,您的应用程序可能会经历性能下降。
并发标记扫描垃圾回收器的旧代空间收集必须定时进行。因为并发模式失败可能是很昂贵的,我们需要适当地调整开始旧代空间清理的时间。我们可以使用**-XX:CMSInitiatingOccupancyFraction标志来做到这一点。它用于设置旧代堆利用率的百分比,在达到此百分比时CMS应开始清理。例如,从75%开始,我们将将该百分比设置为-XX:CMSInitiatingOccupancyFraction=75**。当然,这只是一个信息性的值,垃圾回收器仍然会使用启发式方法,并尝试确定最佳的开始旧代清理工作的值。要避免使用启发式方法,我们可以使用**-XX:+UseCMSInitiatingOccupancyOnly标志。这样,我们将仅遵循-XX:CMSInitiatingOccupancyFraction设置中的百分比。
因此,将
-XX:+UseCMSInitiatingOccupancyOnly标志设置为较高值将延迟堆上的旧代空间清除。这意味着在堆内存上没有足够的空间供新的对象使用时,您的应用程序将运行更长时间而不需要CMS清理。但是,当进程开始时,可能会更昂贵,因为它会增加工作量。另一方面,将-XX:+UseCMSInitiatingOccupancyOnly标志设置为较低值将使CMS十分频繁地清理旧代代空间,但速度可能更快。应根据应用程序进行调整选择的设置。
我们还可以告诉垃圾回收器在再标记暂停之前或在进行完整垃圾回收之前回收年轻代堆。第一个是通过在启动参数中添加
-XX:+CMSScavengeBeforeRemark标志来完成的。第二个是通过在应用程序启动参数中添加-XX:+ScavengeBeforeFullGC标志来完成的。结果是可以提高垃圾回收性能,因为它无需检查年轻代和旧代堆之间的引用。
并发标记扫描垃圾回收器的再标记阶段可能会加速它。默认情况下,它是单线程的,如前所述,我们提到了它停止了所有应用程序线程。通过将
-XX:+CMSParallelRemarkEnabled标志包含在应用程序启动参数中,我们可以强制使再标记阶段使用多个线程。但是,由于某些实现细节,实际情况并不总是并行版本的再标记阶段比单线程版本更快。这是您必须在您的环境中检查和测试的,因为这是非常依赖于使用情况的。但是请记住,仅逐一应用更改并比较结果,以便了解正在发生的情况。
与并行垃圾回收器类似,如果过多时间花在垃圾回收上,CMS垃圾回收器可能会抛出OutOfMemory异常。默认情况下,如果花费超过98%的时间用于垃圾回收,回收的堆空间不到2%,则会抛出此异常。如果我们想要禁用此行为,可以添加
-XX:-UseGCOverheadLimit**标志。然而,请注意,长时间工作且几乎没有或者根本没有释放内存的垃圾回收器通常意味着您的堆大小太低,或者您的应用程序有内存泄漏的问题。
一旦您知道了这些,我们可以开始查看垃圾回收器的日志。它们将告诉我们并发标记扫描垃圾回收器执行的事件。这应该给我们基本的想法,哪一部分堆不健康或可以进行改进。

G1垃圾回收器

G1垃圾回收器是最新的Java版本中针对延迟敏感应用程序的默认垃圾回收器。您可以通过在JVM应用程序启动参数中添加**-XX:+G1GC**标志来启用它。

调优G1垃圾回收器

还有两件值得一提的事情。G1垃圾回收器试图并行地执行更长时间的操作,而不会停止应用程序线程。当应用程序线程暂停时,快速操作将更快完成。因此,它是几乎并发的垃圾收集算法的另一种实现。
G1垃圾回收器以整理方式清理内存。从一个内存区域中复制和压缩活动对象,并且在此过程完成后,复制的对象来自的内存区域再次可用于对象分配。
在非常高的层次上,G1GC有两个阶段。第一个阶段称为仅年轻代,并专注于年轻代空间。在该阶段,对象逐渐从年轻代移动到旧代空间。第二个阶段称为空间回收,并在处理年轻代的同时,增量地回收旧代中的空间。让我们更仔细地看看这些阶段,因为其中有一些可以调整的属性。
仅年轻代阶段以几个年轻代收集开始,将对象提升到旧代。该阶段有效直到旧代空间达到某个阈值。默认情况下为45%利用率,我们可以通过设置**-XX:InitiatingHeapOccupancyPercent标志及其值来控制。一旦达到此阈值,G1会启动另一个年轻代收集,称为并发开始收集。-XX:InitiatingHeapOccupancyPercent标志控制初始标记收集的初始值,GC会进一步进行调整。要关闭这些调整,请将-XX:-G1UseAdaptiveIHOP标志添加到JVM启动参数中。
并发开始在常规年轻代收集中启动对象标记过程。它确定旧代空间中的所有活动可达对象,这些对象需要保留以进行以下空间回收阶段。为了完成标记过程,引入了两个附加步骤-备注和清理。两者暂停应用程序线程。备注步骤执行全局处理参考,类卸载,完全回收空区域并清理内部数据结构。清理步骤确定是否需要空间回收阶段。如果需要,则年轻代阶段结束时将以准备混合年轻代收集的方式结束,并启动空间回收阶段。
空间回收阶段包含多个混合的垃圾收集,可以在G1GC堆空间的年轻代和旧代区域之间工作。当G1GC看到疏散更多的旧代区域不会给出足够的空闲空间以使回收工作具有意义时,空间回收阶段将结束。可以使用
-XX:G1HeapWastePercent标志值来设置。
我们还可以在一定程度上控制周期性垃圾回收是否运行。通过使用
-XX:G1PeriodicGCSystemLoadThreshold标志,我们可以设置周期性垃圾回收不会运行的平均负载。例如,如果我们的系统在过去一分钟的负载为10,并设置-XX:G1PeriodicGCSystemLoadThreshold=10标志,则不会执行定期垃圾回收。
G1垃圾回收器除了*-Xmx* 和*-Xms*标志外,还允许我们使用一组标志来调整堆及其区域的大小。我们可以使用
-XX:MinHeapFreeRatio告诉垃圾回收器应达到的空闲内存比例,使用-XX:MaxHeapFreeRatio标志设置堆上的空闲内存的最大比例。我们还知道G1GC尝试保持年轻代大小在-XX:G1NewSizePercent-XX:G1MaxNewSizePercent的值之间。这也确定了暂停时间。减小大小可能会加快垃圾回收过程,但代价是工作量较少。我们还可以通过使用-XX:NewSize-XX:MaxNewSize标志设置年轻代的严格大小。
调优G1垃圾回收器的官方文档中说,总体而言,我们不应该调整它。最终,我们应该仅修改多个堆大小的所需暂停时间。这样做没错。但是,了解我们可以进行调整的内容以及如何进行调整,以及这些属性如何影响G1垃圾回收器的行为也很重要。
在调优
延迟方面,我们应尽量将暂停时间降到最低。大多数情况下,-Xmx 和*-Xms*值应设置为相同的值,并且我们还应该通过使用-XX:+Always

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值