Java虚拟机 G1 GC 调优解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 在上篇文章中,我们解析了 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
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
12天前
|
存储 Java 开发者
浅析JVM方法解析、创建和链接
上一篇文章《你知道Java类是如何被加载的吗?》分析了HotSpot是如何加载Java类的,本文再来分析下Hotspot又是如何解析、创建和链接类方法的。
|
24天前
|
Java 编译器
Java 泛型详细解析
本文将带你详细解析 Java 泛型,了解泛型的原理、常见的使用方法以及泛型的局限性,让你对泛型有更深入的了解。
37 2
Java 泛型详细解析
|
19天前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
21天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
21天前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
24天前
|
Java 数据库连接 开发者
Java中的异常处理机制:深入解析与最佳实践####
本文旨在为Java开发者提供一份关于异常处理机制的全面指南,从基础概念到高级技巧,涵盖try-catch结构、自定义异常、异常链分析以及最佳实践策略。不同于传统的摘要概述,本文将以一个实际项目案例为线索,逐步揭示如何高效地管理运行时错误,提升代码的健壮性和可维护性。通过对比常见误区与优化方案,读者将获得编写更加健壮Java应用程序的实用知识。 --- ####
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
76 2
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
78 0
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
64 0

推荐镜像

更多