Java虚拟机 G1 GC 调优解析

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 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 调优解析相关内容本文到此为止,大家有什么疑问、想法及建议,欢迎留言沟通。

相关文章
|
8月前
|
Java 开发者
重学Java基础篇—Java类加载顺序深度解析
本文全面解析Java类的生命周期与加载顺序,涵盖从加载到卸载的七个阶段,并深入探讨初始化阶段的执行规则。通过单类、继承体系的实例分析,明确静态与实例初始化的顺序。同时,列举六种触发初始化的场景及特殊场景处理(如接口初始化)。提供类加载完整流程图与记忆口诀,助于理解复杂初始化逻辑。此外,针对空指针异常等问题提出排查方案,并给出最佳实践建议,帮助开发者优化程序设计、定位BUG及理解框架机制。最后扩展讲解类加载器层次与双亲委派机制,为深入研究奠定基础。
325 0
|
11月前
|
人工智能 自然语言处理 Java
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
FastExcel 是一款基于 Java 的高性能 Excel 处理工具,专注于优化大规模数据处理,提供简洁易用的 API 和流式操作能力,支持从 EasyExcel 无缝迁移。
2503 65
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
|
8月前
|
存储 设计模式 Java
重学Java基础篇—ThreadLocal深度解析与最佳实践
ThreadLocal 是一种实现线程隔离的机制,为每个线程创建独立变量副本,适用于数据库连接管理、用户会话信息存储等场景。
288 5
|
8月前
|
存储 监控 安全
重学Java基础篇—类的生命周期深度解析
本文全面解析了Java类的生命周期,涵盖加载、验证、准备、解析、初始化、使用及卸载七个关键阶段。通过分阶段执行机制详解(如加载阶段的触发条件与技术实现),结合方法调用机制、内存回收保护等使用阶段特性,以及卸载条件和特殊场景处理,帮助开发者深入理解JVM运作原理。同时,文章探讨了性能优化建议、典型异常处理及新一代JVM特性(如元空间与模块化系统)。总结中强调安全优先、延迟加载与动态扩展的设计思想,并提供开发建议与进阶方向,助力解决性能调优、内存泄漏排查及框架设计等问题。
375 5
|
8月前
|
机器学习/深度学习 人工智能 Java
Java机器学习实战:基于DJL框架的手写数字识别全解析
在人工智能蓬勃发展的今天,Python凭借丰富的生态库(如TensorFlow、PyTorch)成为AI开发的首选语言。但Java作为企业级应用的基石,其在生产环境部署、性能优化和工程化方面的优势不容忽视。DJL(Deep Java Library)的出现完美填补了Java在深度学习领域的空白,它提供了一套统一的API,允许开发者无缝对接主流深度学习框架,将AI模型高效部署到Java生态中。本文将通过手写数字识别的完整流程,深入解析DJL框架的核心机制与应用实践。
500 3
|
8月前
|
安全 IDE Java
重学Java基础篇—Java Object类常用方法深度解析
Java中,Object类作为所有类的超类,提供了多个核心方法以支持对象的基本行为。其中,`toString()`用于对象的字符串表示,重写时应包含关键信息;`equals()`与`hashCode()`需成对重写,确保对象等价判断的一致性;`getClass()`用于运行时类型识别;`clone()`实现对象复制,需区分浅拷贝与深拷贝;`wait()/notify()`支持线程协作。此外,`finalize()`已过时,建议使用更安全的资源管理方式。合理运用这些方法,并遵循最佳实践,可提升代码质量与健壮性。
255 1
|
8月前
|
传感器 监控 Java
Java代码结构解析:类、方法、主函数(1分钟解剖室)
### Java代码结构简介 掌握Java代码结构如同拥有程序世界的建筑蓝图,类、方法和主函数构成“黄金三角”。类是独立的容器,承载成员变量和方法;方法实现特定功能,参数控制输入环境;主函数是程序入口。常见错误包括类名与文件名不匹配、忘记static修饰符和花括号未闭合。通过实战案例学习电商系统、游戏角色控制和物联网设备监控,理解类的作用、方法类型和主函数任务,避免典型错误,逐步提升编程能力。 **脑图速记法**:类如太空站,方法即舱段;main是发射台,static不能换;文件名对仗,括号要成双;参数是坐标,void不返航。
340 5
|
9月前
|
Java API 数据处理
深潜数据海洋:Java文件读写全面解析与实战指南
通过本文的详细解析与实战示例,您可以系统地掌握Java中各种文件读写操作,从基本的读写到高效的NIO操作,再到文件复制、移动和删除。希望这些内容能够帮助您在实际项目中处理文件数据,提高开发效率和代码质量。
242 4
|
9月前
|
XML JSON Java
Java中Log级别和解析
日志级别定义了日志信息的重要程度,从低到高依次为:TRACE(详细调试)、DEBUG(开发调试)、INFO(一般信息)、WARN(潜在问题)、ERROR(错误信息)和FATAL(严重错误)。开发人员可根据需要设置不同的日志级别,以控制日志输出量,避免影响性能或干扰问题排查。日志框架如Log4j 2由Logger、Appender和Layout组成,通过配置文件指定日志级别、输出目标和格式。
|
10月前
|
存储 Java 计算机视觉
Java二维数组的使用技巧与实例解析
本文详细介绍了Java中二维数组的使用方法
370 15

推荐镜像

更多
  • DNS