java分步式
使用Java应用程序有很多好处。 特别是与C / C ++之类的语言相比时。 在大多数情况下,您将获得操作系统与各种环境之间的互操作性。 您可以将应用程序从服务器移动到服务器,从操作系统移动到操作系统,而无需付出很大的努力,或者在极少数情况下只需进行少量更改即可。
运行基于JVM的应用程序最有趣的好处之一就是自动内存处理。 在代码中创建对象时,该对象将分配在堆上,并保持在那里,直到从代码中引用它为止。 当不再需要它时,需要将其从内存中删除以腾出空间容纳新对象。 在像C或C ++这样的编程语言中,内存的清除是由我们程序员(由程序员手动执行)在代码中完成的。 在Java或Kotlin之类的语言中,我们不需要照顾它-它是由JVM,其垃圾收集器自动完成的。
什么是垃圾回收调优?
垃圾回收GC调整是调整基于JVM的应用程序的启动参数以匹配所需结果的过程。 一无所有。 它可以像调整堆大小一样简单--Xmx和-Xms参数。 顺便说一句,您应该从这里开始。 或者,它可能像调整所有高级参数来调整不同堆区域一样复杂。 一切都取决于情况和您的需求。
为什么垃圾收集调优很重要?
清理应用程序的JVM进程堆内存不是免费的。 需要为垃圾收集器指定资源,以便垃圾收集器能够完成其工作。 您可以想象,CPU可能不忙于处理应用程序的业务逻辑,而是忙于处理从堆中删除未使用的数据。
这就是为什么垃圾收集器尽可能高效地工作至关重要的原因。 GC过程可能很繁重。 在我们作为开发人员和顾问的过程中,我们已经看到了垃圾收集器在60秒的时间内工作20秒的情况。 这意味着该应用程序有33%的时间没有执行任务,而是执行了客房整理。
我们可以预期线程会在很短的时间内停止。 它不断发生:
2019-10-29T10:00:28.879-0100: 0.488: Total time for which application threads were stopped: 0.0001006 seconds, Stopping threads took: 0.0000065 seconds
但是,危险的是很长时间内完全停止应用程序线程-例如几秒钟,在极端情况下甚至是几分钟。 这可能导致您的用户根本无法正确使用您的应用程序。 由于元素未及时响应,您的分布式系统可能崩溃。
为了避免这种情况,我们需要确保为JVM应用程序运行的垃圾收集器配置正确,并且正在尽其所能。
何时进行垃圾收集优化?
您应该知道的第一件事是,调整垃圾回收应该是您最后执行的操作之一。 除非您完全确定问题出在垃圾收集中,否则不要从更改JVM options开始。 直言不讳,在很多情况下,垃圾收集器的工作方式仅突出了一个更大的问题。
如果您的JVM内存利用率看起来不错并且您的垃圾收集器正常工作而不会造成麻烦,则您不应该花时间打开垃圾收集。 您很有可能在重构代码方面更加有效。
那么,怎么说垃圾收集器做得很好呢? 我们可以像我们自己的Sematext Cloud一样查看监视。 它将为您提供有关JVM内存使用率,垃圾回收器工作以及应用程序整体性能的信息。 例如,看下面的图表:
在此图表中,您可以看到称为“鲨鱼齿”的东西。 通常,这是健康的JVM堆的标志。 内存的最大部分(称为旧一代)被填满,然后由垃圾收集器清除。 如果我们将其与垃圾收集器的时间关联起来,我们将看到整个画面。 了解了所有这些信息后,我们可以判断我们是否对垃圾回收的工作方式感到满意或是否需要进行调整。
您可以查看的另一件事是我们在《 理解Java GC日志》博客文章中讨论的垃圾收集日志。 您还可以使用jstat或任何profiler之类的工具。 他们将为您提供有关JVM内部发生情况的详细信息,尤其是涉及堆内存和垃圾回收时。
在考虑垃圾回收性能调整时,还应该考虑一件事。 可以这么说,默认的Java垃圾收集设置可能不适用于您的应用程序。 意思是,您可能不想研究更多的硬件或更强大的机器,而是希望研究如何管理内存。 有时,调整可以降低运营成本,从而降低开支,并在不增加环境的情况下实现增长。
一旦确定了垃圾收集器是罪魁祸首,并且要开始优化其参数,就可以开始研究JVM启动参数了。
垃圾回收优化过程:如何调优Java GC
在谈论调整垃圾收集器时应该采取的过程时,必须记住,JVM世界中还有更多可用的垃圾收集器。 当处理较小的堆和较旧的JVM版本(例如版本7、8或9)时,您可能会为旧的堆使用良好的旧的Concurrent Mark Sweep垃圾收集器。 对于较新版本的JVM(例如11),您可能正在使用G1GC。 如果您想进行实验,则可能正在使用最新的JVM版本以及ZGC。 您必须记住,每个垃圾收集器的工作方式都不同。 因此,它们的调整过程将有所不同。
用不同的垃圾收集器运行基于JVM的应用程序是一回事,做实验是另一回事。 Java垃圾回收优化将需要大量的实验和尝试。 通常,您第一次尝试不会获得理想的结果。 您将要一一介绍更改,并观察每次更改后您的应用程序和垃圾收集器的行为。
无论您进行GC调整的动机是什么,我都想澄清一件事。 为了能够调整垃圾收集器,您需要能够了解其工作原理。 这意味着您需要对GC度量标准或GC日志(或两者都有)具有可见性,这将是最佳解决方案。
开始GC调整
首先查看您的应用程序的行为方式,哪些事件会占用内存空间,以及哪些空间会被填充。 请记住:
- 伊甸园世代中分配的对象被移到幸存者空间
- 如果计数器足够高或计数器增加,则幸存者空间中的已分配对象将移至“终身生成”。
- 权属生成中的已分配对象将被忽略,不会被收集。
您需要确保了解应用程序堆中正在发生的事情,并牢记导致垃圾回收事件的原因。 这将帮助您了解应用程序的内存需求以及如何改善垃圾回收。
让我们开始调整。
堆大小
您会惊讶于忽略正确的堆大小设置的频率。 作为顾问,我们相信其中一些人。 首先检查您的堆大小是否设置正确。
为应用程序设置堆时应考虑什么? 当然,这取决于许多因素。 像Apache Solr或Elasticsearch这样的系统在很大程度上依赖于I / O,并且可以共享操作系统文件系统缓存。 在这种情况下,应为操作系统保留尽可能多的内存,尤其是在您的数据很大的情况下。 如果您的应用程序处理大量数据或进行大量解析,则可能需要更大的堆。
无论如何,您应该记住,直到32GB的堆大小之前,您都将从所谓的压缩普通对象指针中受益。 普通对象指针或OOP是指向内存的64位指针。 它们指向允许JVM引用堆上对象的内存。 至少这是它的工作方式,而无需深入了解内部。
JVM的堆大小最大为32GB,可以压缩那些OOP,从而节省内存。 这是您如何想象JVM世界中压缩的普通对象指针:
前32位用于实际的内存引用,并存储在堆中。 32位足以解决高达32GB的堆中的每个对象。 我们如何计算呢? 我们有232个-我们的空间可以由32位指针寻址。 由于指针尾部的三个零,我们有232 + 3,即235,因此可以寻址32GB的内存空间。 那是我们可以使用压缩的普通对象指针的最大堆大小。
超过32GB的堆将导致JVM使用64位指针 。 在某些情况下,从32GB到35GB的堆,您可能会或多或少地拥有相同数量的可用空间。 这取决于您的应用程序内存使用情况,但是您需要考虑到这一点,并且可能要超过35GB才能看到差异。
最后,如何选择适当的堆大小? 好吧,监视您的使用情况,并查看堆的行为。 您可以为此使用监视功能,例如我们的Sematext Cloud及其JVM监视功能:
您可以看到JVM池大小和GC摘要表。 如您所见,JVM堆大小可以描述为鲨鱼齿–一种健康的模式。 根据第一个图表,我们可以为该应用程序至少需要500 – 600MB的内存。 在这种情况下,对于G1垃圾收集器,撤出内存的点约为总堆大小的1.2GB。 在这种情况下,我们使垃圾收集器在60秒的时间段内运行了大约2秒钟,这意味着JVM在垃圾收集上花费了大约2%的时间。 这是健康的。
我们还可以查看平均垃圾收集时间以及第99个百分点和第90个百分点:
根据这些信息,我们可以看到我们不需要更大的堆。 垃圾收集可以快速有效地清除数据。
另一方面,如果我们知道使用了我们的应用程序并处理了数据,那么它的堆就超过了我们为其设置的最大堆的70 – 80%,并且我们将看到GC在苦苦挣扎,我们知道自己有麻烦了。 例如,查看此应用程序的内存池:
您会看到某些事情开始发生,并且在旧时代空间中,内存使用率一直在80%以上。 将其与垃圾收集器工作相关联:
您可以清楚地看到内存利用率高的迹象。 未清除内存时,垃圾收集器开始执行更多工作。 这意味着即使JVM试图清除数据,也无法清除。 这预示着麻烦–我们只是堆上没有足够的空间来容纳新对象。 但是请记住,这也可能是应用程序内存泄漏的迹象。 如果您看到内存随着时间增长,并且垃圾回收无法释放内存,则可能是应用程序本身存在问题。 值得检查的东西。
那么我们如何设置堆大小? 通过设置其最小和最大大小。 最小大小使用-Xms JVM参数设置,最大大小使用-Xmx参数设置。 例如,要将应用程序的堆大小设置为2GB ,可以在应用程序启动参数中添加-Xms2g -Xmx2g 。 在大多数情况下,我还将它们设置为相同的值以避免堆大小调整,此外,我还将添加-XX:+ AlwaysPreTouch标志,以便在应用程序启动时将内存页面加载到内存中。
我们也可以使用-Xmn属性来控制年轻一代堆空间的大小,就像-Xms和-Xmx一样 。 这使我们可以在需要时明确定义年轻代堆空间的大小。
串行垃圾收集器
串行垃圾收集器是最简单的单线程垃圾收集器。 您可以通过将-XX:+ UseSerialGC标志添加到JVM应用程序启动参数来打开 串行垃圾收集器。 我们不会专注于调整串行垃圾收集器。
并行垃圾收集器
并行垃圾收集器的本质类似于串行垃圾收集器,但是使用多个线程在应用程序堆上执行垃圾收集。 您可以通过将-XX:+ UseParallelGC标志添加到JVM应用程序启动参数来打开Parallel垃圾收集器。 要完全禁用它,请使用-XX:-UseParallelGC标志。
调整并行垃圾收集器
如前所述,Parallel垃圾收集器使用多个线程来执行其清理任务。 通过使用添加到应用程序启动参数中的-XX:ParallelGCThreads标志来设置垃圾收集器可以使用的线程数。
例如,如果我们希望4个线程进行垃圾回收,则可以在应用程序参数中添加以下标志: -XX:ParallelGCThreads = 4 。 请记住,您专用于执行清洁任务的线程越多,可以获得的速度就越快。 但是,拥有更多的垃圾回收线程还有一个弊端。 次要垃圾回收事件中涉及的每个GC线程都将保留一部分使用期限的生成堆以进行升级。 这将造成空间和碎片的划分。 线程越多,碎片越大。 如果这成为问题,那么减少并行垃圾收集线程的数量并增加旧一代的大小将有助于解决碎片问题。
可以使用的第二个选项是-XX:MaxGCPauseMillis 。 它指定两个连续的垃圾回收事件之间的最大暂停时间目标 。 它以毫秒为单位定义。 例如,使用标志-XX:MaxGCPauseMillis = 100,我们告诉Parallel垃圾收集器我们希望两次垃圾收集之间的最大暂停时间为100毫秒。 垃圾回收之间的间隔越长,堆中剩余的垃圾就越多,这会使下一个垃圾回收变得更加昂贵。 另一方面,如果值太小,则应用程序会将其大部分时间花费在垃圾回收上,而不是执行业务逻辑。
可以使用-XX:GCTimeRatio标志来设置最大吞吐量目标 。 它定义了花费在GC中的时间与花费在GC 外的时间之间的比率 。 它定义为1 /(1 + GC_TIME_RATIO_VALUE) ,它是垃圾回收所花费时间的百分比。
例如,设置-XX:GCTimeRatio = 9意味着可以将应用程序10%的工作时间用于垃圾回收。 这意味着应用程序的工作时间应该比垃圾回收的时间长9倍。
默认情况下,JVM将-XX:GCTimeRatio标志的值设置为99,这意味着与垃圾回收相比,应用程序将获得99倍的工作时间,这对于服务器端应用程序而言是一个很好的折衷方案 。
您还可以控制并行垃圾收集器的生成调整。 并行垃圾收集器的目标如下:
- 达到最大暂停时间
- 仅在达到暂停时间时才能达到吞吐量
- 仅在实现前两个目标的情况下才能实现足迹
并行垃圾收集器不断壮大,缩小了世代以实现上述目标。 世代的增长和收缩以固定的百分比递增。 默认情况下,世代以20%的增量增长,而以5%的增量缩减。 每个世代都单独配置。 世代的增长百分比由-XX:YoungGenerationSizeIncrement标志控制。 上一代的增长由-XX:TenuredGenerationSizeIncrement标志控制。
收缩部分可以通过-XX:AdaptiveSizeDecrementScaleFactor标志进行控制。 例如,通过将-XX:YoungGenerationSizeIncrement标志的值除以-XX:AdaptiveSizeDecrementScaleFactor的值来设置年轻代的收缩增量百分比。
如果没有达到停顿时间的目标,那么世代将被缩减。 如果两代的暂停时间都超过了目标,则导致线程停止较长时间的那一代将首先缩小。 如果没有达到产量目标,那么年轻一代和老一代都会长大。
如果在垃圾回收中花费太多时间,则并行垃圾回收器可能会抛出OutOfMemory异常。 默认情况下,如果超过98%的时间用于垃圾回收并且少于2%的堆被恢复,则将抛出此异常。 如果要禁用该行为,可以添加-XX:-UseGCOverheadLimit标志。 但是请注意,垃圾收集器需要花费大量时间,并且几乎清除或几乎没有清除内存,这通常意味着堆大小太小或应用程序遭受内存泄漏的困扰。
了解了所有这些信息之后,我们就可以开始查看垃圾收集器日志了 。 他们将告诉我们Parallel垃圾收集器执行的事件。 这应该为我们提供了从哪里开始调整以及堆的哪一部分不正常或可以使用一些改进的基本思想。
并发标记扫描垃圾收集器
并发标记扫描垃圾回收器,这是一个大多数并发的实现,与应用程序共享用于垃圾回收的线程。 您可以通过将-XX:+ UseConcMarkSweepGC标志添加到JVM应用程序启动参数中来打开它。
调整并发标记扫描垃圾收集器
与JVM世界中其他可用的收集器类似,CMS垃圾收集器是世代的,这意味着您可以预期会发生两种类型的事件-次要和主要收集。 这里的想法是,大多数工作将与应用程序线程并行完成,以防止使用权产生的一代人吃饱。 在正常工作期间,大多数垃圾收集是在不停止应用程序线程的情况下完成的。 在主要收集期间,CMS仅在收集的开始和中间停止线程很短的时间。 次要收集的完成方式与Parallel垃圾收集器的工作方式非常相似-所有应用程序线程在GC期间均已停止。
CMS垃圾收集器需要调整的信号之一是并发模式故障 。 这表明并发标记清除垃圾收集器无法在旧的一代填满之前回收所有无法访问的对象,或者堆使用的一代中根本没有足够的零散空间来提升对象。
但是我们提到的并发呢? 让我们回到停顿片刻。 在并发阶段,CMS垃圾收集器会暂停两次。 第一个称为初始标记暂停 。 它用于标记可以从根以及堆中任何其他位置直接访问的活动对象。 第二个称为备注暂停的暂停是在并发跟踪阶段结束时完成的。 它查找在初始标记暂停期间丢失的对象,这主要是因为同时进行了更新。 并发跟踪阶段在这两个暂停之间完成。 在此阶段中,一个或多个垃圾收集器线程可能正在工作以清除垃圾。 整个周期结束后,并发标记清除垃圾收集器将等到下一个周期,而几乎不消耗任何资源。 但是,请注意,在并发阶段,您的应用程序可能会性能下降。
使用CMS垃圾收集器时,必须对临时生成空间的收集进行计时 。 由于并发模式失败的 代价可能很高,因此我们需要适当地调整 旧堆清理的开始 ,以免发生此类事件。 我们可以使用-XX:CMSInitiatingOccupancyFraction标志来实现。 它用于设置CMS应该开始清除 旧堆利用率的百分比 。 例如,从75%开始,我们将提到的标志设置为-XX:CMSInitiatingOccupancyFraction = 75 。 当然,这只是一个有用的值,垃圾收集器仍将使用启发式方法,并尝试确定最佳值以开始其旧的清理工作。 为了避免使用启发式方法,我们可以使用-XX:+ UseCMSInitiatingOccupancyOnly标志。 这样,我们将仅坚持-XX:CMSInitiatingOccupancyFraction设置中的百分比。
因此,将-XX:+ UseCMSInitiatingOccupancyOnly标志设置为更高的值时,会延迟清理堆上的旧一代空间。 这意味着您的应用程序将运行更长的时间,而无需启动CMS来清除使用权空间。 但是,当该过程开始时,它可能会更加昂贵,因为它将进行更多的工作。 另一方面,将-XX:+ UseCMSInitiatingOccupancyOnly标志设置为较低的值将使CMS终身代清洁更频繁,但可能更快。 选择哪种-取决于您的应用程序,需要根据用例进行调整。
我们还可以告诉我们的垃圾收集器在备注暂停期间或执行Full GC之前收集年轻一代的堆。 首先通过将-XX:+ CMSScavengeBeforeRemark标志添加到我们的启动参数中来完成。 第二步是通过在应用程序启动参数中添加-XX:+ ScavengeBeforeFullGC标志来完成的。 结果,它可以提高垃圾回收的性能,因为它不需要检查新老堆空间之间的引用。
并发标记扫描垃圾回收器的标记阶段可以潜在地加快它的速度。 默认情况下,它是单线程的,您记得我们提到过它会停止所有应用程序线程。 通过在应用程序启动参数中包含-XX:+ CMSParallelRemarkEnabled标志,我们可以强制备注阶段使用多个线程。 但是,由于某些实现细节,与单线程版本相比,备注阶段的并发版本实际上并不会总是更快。 这是您必须在环境中检查和测试的内容。
与并行垃圾收集器类似,如果在垃圾收集上花费太多时间 ,并发标记扫描垃圾收集器可能会抛出OutOfMemory异常。 默认情况下,如果超过98%的时间用于垃圾回收并且少于2%的堆被恢复,则将引发异常。 如果要禁用该行为,可以添加-XX:-UseGCOverheadLimit标志。 与Parallel垃圾收集器相比,差异在于,仅当应用程序线程 停止时才计入98%的时间 。
G1垃圾收集器
G1垃圾收集器,这是针对延迟敏感型应用程序的最新Java版本中的默认垃圾收集器。 您可以通过将-XX:+ G1GC标志添加到JVM应用程序启动参数中来打开它。
调整G1垃圾收集器
还有两件事值得一提。 G1垃圾收集器尝试在不停止应用程序线程的情况下并行执行更长的操作。 当应用程序线程暂停时,快速操作将更快地执行。 因此,它是大多数并发垃圾回收算法的另一种实现。
G1垃圾收集器主要以疏散方式清理内存。 将一个存储区域中的活动对象复制到新区域,并在此过程中进行压缩。 该过程完成后,从中复制对象的存储区再次可用于对象分配。
在非常高的水平上,G1GC在两个阶段之间进行。 第一阶段称为“ 仅限年轻人” ,重点关注年轻人时代。 在此阶段中,对象逐渐从年轻一代移动到老一代空间。 第二阶段称为空间回收 ,它在老一代中逐步回收空间,同时也照顾年轻一代。 让我们仔细看看这些阶段,因为我们可以在其中调整一些属性。
“ 仅年轻”阶段开始于一些年轻一代的收藏,这些收藏将对象推广到终身一代。 该阶段一直有效,直到老一代空间达到某个阈值为止。 默认情况下,利用率为45%,我们可以通过设置-XX:InitiatingHeapOccupancyPercent标志及其值来控制它。 达到该阈值后,G1将启动另一个年轻代集合,一个称为并发开始 。 控制初始标记收集的-XX:InitiatingHeapOccupancyPercent标志是由垃圾收集器进一步调整的初始值。 要关闭调整,请在您的JVM启动参数中添加-XX:-G1UseAdaptiveIHOP标志。
除了正常的年轻代集合外, 并发启动还启动对象标记过程。 它可以确定旧空间中所有可活动的,可到达的物体,这些物体在接下来的空间开垦阶段需要保留。 为了完成标记过程,引入了两个附加步骤-标记和清理。 他们两个都暂停应用程序线程。 备注步骤执行引用的全局处理,类卸载,完全回收空区域并清理内部数据结构。 清理步骤确定是否需要空间回收阶段。 如果需要,仅准备阶段将以“ Prepared Mixed”项目的年轻集合结束,然后启动空间回收阶段。
空间回收阶段包含多个混合垃圾回收,这些回收垃圾回收同时在G1GC堆空间的年轻代和老一代区域中工作。 当G1GC看到撤离更多的旧一代地区不会提供足够的自由空间以进行有价值的回收工作时,太空回收阶段结束。 可以使用-XX:G1HeapWastePercent标志值进行设置。
我们也可以至少在一定程度上控制是否会运行定期垃圾回收。 通过使用-XX:G1PeriodicGCSystemLoadThreshold标志,我们可以设置将不运行定期垃圾回收的平均负载。 例如,如果我们的系统在最后一分钟的负载为10,并且设置了-XX:G1PeriodicGCSystemLoadThreshold = 10标志,则将不执行期间垃圾回收。
除了-Xmx和-Xms标志之外,G1垃圾收集器还允许我们使用一组标志来确定堆及其区域的大小。 我们可以使用-XX:MinHeapFreeRatio来告诉垃圾回收器应实现的可用内存的比率,并可以使用-XX:MaxHeapFreeRatio标志来设置所需的堆上可用内存的最大比率。 我们也知道G1GC试图将年轻代大小保持在-XX:G1NewSizePercent和-XX:G1MaxNewSizePercent的值之间。 这也决定了暂停时间。 减小大小可能会减少工作量,从而加快垃圾收集过程。 我们还可以使用-XX:NewSize和-XX:MaxNewSize标志来设置年轻一代的严格大小。
有关调整G1垃圾收集器的文档说,我们一般不应该接触它。 最终,我们应该只为不同的堆大小修改所需的暂停时间。 很公平。 但是,知道如何以及如何调整以及这些属性如何影响G1垃圾收集器的行为也很不错。
在调整垃圾收集器的延迟时,我们应将暂停时间保持在最短。 这意味着在大多数情况下, -Xmx和-Xms值应设置为相同的值,并且在应用程序启动期间,我们还应使用-XX:+ AlwaysPreTouch标志来加载内存页面。
如果仅年轻阶段花费的时间太长,则表明减小-XX:G1NewSizePercent (默认为5)值是个好主意。 在某些情况下,减小-XX:G1MaxNewSizePercent (默认值为60)也可以有所帮助。 如果“混合”集合花费的时间太长,建议增加-XX:G1MixedGCCountTarget标志的值,以将保有期限的 GC分布到更多集合中。 增加-XX:G1HeapWastePercent可以更早停止旧的垃圾回收。 您还可以更改-XX:G1MixedGCLiveThresholdPercent-默认值为65,并控制占用阈值,高于该阈值将在混合集合中包括旧堆。 增大此值将告诉垃圾收集在进行混合收集时忽略较少的旧一代空间区域。 其中包含许多对象的区域需要更长的时间来收集垃圾。 通过使用上述标记,我们可以避免将这些区域设置为垃圾收集的候选对象。 如果看到较高的更新和扫描RS时间,则减小-XX:G1RSetUpdatingPauseTimePercent标志值(包括-XX:-ReduceInitialCardMarks标志)和增大-XX:G1RSetRegionEntries标志可能会有所帮助。 还有一个附加标志-XX:MaxGCPauseTimeMillis (默认为250),用于定义所需的最大暂停时间。 如果您想减少暂停时间,则降低此值也可能有所帮助。
在调整吞吐量时,我们希望垃圾收集器清理尽可能多的垃圾。 通常在处理和保存大量数据的系统中。 您应该做的第一件事是增加-XX:MaxGCPauseMillis值。 通过这样做,我们可以放松垃圾收集器。 这使它可以工作更长的时间来处理堆上的更多对象。 但是,这可能还不够。 在这种情况下,增加-XX:G1NewSizePercent标志值应该会有所帮助。 在某些情况下,吞吐量可能受年轻生成区域的大小限制-在这种情况下,增加-XX:G1MaxNewSizePercent标志值应该会有所帮助。
我们还可以减少需要CPU大量工作的并行性。 使用-XX:G1RSetUpdatingPauseTimePercent标志并增加其值将使应用程序线程暂停时可以进行更多工作,并减少在该阶段的并行部分花费的时间。 与延迟调整类似,您可能希望将-Xmx和-Xms标志保持相同的值,以避免调整堆大小。 使用-XX:+ AlwaysPreTouch标志和-XX:+ UseLargePages标志将内存页加载到内存中。 但是请记住,逐一应用更改并比较结果,以便您了解发生了什么。
最后,我们可以调整 堆大小 。 我们可以在这里考虑一个选项,即-XX:GCTimeRatio (默认为12)。 它确定了垃圾回收与应用程序线程完成工作所花费的时间之比,并计算为1 /(1 + GCTimeRatio) 。 默认值将导致大约8%的应用程序工作时间花费在垃圾收集上,这比Parallel GC多。 更多的垃圾回收时间将允许清除堆上的更多空间,但这在很大程度上取决于应用程序,因此很难给出一般建议。 尝试找到适合您需求的价值。
G1垃圾收集器还有一些通用的可调参数。 使用此垃圾收集器时,我们可以控制并行度。 通过包含-XX:+ ParallelRefProcEnabled标志并更改-XX:ReferencesPerThread标志值。 对于-XX:ReferencesPerThread标志定义的每N个引用,将使用单个线程。 将此值设置为0将告诉G1垃圾收集器始终使用-XX:ParallelGCThreads标志值指定的线程数。 要进行更多的并行化,请减小-XX:ReferencesPerThread标志值。 这应该加快垃圾收集的并行部分。
Z垃圾收集器
仍处于试验阶段,可扩展性强且延迟低。 如果你想和是Z垃圾收集器进行试验则必须使用JDK 11或更高版本,并添加-XX:+ UseZGC标志你的应用程序的启动参数与沿-XX:+ UnlockExperimentalVMOptions标志作为Z垃圾收集,仍处于实验。
调整Z垃圾收集器
当涉及到Z垃圾收集器时,没有太多参数可供我们使用。 如文档所述,这里最重要的选项是最大堆大小,因此是-Xmx标志。 由于Z垃圾收集器是并发收集器,因此必须以一种可以调整堆大小的方式来调整堆大小,以使其可以容纳应用程序的实时对象集,并在垃圾收集器运行时留出余量以允许分配。 这意味着与其他垃圾回收器相比,堆大小可能需要更大,并且分配给堆的内存越多,您可能期望垃圾回收器获得更好的结果。
您可以期望的第二个选项当然是Z垃圾收集器将使用的线程数。 毕竟,它是一个并发收集器,因此它可以利用多个线程。 我们可以使用-XX:ConcGCThreads标志来设置Z垃圾收集器将使用的线程数。 收集器本身使用试探法选择应该使用的适当线程数,但是通常,它高度依赖于应用程序,在某些情况下,将该数目设置为静态值可能会带来更好的结果。 但是,这需要进行测试,因为它非常取决于用例。 不过,有两件事要记住。 如果为垃圾回收器分配了太多线程,则您的应用程序可能没有足够的计算能力来完成其工作。 将垃圾收集器线程数设置为较低的数目,可能无法以足够快的速度收集垃圾。 调整时要考虑到这一点。
其他JVM选项
关于垃圾收集参数以及它们如何影响垃圾收集,我们已经介绍了很多内容。 但是,并非一切。 除此之外,还有更多的方法。 当然,我们不会谈论每个参数,只是没有意义。 但是,您还应该了解几件事。
JVM统计信息导致长垃圾回收暂停
有人报告说,在Linux系统上,在I / O利用率很高的情况下,垃圾回收会使线程暂停很长一段时间。 这可能是由JVM使用称为hsperfdata的内存映射文件引起的。 该文件写在/ tmp目录中,用于保存统计信息和安全点。 提到的文件在GC期间更新。 在Linux上,可以阻止修改内存映射文件,直到I / O完成。 可以想象,这样的操作可能需要更长的时间,大概是数百毫秒。
如何在您的环境中发现此类问题? 您需要调查垃圾回收的时间。 如果您在垃圾收集日志中看到JVM花费在垃圾收集上的实时时间远长于用户和系统指标的总和,那么您就有潜力了。 例如:
[Times: user=0.13 sys=0.11, real=5.45 secs]
如果系统很大程度上基于I / O,并且看到上述行为,则可以将GC日志和tmpfs的路径移至快速的SSD驱动器。 在最新的JDK版本中,Java使用的临时目录是硬编码的,因此我们不能使用-Djava.io.tmpdir进行更改。 您还可以在JVM应用程序参数中包含-XX:+ PerfDisableSharedMem标志。 您需要注意,包括该选项将破坏使用hsperfdata文件中统计信息的工具。 例如,jstat将不起作用。
您可以在Linkedin工程团队的博客文章中阅读有关此问题的更多信息。
内存不足异常时发生堆转储
在处理内存不足异常,诊断其原因并调查诸如内存泄漏之类的问题时,非常有用的一件事就是堆转储。 堆转储基本上是一个文件,堆的内容写在磁盘上。 我们可以根据需要生成堆转储,但是这会花费时间并冻结应用程序,或者在最佳情况下会使其变慢。 但是,如果我们的应用程序崩溃,我们将无法获取堆转储-它已经消失了。
为了避免丢失有助于我们诊断问题的信息,我们可以指示JVM在发生OutOfMemory错误时创建堆转储。 我们通过包含-XX:+ HeapDumpOnOutOfMemoryError标志来实现。 我们还可以通过使用-XX:HeapDumpPath标志并将其值设置为要将堆转储写入的位置来指定堆的存储位置。 例如: -XX:HeapDumpPath = / tmp / heapdump.hprof 。
请记住,堆转储文件可能很大–与堆大小一样大。 因此,在设置文件写入路径时需要考虑到这一点。 我们已经看到了JVM无法在目标文件系统上写入64GB堆转储文件的情况。
要分析文件,可以使用一些工具。 有诸如MAT之类的开源工具和诸如YourKit Java Profiler或JProfiler之类的专有工具。 还有诸如heaphero.io之类的服务可以帮助您进行分析,而Oracle JDK发行版的较早版本随附jhat(Java堆分析工具) 。 选择一个您喜欢并适合您的需求。
使用-XX:+ AggressiveOpts
-XX:+ AgressiveOpts标志打开了其他标志,这些标志被证明可以在一组基准测试期间提高性能。 这些标志可以随版本而变化,并且包含诸如大型自动装箱缓存和删除主动式自动装箱之类的选项。 它还包括禁用偏置锁定延迟。 你应该使用这个标志吗? 这取决于您的用例和生产系统。 像往常一样,在您的环境中进行测试,比较带有和不带有标志的实例,并查看其差异有多大。
结论
调整垃圾回收并非易事。 它需要知识和理解。 您需要了解正在使用的垃圾收集器,并且需要了解应用程序的内存需求。 每个应用程序都是不同的,并且具有不同的内存使用模式,因此需要不同的垃圾回收策略。 这也不是一项快速的任务。 在迭代中进行改进将花费时间和资源,这些迭代将向您显示每次变更的方向是否正确。
记住,在调优JVM世界中的垃圾收集器时,我们只是碰到了冰山一角。 我们仅提到了可以打开/关闭和调整的有限数量的可用标志。 有关其他背景知识和学习,建议您阅读《 Oracle HotSpot VM垃圾收集优化指南》,并阅读您认为可能感兴趣的部分。 查看您的垃圾收集日志,对其进行分析,并尝试了解它们。 它将帮助您了解您的环境以及收集垃圾时JVM内部的情况。 除此之外,请尝试很多! 在测试环境中,开发人员机器上进行实验,在某些生产或预生产实例中进行实验,并观察行为上的差异。
希望本文将对您在基于JVM的应用程序中进行健康垃圾收集的过程有所帮助。 祝好运!
翻译自: https://dev.to/sematext/a-step-by-step-guide-to-java-garbage-collection-tuning-2m1g
java分步式