Java虚拟机 CMS GC 调优解析

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 随着 JDK 版本的不断升级,其 GC 策略也随之不停革新,从早期的 1.4 到如今的 11(本文仅讨论在线上环境落地规模较大的版本),其对应的 GC 策略也随之由 Serial、Parallel、CMS 演进至当前的 G1 甚至即将落地的 ZGC 。每一次的调整无不是基于环境的适配性以及业务场景特性,无论如何,只要能够基于特定的操作系统内核、物理内存、JDK版本以及业务特性,达到收益最大化,采用何种实现策略都不为过。当然,还是建议大家以官方的推荐为准,基于自己的业务场景进行不断优化调整,这样才能保证万无一失,使得我们的业务能够健康发展。

    随着 JDK 版本的不断升级,其 GC 策略也随之不停革新,从早期的 1.4 到如今的 11(本文仅讨论在线上环境落地规模较大的版本),其对应的 GC 策略也随之由 Serial、Parallel、CMS 演进至当前的 G1 甚至即将落地的 ZGC 。每一次的调整无不是基于环境的适配性以及业务场景特性,无论如何,只要能够基于特定的操作系统内核、物理内存、JDK版本以及业务特性,达到收益最大化,采用何种实现策略都不为过。当然,还是建议大家以官方的推荐为准,基于自己的业务场景进行不断优化调整,这样才能保证万无一失,使得我们的业务能够健康发展。

    本文主要以 CMS GC 为核心,简要解析有关在基于CMS GC 策略下进行 Java 虚拟机调优的最佳实践建议。涉及常见关键症状、堆空间以及 GC Log。此最佳实践建议适用于在 Java® 8上运行的AM / OpenAM、DS / OpenDJ、IDM / OpenIDM和IG /OpenIG。其实,从本质上讲,Java 虚拟机调整并不是一门精确的科学理论,因为它会因各个环境差异性和应用程序多样性而表现的有所不同。基于对当前环境进行一致的压力及稳定性测试才是评估所优化调整影响业务场景的唯一真实方法及指导。

    那么,在现实的业务场景中,我们是如何决策优化呢?在开始进行 Java 虚拟机调整之前,笔者建议大家需要关注以下几点:

1、 内存泄漏

    Memory leaks 内存泄漏问题,几乎伴随着整个 Java 虚拟机生态,基于不同的应用场景,其表现形式为多样化。因此,如果无论我们基于何种方式的调整,其效果往往不是那么理想,这些可疑点可能总会导致垃圾回收(GC)问题。此点为 Java 虚拟机性能调整过程中所必须面对、解决的。 例如,应用中的一些对象不再被应用程序使用同时垃圾收集无法识别的情况。因此,这些未使用的对象仍然在 Java 堆空间中无限期地存在。不停的堆积最终会触发 java.lang.OutOfMemoryError 错误。

2、资源成本

   关于成本问题,视公司的实际情况而定,仁者见仁,智者见智。技术实力落后的团队,更倾向于增加更多的硬件设备资源来提高性能,毕竟,现在的资源相比之前成本低多了。然而,技术实力足够牛逼的团队,更愿意从本质上,即技术层面非资源层面,去解决问题。毕竟,后者更能够对整个业务系统的瓶颈、缺陷有所了解、把控,更能在后续的容量规划中提供有价值的参考数据作为支撑,虽然后者可能耗费较多时间成本,但基于后续的业务规划而言,其相比前者更具有战略价值。这也是笔者所推崇的。

3、近期规划

   我们都知道,性能调优是一个持久的过程,针对我们自己的业务场景所规划的函数模型,例如:P 的表现值由(x、y、z、...)*r 等多个不同性能因子决定,那么,可能在某一有限的项目活动周期内,我们可能无法顾及所有的性能因子,因此,就需要进行优先级决策,从影响的大小或者范围进行降级排序,然后对选择的性能因子进行验证,调整,完善,当然,也有可能其中的某些因子本身就互逆,导致某些场景失效。所以,如果需要解决根本问题,则调优只能以阶段性规划为导向局部因素调整来满足当前项目周期内的业务痛点。

4、最终期望

    基于上述3点所述,在我们解决当前环境中的核心痛点后,需要从业务角度去优化后续带来的挑战,例如,新业务的落地、流量的剧增以及其他场景对当前系统架构产生影响的各个方面。因此,从长远规划来看,架构的稳定性比调整单纯的性能更为重要,尽管存在重叠。

    本文主要基于笔者当前公司的应用环境,主要采用 Java 8。在 Java 8中,CMS 是默认的垃圾收集器,其核心目标为倾向于以最少的垃圾收集暂停时间提供最佳的性能特性。因此,只有当我们的应用服务在进行性能对比,在基于 CMS 策略下,其效能不能够支撑我们的业务需求场景下,才考虑尝试使用 G1 或者其他的垃圾收集器。下面,我们针对 Java 虚拟机中核心的组件进行简要解析,以帮助大家能够更深入了解其运行原理及管理机制。在进行解析之前,我们先来了解下 Java 虚拟机体现架构,具体如下示意图所示:

    从上面架构图可以看出,各个组件在整个 Java 虚拟机体系中的位置以及发挥的作用。关于 Java 虚拟机体系架构的详细解析,暂不在本章中所述,大家有兴趣可以去官网查阅。

Heap

    什么是堆?它能干嘛?何时需要对它进行调整?Java 堆是分配给在 JVM中运行应用程序的内存资源池。堆内存中的对象可以在线程之间共享。垃圾回收(GC)是指管理运行时内存的自动调节过程。JVM 会随着 GC Pause的影响,其频率、持续时间各不相同。如果这些暂停变得更加频繁或持续更长的时间,表明我们的应用程序可能需要调整 Java堆。

    在实际的业务场景中,通常我们会指定初始堆 -Xms 与最大堆 -Xmx,并对其分配内存的大小尽可能一致,以获得最优的性能表现。否则,JVM 在增加其堆时会运行完整的垃圾收集器(GC)周期,在此期间,JVM 可以将正在进行的操作暂停几秒钟。通常,较小的堆会增加 JVM GC 的执行频次,但会减少持续时间。同样,较大的堆将减少频率并增加持续时间。调整 JVM 堆时,我们的初衷目标在于在频率和持续时间之间取得平衡,以减少 GC 对应用程序的影响。

    下面,我们来看一下整个 JVM 内存的资源分配示意图,具体如下所示:

    基于上述参考图,堆最初是在 JVM 启动时创建的,并将其划分为不同的“空间”或“代”,其中,核心的是 Young(新生)和 Tenured(老年):

    Young 代用于新生对象。GC 进程(ParNew)变满时会自动运行,该进程将删除未使用的对象并将所有生存时间足够长的对象移至 Tenured 代,从而释放 Young 代中用于更多新对象的空间。通常,年轻代中的对象是短暂且短暂的。虽然很小,但活跃,GC 经常发生,但对性能的影响有限。

    Tenured 代用于寿命更长的对象,也就是我们所说的老年对象。另一个 GC 进程(CMS)变满时将运行,以删除所有未使用的对象。与 Young 代相比,Tenured 代更大且不那么活跃,但是 GC 往往会对性能产生更大的影响。

    那么,什么时候需要对 Heap 下手呢?通常,当业务场景出现如下表现时,可考虑进行调整,具体:

     1、CPU使用率过高

    当我们的程序处理能力无法满足实际的业务需求,通过后台 GC Log 查看,出现大量的 Full GC,频繁的 Full GC 操作需要耗费大量的 CPU 时间片,所以,我们会看到 CPU 出现激增情况。

     2、应用服务挂起

    当我们的应用程序假死并停止响应时,前端显示 500 或者业务中断,同时,日志文件中也将不会输出相关有价值的信息,或者我们在执行某一定时任务时遇到一般响应缓慢的情况,则可能是内存分配异常导致。

     3、大量的转储文件生成

    针对内存层面,当 JVM 中对象过多, Java堆耗尽时,就会产生 Java Heap Dump文件,通常以 .heapdump 或 .hprof 格式展现。此文件较大,最小大于1G,最大不限制,通常大约在2~10 G之间,不断地生成此类文件,很容易使得磁盘打爆,服务挂掉。针对 CPU 层面,则往往会生成较多的 javacore.txt 文件,此文件伙同堆转储文件。

GC Loging

  接下来,我们来了解下 GC Log 相关方面细节。通常,在基于 Java 8 ,建议大家优先采用 CMS(Concurrent Mark Sweep),它是一个吞吐量收集器,在与应用程序同时运行时往往会提供最佳性能,这意味着较少的暂停时间。实际的场景中,通过设置以下 JVM 选项来指定此收集器,具体如下所示:


-XX:+UseConcMarkSweepGC

     ParNew 部分

     ParNew 部分的 GC 日志输出示例,具体如下所示:


21.222: [GC21.222: [ParNew: 535347K->39439K(623550K), 0.0394310 secs] 584661K->87777K(3102652K), 0.0401790 secs] [Times: user=0.17 sys=0.00, real=0.04 secs] 
26.525: [GC26.525: [ParNew: 583983K->27488K(623550K), 0.0868170 secs] 614171K->104810K(3102652K), 0.0875620 secs] [Times: user=0.24 sys=0.01, real=0.09 secs]

    该日志文件中的每一行代表一个 GC,并显示有关 ParNew 的以下类型的信息:

    1、时间戳信息-如果未设置 PrintGCTimeStamps 和 PrintGCDateStamps 选项,则仅以 JVM 启动后的秒数显示:


21.222: [GC21.222:
26.525: [GC26.525:

    此信息很关键,因为它显示了 GC 的频率。 假设我们定义的目标是使 GC 每1到5秒发生一次;如果它们每秒发生一次以上,则需要调整 JVM,并且很有可能会看到很高的CPU使用率。

     2、年轻代信息


[ParNew: 535347K->39439K(623550K), 0.0394310 secs]
[ParNew: 583983K->27488K(623550K), 0.0868170 secs]

    此信息显示执行 GC 之前的 Young 空间的初始大小,执行 GC 之后的大小,可用的总大小以及执行 GC 所需的时间。如果执行 GC 所需的时间过长(超过0.1秒),则可能是您的 Young Generation堆大小太大。如果将 NewSize 和 MaxNewSize 设置为相同的值,则可用的总大小不会增加,这就是我们希望在日志中看到的大小。

     3、总分配堆信息


584661K->87777K(3102652K), 0.0426850 secs]
614171K->104810K(3102652K), 0.0892180 secs]

    此信息显示执行 GC 之前的总体堆的初始大小,执行 GC 之后的大小以及可用的总大小。使用第二批数据(完成 GC 后的大小),我们可以看到它如何逐渐增长到完成 GC 时的水平,然后又减小到基线数据。如果发生内存泄漏,即使执行完 GC 后,基线数字也会逐渐增加,因为它越来越满。

     4、系统耗时信息


[Times: user=0.13 sys=0.00, real=0.02 secs] 
[Times: user=0.27 sys=0.02, real=0.11 secs]

    该信息显示了在用户空间上花费了多少时间,在内核或系统空间上花费了多少时间以及对用户的真正影响是什么。如果用户时间过长,则表明没有足够的CPU来容纳用户线程数。如果系统时间过长,则表明应用系统正在将内存交换到磁盘,这意味着没有足够的物理内存来满足您的堆大小。

    CMS 部分

    日志文件的 CMS 部分与 ParNew 部分不同,因为它显示了同一 GC 的多行。CMS部分的示例 GC 日志输出如下所示:


30.136: [GC [1 CMS-initial-mark: 26386K(786432K)] 26404K(1048384K), 0.0074495 secs]
30.144: [CMS-concurrent-mark-start]
30.663: [CMS-concurrent-mark: 0.521/0.529 secs]
340.663: [CMS-concurrent-preclean-start]
30.801: [CMS-concurrent-preclean: 0.017/0.018 secs]
30.804: [GC40.704: [Rescan (parallel) , 0.1790103 secs]40.883: [weak refs processing, 0.0100966 secs] [1 CMS-remark: 26386K(786432K)] 52644K(1048384K), 0.1897792 secs]
30.984: [CMS-concurrent-sweep-start]
31.020: [CMS-concurrent-sweep: 0.126/0.126 secs]
31.020: [CMS-concurrent-reset-start]
31.135: [CMS-concurrent-reset: 0.127/0.127 secs]

   该日志显示了 GC 经历的不同阶段。所有标记为并发的事件都是在应用程序运行时发生的,因此影响很小。CMS 初始标记和 CMS 标记显示堆大小调整详细信息。

   通常,在实际的业务场景中,我们往往会看到这些部分日志打印混杂在一起,例如,某些 ParNews 比 CMS 多一些,ParNews多一些,另一个CMS 等,因为 GC 进程会自动发生以管理堆。

CMS GC 常见场景分析

   此部分内容主要解析在基于 CMS 策略时,出现的各种场景以及针对性的解决方案,以便大家能够掌握此处内容。

    1、年轻代分配过小

    以下日志片段显示了 Young 代堆大小太小的问题,具体如下所示:


1.813: [GC1.813: [ParNew: 1152K­>128K(1152K), 0.0008100 secs] 16620K­->15756K(26936K), 0.0008350 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.816: [GC1.816: [ParNew: 1152K­>128K(1152K), 0.0006430 secs] 16780K­->15913K(26936K), 0.0006640 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.819: [GC1.819: [ParNew: 1152K­>128K(1152K), 0.0005370 secs] 16937K-­>16038K(26936K), 0.0005570 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

    基于上述 GC Log 输出,我们可以看到:在1秒钟内发生了多个 GC。这意味着应用程序需要增加 Young 代的大小(NewSize和MaxNewSize),还可能需要增加总堆大小以补偿此更改(Xms和Xmx)。或者,可以更改 NewRatio 选项(默认情况下设置为2,这意味着 Tenured 代堆的大小是 Young 代堆的两倍,或者 Young 代堆的大小是 Tenured 世代堆大小的1/3。)。

     2、老年代分配过

    以下日志片段显示了一个问题,其中,Young 代堆大小太大,因此 Tenured 代堆大小太小,具体如下所示:


275.616: [GC (CMS Initial Mark) [1 CMS-initial-mark: 104176K(135168K)] 2758985K(6741248K), 0.8762860 secs] [Times: user=0.88 sys=0.00, real=0.88 secs] 
276.583: [CMS-concurrent-mark-start]
276.657: [CMS-concurrent-mark: 0.063/0.065 secs] [Times: user=0.12 sys=0.00, real=0.06 secs] 
276.657: [CMS-concurrent-preclean-start]
276.657: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
276.657: [CMS-concurrent-abortable-preclean-start]
276.657: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
276.657: [GC (CMS Final Remark)[YG occupancy: 2657419 K (6606080 K)]277.658: [Rescan (parallel) , 0.9815460 secs]278.639: [weak refs processing, 0.0000320 secs]278.640: [scrub string table, 0.0011700 secs] [1 CMS-remark: 104176K(135168K)] 2761595K(6741248K), 0.9828250 secs] [Times: user=7.18 sys=0.09, real=0.99 secs] 
277.631: [CMS-concurrent-sweep-start]
277.658: [CMS-concurrent-sweep: 0.026/0.027 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
277.658: [CMS-concurrent-reset-start]
277.658: [CMS-concurrent-reset: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
280.673: [GC (CMS Initial Mark) [1 CMS-initial-mark: 104079K(135168K)] 2774091K(6741248K), 0.9033730 secs] [Times: user=0.90 sys=0.00, real=0.90 secs] 
281.577: [CMS-concurrent-mark-start]
281.640: [CMS-concurrent-mark: 0.063/0.063 secs] [Times: user=0.13 sys=0.00, real=0.07 secs] 
281.640: [CMS-concurrent-preclean-start]
281.641: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
281.641: [CMS-concurrent-abortable-preclean-start]
281.641: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
281.641: [GC (CMS Final Remark)[YG occupancy: 2670011 K (6606080 K)]281.641: [Rescan (parallel) , 0.9914290 secs]282.633: [weak refs processing, 0.0000110 secs]282.633: [scrub string table, 0.0008100 secs] [1 CMS-remark: 104079K(135168K)] 2774091K(6741248K), 0.9923100 secs] [Times: user=7.14 sys=0.11, real=0.99 secs] 
282.634: [CMS-concurrent-sweep-start]
282.659: [CMS-concurrent-sweep: 0.024/0.025 secs] [Times: user=0.06 sys=0.01, real=0.02 secs] 
282.659: [CMS-concurrent-reset-start]
282.659: [CMS-concurrent-reset: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

    如上述 GC Log 所见,CMS 回收的发生频率很高,而两者之间没有任何 ParNew 回收。这是由于 Young 代堆大小太大而导致每次发生 ParNew 回收时,Tenured 堆大小都会立即变满并运行 CMS 回收。如果我们在不增加总堆大小的情况下增加了 Young 代的堆大小,则可能会发生这种情况。如果有内存泄漏,也可能发生此日志摘要信息。

    针对 CMS 场景中的 CMS Failure、Full GC 以及其他相关内容,可参考之前文章,具体链接为:CMS 触发GC(Allocation Failure)解析

    最后,从宏观角度,针对系统调优,笔者给出以下建议,具体:

    1、尽可能对当前的应用系统进行负载/压力测试,以建立性能基线。

    2、在进行调优时,尽可能每次只调整其中一个性能因子,观测及记录每一次的优化结果,必要时进行恢复。

   至此,关于 Java虚拟机 CMS GC 调优解析相关内容本文到此为止,大家有什么疑问、想法及建议,欢迎留言沟通。

相关文章
|
28天前
|
监控 算法 Java
Java虚拟机(JVM)的垃圾回收机制深度解析####
本文深入探讨了Java虚拟机(JVM)的垃圾回收机制,旨在揭示其背后的工作原理与优化策略。我们将从垃圾回收的基本概念入手,逐步剖析标记-清除、复制算法、标记-整理等主流垃圾回收算法的原理与实现细节。通过对比不同算法的优缺点及适用场景,为开发者提供优化Java应用性能与内存管理的实践指南。 ####
|
17天前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
19天前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
24天前
|
监控 Java 编译器
Java虚拟机调优指南####
本文深入探讨了Java虚拟机(JVM)调优的精髓,从内存管理、垃圾回收到性能监控等多个维度出发,为开发者提供了一系列实用的调优策略。通过优化配置与参数调整,旨在帮助读者提升Java应用的运行效率和稳定性,确保其在高并发、大数据量场景下依然能够保持高效运作。 ####
27 1
|
26天前
|
存储 算法 Java
JVM进阶调优系列(10)敢向stop the world喊卡的G1垃圾回收器 | 有必要讲透
本文详细介绍了G1垃圾回收器的背景、核心原理及其回收过程。G1,即Garbage First,旨在通过将堆内存划分为多个Region来实现低延时的垃圾回收,每个Region可以根据其垃圾回收的价值被优先回收。文章还探讨了G1的Young GC、Mixed GC以及Full GC的具体流程,并列出了G1回收器的核心参数配置,帮助读者更好地理解和优化G1的使用。
|
27天前
|
监控 Java 测试技术
Elasticsearch集群JVM调优垃圾回收器的选择
Elasticsearch集群JVM调优垃圾回收器的选择
48 1
|
1月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
1月前
|
监控 Java 编译器
Java虚拟机调优实战指南####
本文深入探讨了Java虚拟机(JVM)的调优策略,旨在帮助开发者和系统管理员通过具体、实用的技巧提升Java应用的性能与稳定性。不同于传统摘要的概括性描述,本文摘要将直接列出五大核心调优要点,为读者提供快速预览: 1. **初始堆内存设置**:合理配置-Xms和-Xmx参数,避免频繁的内存分配与回收。 2. **垃圾收集器选择**:根据应用特性选择合适的GC策略,如G1 GC、ZGC等。 3. **线程优化**:调整线程栈大小及并发线程数,平衡资源利用率与响应速度。 4. **JIT编译器优化**:利用-XX:CompileThreshold等参数优化即时编译性能。 5. **监控与诊断工
|
1月前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
1月前
|
监控 数据挖掘 OLAP
深入解析:AnalyticDB中的高级查询优化与性能调优
【10月更文挑战第22天】 AnalyticDB(ADB)是阿里云推出的一款实时OLAP数据库服务,它能够处理大规模的数据分析任务,提供亚秒级的查询响应时间。对于已经熟悉AnalyticDB基本操作的用户来说,如何通过查询优化和性能调优来提高数据处理效率,是进一步提升系统性能的关键。本文将从个人的角度出发,结合实际经验,深入探讨AnalyticDB中的高级查询优化与性能调优技巧。
104 4

推荐镜像

更多
下一篇
DataWorks