JVM进阶调优系列(10)敢向stop the world喊卡的G1垃圾回收器 | 有必要讲透

简介: 本文详细介绍了G1垃圾回收器的背景、核心原理及其回收过程。G1,即Garbage First,旨在通过将堆内存划分为多个Region来实现低延时的垃圾回收,每个Region可以根据其垃圾回收的价值被优先回收。文章还探讨了G1的Young GC、Mixed GC以及Full GC的具体流程,并列出了G1回收器的核心参数配置,帮助读者更好地理解和优化G1的使用。

一、前言背景

二、什么是G1回收器?

三、为什么需要G1回收器?

四、G1的核心原理

4.1 Region区的出现,让G1的目标成为可能

4.2 G1为何叫做 Garbage First?

五、G1垃圾回收过程详解

5.1 Young GC

5.2 mixed GC

5.3 G1 真的有Full GC吗?

5.4 G1的GC过程

5.4.1 初始标记

5.4.2 并发标记

5.4.3 最终标记

5.4.4 清理

六、G1回收器核心参数


读书笔记:在发现自己情绪不对时候,需要积极按下暂停键,让原始大脑冷静下来,等自我感觉好了,再专注于解决问题的方案。不能被逻辑后果带偏。

一、前言背景

     日常我们的服务应用进行分布式部署,单节点不需要扛很高的并发,一个服务给到2~4G就足够用,如果不是计算密集型应用,一个节点给到8G就能支撑很高的并发。而为大内存而生的G1,如果普通应用采用G1做GC回收,无疑有一种杀鸡焉用牛刀的感觉。但随着研发经验的不断积累,业务量提升,不断涉足富有挑战的性能问题,以及面试造航母的需要,了解、掌握、实践G1垃圾回收器将是一条必经之路。

    目前主流开源框架Hbase、Elastic search,都是通过JAVA开发实现。随着版本迭代,以及用户数据存储增长,每个节点需要8G,16G,甚至更多时候,如果使用CMS垃圾回收器难以满足应用需要,这些框架至少要升级切换到G1垃圾回收器。


二、什么是G1回收器?

     G1,全称是 Garbage First,顾名思义【垃圾优先】。G1垃圾回收器以GC低延时处理为核心目标。在2011年,JDK7开始支持-XX:+UseG1GC使用G1垃圾回收器进行GC回收。并在2012年,G1 开始面世商用。记得在2013、2014年大数据计算开始火遍千家万户时候,移动互联网浪潮的海量数据、高并发场景应用,让应用对内存、CPU的依赖开始大幅上升。时间来到2017年,JDK 9的腾空出世,直接把G1设置为默认垃圾处理器,这种面向大内存多处理器的垃圾回收器,让后端服务器应用有了更优的选择。


三、为什么需要G1回收器?

     G1的目标很简单。官方对G1的期待是让GC时间可控,以及尽可能的降低GC时间开销,最终替代CMS垃圾处理器。这是典型做最有挑战的事,实现最清晰明了的目标。

     在G1出现之前,我们JVM参数里通常从多个分代收集器里进行组合配置。比如年轻代有Serial、ParNew、Parallel Scavenge回收器可选,老年代有CMS、Serial Old、Parallel Old可选。但是这些垃圾回收器都有一个痛点,就是GC stop the world会导致系统卡顿,而且卡顿时间是不可预知不可控。尤其是JVM 堆内存达到8G、16G、甚至32G的时候,这时候发生GC,需要回收的对象很多,自然而然GC的时间会被拉长,可能从以往我们熟悉的FGC也就几百毫秒,上升到几s,甚至10+s。

     很明显,在面对大内存的场景,传统的分代GC垃圾回收器已经无法满足我们的需求。在这样的应用场景下,GC 的stop the world,会让我们的服务出现明显的卡顿,对核心业务来说,这是不可接受的。

     而G1,支持指定最大GC停顿时间-XX:MaxGCPauseMillis=200ms(默认)的特点,直接让之前分代垃圾处理器望尘莫及。G1的出现,开启了JVM分区回收的新时代。此后JAVA 在2019年,JDK12引入了收集器ShenandoahGC;同年发布JDK13,引入了增强ZGC收集器,都是分代回收器。


四、G1的核心原理

      G1的核心思想就是内存分区回收。体现在堆内存不再仅仅是年轻代和老年代的简单划分,而增加了Region 的概念。将堆内存切分成一块块大小独立而且大小固定的Region区【这个设计,对实现GC时间可控目标,提供了从0-1的可能】,多个Region 可以动态的组合成年轻代和老年代内存空间。

      对G1垃圾回收器堆内存情况的第一印象,满眼都是Region块(实际G1的实现确实是这样,强化了分区的思想,只是底层GC还是基于年轻代、老年代的对象年龄逻辑进行),如图:


       而实际上,堆内存结构如如下,虽然内存被分为一块块大小固定的Region区,但是里面多个Region组成年轻代、老年代,还有一个新增的Humongous区。这个H区是专门用来存放大对象的区,相当于之前老年代的存放大对象,具体如下:

4.1 Region区的出现,让G1的目标成为可能

      对堆内存结构有了基本认识之后,我们尝试理解更深入的架构原理。首先Region为什么要把堆内存分拆成一个个块?这样做的优点是什么?「拉丁解牛说技术,实用至上,坚持用最简洁直白的文字+最少的代码示例分享干货。」

      由于堆内存不再强制按连续的内存空间划分年轻代、老年代。老年代和年轻代只是逻辑上的概念空间,实际内存空间是靠一个个Region块组成。刚才的图看起来Region是连续的,实际并不是。事实上JVM每次需要新的Region块,就在JVM里随机找一块内存空间分配给新的Region区。具体如下如:

        绿色的块,是年轻代Young区。灰色的块是老年代Old区,而红色的是Humongous大对象区。在年轻代里的Young区,实际还会保留继续划分Eden区、S区。比如下图,绿色的是年轻代的suvivor区,浅蓝色的年轻代的Eden区。

综上:

     年轻代的存储空间为:Eden区的Region+Suvivor区的Region块组成。

     老年代的存储空间为:Old区的全部Region块组成。

     大对象存储空间:由Humongous区的Region块组成。这里需要特别说明,该区是在G1新提出的一个区,专门用来存放大对象。这个大对象的大小必须大于等于一个Region大小的一半。Region的大小一般是1~32M,比如我们设置-XX:G1HeapRegionSize=2M,设置 每个Region的大小是 2M。那如果一个对象大于了1M,就会被存放在Humongous的region,如果大于2M,比如30M,那就用15个连续的Humongous的region块来存储。「拉丁解牛说技术,实用至上,坚持用最简洁直白的文字+最少的代码示例分享干货。」

     G1在GC发生后,一个Region 的存活对象会被移动到另一个Region块上。而且重新腾空的Region块,可以从之前的角色自由切换。比如之前这个Region是用来作为Eden区的内存,现在可以作为S区、或者H区、又或者Old区的Region块。


4.2 G1为何叫做 Garbage First?

     G1的垃圾回收,不再是对某一片连续的分代内存进行GC,而是对全局的region块进行gc,在GC前,可以对每个region 的回收价值做好充分评估排序【评估哪个region目前可回收的垃圾对象多,且回收耗时短,就优先回收这类型region块】。G1通过对region的gc回收价值评估,可以实现在有限gc时间内,迅速的将那种存活对象少且占用空间较大的region优先回收。这就是G1 Garbage First的GC目标。


五、G1垃圾回收过程详解

      我们从堆内存初始状态,到堆内存占用几乎占满,发生FullGC一步步讲解。这个过程会先后触发YGC、Mixed GC、Full GC,然后再具体分析gc过程。


5.1 Young GC

      在G1垃圾收集器里,最开始堆内存只会给年轻代大概5%,比如8G内存,大概就是400Mb,按默认region块数量是2048个,那每个region大小就是4Mb。「拉丁解牛说技术,实用至上,坚持用最简洁直白的文字+最少的代码示例分享干货。」

      现在Java服务启动,随着时间推移,年轻代的初始大小400Mb被全部占完,这400M里,其中E:S1:S0=8:1:1的比例和之前一样,Eden区大概占用了320Mb,S区各占40Mb。这时候G1不一定要进行YGC,它首先要评估,这些年轻代的region的回收时间,如果远低于XX:MaxGCPauseMills(默认200ms)这个核心GC指标,就暂时不进行YGC。继续分配新region给年轻代,最终年轻代可以达到-XX:G1MaxNewSizePercent,默认是60%,也就是4.8G大小。这期间,如果发现年轻代gc的时间,接近XX:MaxGCPauseMills设置值,就必须开始做YGC。

      YGC过程大体和之前的分代回收类似,采用的是复制算法。简单的说:首先将Eden、S区,存入新的S区的Region块,或者存活对象年龄大于MaxTenuringThreshold参数以及其他可进入老年代的条件,就存入老年代的region块。然后清空参与GC的Eden、S区的region块,供年轻代、或者大对象区、老年代分配使用。【YGC实际也会对大对象H区的存活对象进行GC】


5.2 mixed GC

     混合回收,会对年轻代、老年代、大对象区region进行GC回收。触发条件是-XX:InitiatingHeapOccupancyPercent参数控制,默认值是45%。也就是老年代的存储空间应用占比达到该参数值,G1就触发进行mixed GC。采用的算法还是复制算法,所以G1没有内存碎片,这个比CMS的算法更有优越香。CMS需要配置参数进行内存整理(这个耗时大,是CMS垃圾回收器的痛点),才能解决内存碎片问题。


5.3 G1 真的有Full GC吗?

      当内存分配失败,或者没有足够内存给存活对象,元数据区内存不足等,G1就触发full gc。但是实际上JVM选择SerialOld收集器执行STW进行全面回收。虽然我们选择了G1垃圾回收器,但是发生Full gc的时候,JVM会自动切换使用SerialOld进行FullGC。

      这里再补充一点,从region回收角度看,G1就是纯纯的复制算法,非常高效。从全局来看,存活的对象需要放到新分配的region块,这是标记-清除算法。最后总结起来,GC的GC算法,细节上看是复制算法,整体是标记-清除算法。


5.4 G1的GC过程

     排除YGC,以及Full GC(之前文章有讲过这两个GC过程),这里重点说Mixed GC混合GC过程。

     在发生混合GC的时候,往往大对象,老年代都已经有很多对象。这个GC过程主要分四个阶段:初始标记、并发标记、重新标记、清理。

5.4.1 初始标记initial mark

     本阶段会进行一次stop the world,并用单线程GC去标记所有GC roots,以及标记GC roots直接可达的对象。本阶段耗时很短。

5.4.2 并发标记concurrent marking

     该阶段不会stop the world,程序恢复运行。多个GC线程开始并发标记递归 GCroots可以触达的所有对象,除了直接关联roots的,还有对象相互引用的,在这个阶段都会被标记出来。该过程比初始阶段耗时大,但是好在是并发执行,且没有stop the world。

5.4.3 最终标记final remark

      由于并发标记阶段,程序继续运行会出现新的垃圾对象,以及可能有重新被引用的对象。所以这次也要stop the world确保标记准确无误。

5.4.4 清理

      开始对每个region块的回收价值进行评估排序,并进行选择性的多线程GC回收。该过程会将参与gc回收region存活对象,复制到新region块,并更新相关引用。该过程是也是并发执行。


六、G1回收器核心参数

-XX:MaxGCPauseMills,设置GC预期回收停顿时间值,G1默认为200ms。

-XX:G1NewSizePercent,设置年轻的初始占比,默认是5%。

-XX:G1MaxNewSizePercent,设置年轻代最大百分比,默认60%。

-XX:InitiatingHeapOccupancyPercent,当年老代的存储占有率达到该参数值,G1会进行MixedGC。默认值45%。

+G1EagerReclaimHumongousObjects,YGC时是否回收大对象  ,默认是true。

-XX:G1HeapRegionSize,设置每个region块的大小,必须是2的幂,范围在1MB到32MB之间。默认情况下,这个值是堆内存的1/2000。

-XX:PretenureSizeThreshold,设置被作为大对象的阈值。默认是一个region块的大小的一半。

-XX:ParallelGCThreads,并发回收线程数量。

-XX:ConcGCThreads,参与并发标记线程数量。

-XX:G1MixedGCCountTarget,混合回收时的目标回收次数。当G1在GC达到停顿时间目标的情况下,将分成该参数设置次数完成回收。

-XX:G1MixedGCLiveThresholdPercent,在混合回收期间,当一个Region中的存活对象超过Region大小的百分比时,不对该Region进行回收。默认值是85%。


推荐阅读拉丁解牛_JVM专题系列:

1、JVM进阶调优系列(3)堆内存的对象什么时候被回收?

2、JVM进阶调优系列(2)字节面试:JVM内存区域怎么划分,分别有什么用?

3、JVM进阶调优系列(1)类加载器原理一文讲透

4、JAVA并发编程系列(13)Future、FutureTask异步小王子

相关文章
|
10月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
869 55
|
12月前
|
Prometheus 监控 算法
CMS圣经:CMS垃圾回收器的原理、调优,多标+漏标+浮动垃圾 分析与 研究
本文介绍了CMS(Concurrent Mark-Sweep)垃圾回收器的工作原理、优缺点及常见问题,并通过具体案例分析了其优化策略。重点探讨了CMS的各个阶段,包括标记、并发清理和重标记
CMS圣经:CMS垃圾回收器的原理、调优,多标+漏标+浮动垃圾 分析与 研究
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
379 27
|
12月前
|
存储 算法 Java
G1原理—5.G1垃圾回收过程之Mixed GC
本文介绍了G1的Mixed GC垃圾回收过程,包括并发标记算法详解、三色标记法如何解决错标漏标问题、SATB如何解决错标漏标问题、Mixed GC的过程、选择CollectSet的算法
G1原理—5.G1垃圾回收过程之Mixed GC
|
12月前
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
12月前
|
存储 监控 架构师
ZGC圣经:ZGC垃圾回收器的原理、调优,ZGC 漏标的 分析与 研究
ZGC圣经:ZGC垃圾回收器的原理、调优,ZGC 漏标的 分析与 研究
|
10月前
|
缓存 算法 Java
JVM深入原理(八)(一):垃圾回收
弱引用-作用:JVM中使用WeakReference对象来实现软引用,一般在ThreadLocal中,当进行垃圾回收时,被弱引用对象引用的对象就直接被回收.软引用-作用:JVM中使用SoftReference对象来实现软引用,一般在缓存中使用,当程序内存不足时,被引用的对象就会被回收.强引用-作用:可达性算法描述的根对象引用普通对象的引用,指的就是强引用,只要有这层关系存在,被引用的对象就会不被垃圾回收。引用计数法-缺点:如果两个对象循环引用,而又没有其他的对象来引用它们,这样就造成垃圾堆积。
243 0
|
10月前
|
算法 Java 对象存储
JVM深入原理(八)(二):垃圾回收
Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为StopTheWorld简称STW,如果STW时间过长则会影响用户的使用。一般来说,堆内存越大,最大STW就越长,想减少最大STW,就会减少吞吐量,不同的GC算法适用于不同的场景。分代回收算法将整个堆中的区域划分为新生代和老年代。--超过新生代大小的大对象会直接晋升到老年代。
240 0
|
12月前
|
存储 缓存 算法
G1原理—3.G1是如何提升垃圾回收效率
本文深入探讨了G1垃圾回收器提升GC效率的核心机制,包括记忆集(RSet)、位图(BitMap)和卡表(CardTable)的设计与作用。记忆集通过记录跨代引用避免了不必要的老年代遍历,位图用于高效描述内存使用状态以优化标记过程,而卡表则在节约记忆集内存的同时提供更详细的引用信息。此外,文章还解析了DCQ(Dirty Card Queue)和DCQS(Dirty Card Queue Set)机制如何异步更新RSet,确保在高并发场景下的性能与准确性。这些设计共同提升了G1在标记、清理及整理内存时的效率。
611 10
|
12月前
|
存储 算法 Java
G1原理—6.G1垃圾回收过程之Full GC
本文详细探讨了G1垃圾回收器对Full GC(FGC)的优化处理,涵盖FGC的前置处理、整体流程及并行化改进。重点分析了传统FGC串行化的局限性以及G1通过Region分区和RSet机制实现并行标记的优势,包括任务窃取提升效率、跨分区压缩以生成空闲Region等技术细节。此外,文章还介绍了G1的新特性——字符串去重优化,通过判断char数组一致性减少重复字符串占用内存,从而提升内存使用效率。总结部分全面回顾了G1在FGC中的各项优化措施及其带来的性能改善。
G1原理—6.G1垃圾回收过程之Full GC