Java虚拟机 G1 GC 调优解析

简介: 在上篇文章中,我们解析了 Java 虚拟机体系生态中基于 CMS GC 策略的调优场景及基本案例,具体链接为:Java虚拟机 CMS GC 调优解析。本文则重点介绍另一款当前比较流行的 GC 策略 - G1。

   在上篇文章中,我们解析了 Java 虚拟机体系生态中基于 CMS GC 策略的调优场景及基本案例,具体链接为:Java虚拟机 CMS GC 调优解析。本文则重点介绍另一款当前比较流行的 GC 策略 - G1。

   依据官方 Java 虚拟机的规划,自 Java 9 开始,在实际的生产环境中不再建议使用基于 ConcurrentMarkSweep(CMS)垃圾收集器。根据 JEP-291,已做出此决定以减轻GC 代码库的维护负担并加速新开发。毕竟,Java 9 之后,G1 GC 已成为默认的 GC 算法。(当然,基于不同的环境,Z 垃圾收集器-ZGC 、Shenandoah GC 亦逐渐开始成为主流算法)因此,我们可以根据实际业务场景考虑将我们的应用程序移至该算法。它可能提供比 CMS GC 算法更优的性能特征。由于其参数相对较少,因此调整起来要容易得多。此外,G1 同时也提供了一些选项以从内存中消除重复的字符串,从而可以帮助我们应用减少总体内存占用。

   其实,在某些场景下,基于 CMS GC 的性能有的时候往往会比 G1 要高、更优,因此,只有基于实际的性能验证,在官方所约束的环境下,才能决定采用哪种符合业务场景的 GC 策略。由于在更高版本的 Java 版本中已弃用 CMS,故此我们才迫不得已需要将其移植至另一种类型的 GC 策略。

   那么,如果我们在生产环境中基于 Java 9 后续的版本(以 11 为例),显性定义 CMS GC 策略,即关键字“ -XX:+ UseConcMarkSweepGC ”,将会出现何种异常呢?我们先来看个简要的场景,具体如下:


[administrator@JavaLangOutOfMemory cpu ]% java -version
java version "11.0.10" 2021-01-19 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.10+8-LTS-162)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.10+8-LTS-162, mixed mode)

   基于上述参数,我们定义了 Java 11 版本环境,接下来,通过运行一个简单的 Java 应用来对比验证 CMS GC 在 Java 9 后续版本环境中的表现情况,具体如下所示:


[administrator@JavaLangOutOfMemory cpu ]% cat startup-cms.sh
#!/bin/bash
java -Xms512M -Xmx512M -XX:+UseConcMarkSweepGC -verbose:gc -XX:+PrintCommandLineFlags -XX:+ExplicitGCInvokesConcurrent -Xlog:gc*=debug:file=/Users/administrator/cpu/admin_gc_log/admin_gc.log:utctime,level,tags:filecount=50,filesize=100M -Duser.timezone=GMT+11 -jar /Users/administrator/cpu/devopsDemo.jar PROBLEM_IO
[administrator@JavaLangOutOfMemory cpu ]% ./startup-cms.sh 
Java HotSpot(TM) 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release.
-XX:+ExplicitGCInvokesConcurrent -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=536870912 -XX:MaxNewSize=178958336 -XX:MaxTenuringThreshold=6 -XX:NewSize=178958336 -XX:OldSize=357912576 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC 
[0.043s][info][gc] Using Concurrent Mark Sweep
Application started!
Starting to write to fileIO-1.txt
Starting to write to fileIO-2.txt
Starting to write to fileIO-3.txt
Starting to write to fileIO-4.txt
Starting to write to fileIO-5.txt
[1.295s][info][gc] GC(0) Pause Young (Allocation Failure) 136M->0M(494M) 7.478ms
Read & write 1000 times to fileIO-5.txt
Read & write 1000 times to fileIO-1.txt
Read & write 1000 times to fileIO-3.txt
Read & write 1000 times to fileIO-4.txt
Read & write 1000 times to fileIO-2.txt
[2.321s][info][gc] GC(1) Pause Young (Allocation Failure) 137M->0M(494M) 3.365ms
Read & write 1000 times to fileIO-4.txt
Read & write 1000 times to fileIO-1.txt
Read & write 1000 times to fileIO-5.txt
Read & write 1000 times to fileIO-3.txt
Read & write 1000 times to fileIO-2.txt
[3.089s][info][gc] GC(2) Pause Young (Allocation Failure) 137M->1M(494M) 3.114ms
Read & write 1000 times to fileIO-5.txt
Read & write 1000 times to fileIO-1.txt
Read & write 1000 times to fileIO-4.txt
^C%

   基于上述的执行情况,我们可以看到,如果使用 -XX:+UseConcMarkSweepGC 策略启动应用程序 ,将激活 CMS GC 事件机制,应用程序也能够运行起来,此时将看到以下警告消息:“ Java HotSpot(TM) 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release ” 。关于 CMS GC 的相关内容,可参考之前文章:CMS GC已成过去式。接下来,我们进入本文的主题 Garbage First GC 。

Heap

  其实,针对 G1 GC 优化,在之前的文章中有所涉及,大家有兴趣可以查阅,链接为:G1 GC简单优化技巧。本文将会从基础的架构原理角度,基于具体的场景进行阐述,以便大家能够更加深入去理解整个 G1 GC 技术体系。从本质上讲,无论是基于早期的Serial、Parallel、CMS、G1 ,还是即将或已落地的 ZGC、Shenandoah GC,它们之间在某种特定的意义角度还是有相互关联的。

   基于对象的生命周期发展,G1 与串行,并行和 CMS GC 没什么不同,都是基于分代垃圾收集器概念,通俗意义上来讲,在大多数对象都死于年轻的前提下,将堆分成若干代。在年轻代中处理(清理)对象比转移到老年代并在那里清理要更加简洁、有效。

   基于堆的规划层面,G1 与传统的分代垃圾收集器还是有所差异,在 G1 之前,堆被定义为连续的区域,具体如下图所示:

    基于上述参考示意图,我们可以看到堆空间已被定义为不同的代(Generation),即年轻代(由 Eden Space 和 Survivor 等区域组成)和老年代,同时,此空间也是连续的。

   相对比传统的而言,G1 GC 在堆空间分配上有很大的不同。G1 将堆划分为多个(通常为2048个)较小的 Region(堆区域,Region 的大小可以通过 G1 HeapRegionSize 参数进行设置,其必须是2的幂,范围允许为 1Mb 到 32 Mb。JVM 会基于堆内存的初始值和最大值的平均数计算分区的尺寸,平均的堆尺寸会分出约 2000 个 Region。分区大小一旦设置,则启动之后不会再变化。因此,堆大小直接改变了区域大小。每个区域都可以分配为伊甸园,幸存者或旧区域。分配给 Eden,Survivor 或 Old 的区域数量是灵活的,由 GC 在运行时确定。具体如下图所示:

   注:Humongous regions(巨型对象区域)、Free resgions(未分配区域,也会叫做可用分区)暂未在上述图中进行标注。

GC 阶段

   通常,在基于 G1 GC 策略场景中,主要存在3个核心的垃圾收集策略。(严格声明:JDK 10 之前的 G1中的 GC 只有 Young GC 和 Mixed GC。FullGC处理会交给单线程的 Serial Old 垃圾收集器。)


   1、Minior GC - 年轻代垃圾收集

   也称为“ Young GC ”,一些年轻的 GC 在 Eden Survivor 之间进行移动,并最终将其移到 Old 空间。即在我们 New 一个对象(非巨型对象)时,并对其进行空间分配,当所有 Eden Region 使用达到最大阀值并且无法申请足够内存时,会触发一次 Young GC。每次 Young GC 会回收所有 Eden 以及 Survivor 区,并且将存活对象复制到 Old 区以及另一部分的 Survivor 区。到 Old 区的标准就是在 PLAB 中得到的计算结果。因为Young GC 会进行根扫描,所以会进行 Stop the World。


   2、Mixed GC - 混合垃圾收集

  此阶段涉及一个或多个混合收集,即新一代以及垃圾量最大的许多旧区域(可配置)。在混合收集结束时,G1 确定是否需要另一个混合收集以达到其阈值(可配置)。此后,该循环再次从另一个年轻的 GC 阶段开始。Mixed GC 是 G1 GC 特有的,跟 Full GC 不同的是 Mixed GC 只回收部分老年代的 Region。通常,基于全局角度,Mixed GC 一般会发生在一次 Young GC 后面,为了提高效率,Mixed GC 会复用Young GC 的全局的根扫描结果,毕竟 Stop the World 过程是不可或缺的,从整体层面上来说其缩短了暂停时间。

   3、Full GC - 全局垃圾收集

   与其他 GC 一样,这是最终的选择。如果应用程序在收集活动信息时内存不足,则可能导致 Stop-the-World 的 Full GC,即年轻代和老年代。具体来讲,G1 在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发Full GC。Full GC 使用的是 Stop the World 的单线程的 Serial Old 模式,所以一旦触发Full GC 则会 STW 应用线程,并且执行效率很慢。其实,从本质上讲,JDK 8 版本的 G1 是不提供 Full GC 事件的处理。G1 GC 和其他分代垃圾收集器的主要目标之一:避免使用昂贵的 Full GC。

GC 调优

    与传统的 CMS 相比,G1 GC 可用的 JVM 参数选项明显要少得多( 官方所定义:CMS 72、G1 26、ZGC 8),主要目的是为了减少使用。通常,基于 G1 GC 的 JVM 的基本策略主要取决于2点:堆大小和暂停时间,然后让 JVM 动态修改所需的设置以尝试满足暂停时间目标。如果未达到性能目标,需要考虑基于 GC 监视和日志分析的其他选项。这是一个反复的过程,重要的是要确保有足够的时间和资源分配给此关键任务。

   1、Heap 大小

   在 JDK 11 之前,若我们在生产环境中使用基于 G1 GC 策略,需要显性定义参数: -XX:+ UseG1GC ,通过设置此关键字,才能使得策略场景生效。毕竟,在 Java 8上,默认 GC 是 CMS GC,而在 Java 11 上,默认 GC 才是 G1 GC。这也意味着,在升级 Java 版本时,除非明确设置了 GC 策略类型,否则其仍然依据厂商所定义的策略执行。除此之外,针对堆的内存分配,建议将 -Xms 和 -Xmx 大小显式设置为相同的值,以避免在应用程序生命周期中堆的动态收缩和增长。具体可以使用以下 JVM 参数选项执行此操作:



-XX:InitialHeapSize(最小Java堆大小)---等同于 -Xms
-XX:MaxHeapSize(最大Java堆大小)---等同于 -Xmx

   2、GC Pause 目标

   通常,在基于 G1 GC 策略场景中,建议设置暂停时间目标,并让 GC 能够根据实际需要更新堆。除此之外,谨记:除非需要,否则不要设置年轻代(-XX:NewSize -XX:MaxNewSize 或 -Xmn)的大小。否则,当我们显性手工设置了其大小,就意味着放弃了 G1 的自动调优。要设置暂停时间目标,我们需要设置以下 JVM 参数选项(默认值为200),具体如下所示:


-XX:MaxGCPauseMillis=500

   基于上述参数,然后对其进行性能验证,以检测当前设置是否满足预期的性能目标。毕竟,对这个参数的调优是一个持续的过程,逐步调整到最佳状态。暂停时间只是一个目标,在实际的业务场景中并不能总是得到满足。  

   3、GC Logging

  针对 GC Log 的使用,与早期的 CMS 并无差异,只不过,从 Java 9 开始,统一的JVM 日志记录已经取代了旧的日志记录选项。Java 8 的日志记录选项不能用于 Java11。具体如下:


-XX:+UseG1GC -XX:InitialHeapSize=2048M -XX:MaxHeapSize=2048M -XX:MaxGCPauseMillis=500 -XX:+DisableExplicitGC -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxMetaspaceSize=256m -XX:MaxTenuringThreshold=1 -Xlog:gc=debug:file=/data/logs/admin-web-gc.log:time,uptime,level,tags:filecount=5,filesize=100m

   4、其他辅助性参数

    针对辅助性优化,主要针对 Mixed GC 的调整,具体涉及以下参数:


-XX:InitiatingHeapOccupancyPercent -XX:G1MixedGCLiveThresholdPercent -XX:G1MixedGCCountTarger

   针对其他方面的优化,可参考之前的文章,具体链接为:G1 GC简单优化技巧


   综上所述,本文所解析的内容主要基于 JDK 8 版本环境,在 Jdk10 及以上版本的 G1 GC 会有更多的优化。Full CG 方面,将提供并发标记的 Full GC 方案:Parallelize Mark-Sweep-Compact。Card Table 的扫描也会得到加速。同时,也对 RSet 进行更新优化,目前的 RSet 会存储在所有的分区里,新版本的 RSet 只需要在 CSet 中,并且是在 Remark 到 Clean 阶段之间并发构建 RSet。这项优化会增加整个并发标记的周期,但是缩减了很多 RSet 的占用空间。另外,对于 PauseTime 会有更精准的处理,在 Mixed GC 的对象拷贝阶段,提供了可放弃拷贝的(Abortable)选项。Mixed GC 会计算下一个 Region 的对象拷贝,如果可能会超过预期的 Pause Time,则会放弃这次拷贝。

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

相关文章
|
1天前
|
Java 开发者 UED
Java中的异常处理机制深度解析
在Java编程中,异常处理是确保软件健壮性的关键因素。本文将深入探讨Java的异常处理机制,包括异常的类型、如何捕获和处理异常,以及最佳实践。我们将通过实例学习如何优雅地处理异常,避免常见的陷阱,并提升代码的可维护性和用户体验。
|
1天前
|
搜索推荐 Java 程序员
【案例解析】从菜鸟到高手,如何优雅地使用Java条件语句?
【6月更文挑战第14天】本文介绍了Java编程中条件语句的重要性,特别是if-else和switch的使用。通过四个案例,展示了如何优雅地运用这些语句:案例一演示了基础if-else用于用户登录响应;案例二利用switch处理枚举类型,如订单状态;案例三展示了条件语句的嵌套与组合,用于游戏评分系统;案例四探讨了代码优化与重构,减少冗长的if-else结构。文章强调,掌握条件语句能提升编码效率和代码质量,鼓励开发者不断实践以写出高效优雅的代码。
|
1天前
|
Java 开发者
别再傻傻分不清!Java if-else与switch的性能对比全解析!
【6月更文挑战第14天】本文探讨了Java中if-else与switch语句的性能异同。虽然现代JVM的优化使得两者性能差异不大,但特定情况下仍有区别。switch通过跳转表提供高效执行,尤其适用于枚举和固定值,而if-else依赖条件顺序,JVM可能优化常量条件。实验显示,处理大量重复case时,switch性能更优。选择时还需考虑可读性和维护性,灵活运用以实现高效优雅的代码。
|
2天前
|
安全 Java API
深入解析 Java 8 新特性:LocalDate 的强大功能与实用技巧
深入解析 Java 8 新特性:LocalDate 的强大功能与实用技巧
8 1
|
2天前
|
Java API 调度
深入解析Java线程状态与生命周期
深入解析Java线程状态与生命周期
9 1
|
2天前
|
存储 安全 Java
深入理解Java中的ThreadLocal机制:原理、方法与使用场景解析
深入理解Java中的ThreadLocal机制:原理、方法与使用场景解析
10 2
|
2天前
|
存储 前端开发 Java
深入解析Java类加载机制:原理、过程与实践
深入解析Java类加载机制:原理、过程与实践
8 2
|
2天前
|
Java API
深入探讨 Java 8 集合操作:全面解析 Stream API 的强大功能
深入探讨 Java 8 集合操作:全面解析 Stream API 的强大功能
9 2
|
2天前
|
存储 安全 Java
深入解析 Java 中的 Synchronized:原理、实现与性能优化
深入解析 Java 中的 Synchronized:原理、实现与性能优化
8 1
|
2天前
|
存储 安全 Java
深入解析Java HashMap的高性能扩容机制与树化优化
深入解析Java HashMap的高性能扩容机制与树化优化
6 1

推荐镜像

更多