深入解析G1垃圾回收器

news2025/1/21 5:49:16

本文已收录至GitHub,推荐阅读 👉 Java随想录

微信公众号:Java随想录

原创不易,注重版权。转载请注明原作者和原文链接

文章目录

    • 基于Region的堆内存布局
    • 可预测的停顿时间模型
    • 跨Region引用对象
    • 对象引用关系改变
    • 运作过程
    • CMS VS G1

上篇文章我们聊了CMS,这篇就来好好唠唠G1。

CMS和G1可以说是一对欢喜冤家,面试问你CMS,总喜欢把G1拿进来进行比较。

G1在JDK7中加入JVM,在JDK9中成为了默认的垃圾收集器,如果在JDK8中使用G1,我们可以使用参数 -XX:+UseG1GC 来开启。

G1和CMS相比有哪些优缺点?G1为什么能够建立可停顿的时间模型?

别着急,本篇文章告诉你答案。

G1,全名叫:Garbage First。是垃圾收集器技术发展历史上的里程碑式的成果,开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。

这句话啥意思?

在G1之前的垃圾回收器,如Parallel Scavenge、Parallel Old、CMS等,主要针对Java堆内存中的特定部分(新生代或老年代)进行操作。然而,G1将Java堆划分为多个「小区域」,并根据每个区域中垃圾对象的数量和大小来优先进行垃圾回收。

称之为「基于Region的内存布局」。

另外设计者们设计G1的时候希望G1能够建立起「停顿时间模型」,停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。

所以我们能够总结出G1身上的两个标签:

  • 基于Region的内存布局
  • 停顿时间模型

先来说说基于内存布局是怎么个事儿。

基于Region的堆内存布局

G1的基于Region的堆内存布局,这是能够建立起「停顿时间模型」的关键。

G1逻辑上分代,但是物理上不分代。

G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域,每一个区域称之为「Region」。

每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理。

好比角色扮演,不同的角色拿着不同的剧本

G1可以通过参数控制新生代内存的大小:-XX:G1NewSizePercent(默认等于5),-XX:G1MaxNewSizePercent(默认等于60)。

也就是说新生代大小默认占整个堆内存的 5% ~ 60%

G1收集器将整个Java堆划分成约「2048个大小相同的独立Region块」,每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。

可以简单推算一下,G1能管理的最大内存大约 32MB * 2048 = 64G左右。

Region中还有一类特殊的「Humongous区域」,专门用来存储大对象,可以简单理解为对应着老年代。

G1认为只要大小超过了一个Region容量一半的对象(即超过1.5个region)即可判定为大对象。

而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中。

G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。

分配大对象的时候,因为占用空间太大,可能会过早发生GC停顿。G1在每次分配大对象的时候都会去检查当前堆内存占用是否超过初始堆占用阈值IHOP(The Initiating Heap Occupancy Percent),缺省情况是Java堆内存的45%。当老年代的空间超过45%,G1会启动一次混合周期。

可预测的停顿时间模型

基于Region的停顿时间模型是G1能够建立可预测的停顿时间模型的前提。

G1将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。

G1收集器会去跟踪各个Region里面的垃圾堆积的「价值」大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表。

每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是「Garbage First」名字的由来。

这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。

所以说G1实现可预测的停顿时间模型的关键就是Region布局优先级队列。看起来好像G1的实现也不复杂,但是其实有许多细节是需要考虑的。

跨Region引用对象

首先第一个问题:G1将Java堆分成多个独立Region后,Region里面存在的跨Region引用对象如何解决?

本质上还是我们之前提过的「跨代引用」问题,解决方案的思路我们已经知道,使用「记忆集」。

G1的记忆集在存储结构的本质上是一种「哈希表」,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。

使用记忆集固然没啥毛病,但是麻烦的是,G1的堆内存是以Region为基本回收单位的,所以它的每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。

由于Region数量较多,每个Region都维护有自己的记忆集,光是存储记忆集这块就要占用相当一部分内存,G1比其他圾收集器有着更高的内存占用负担。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作。

这可以说是G1的缺陷之一。

除了跨代引用外,对象引用关系改变,如何解决?

对象引用关系改变

解决的办法我们之前在讲「三色标记算法」的时候提过,G1使用「原始快照」来解决这一问题。

垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要继续运行就肯定会持续有新对象被创建。

G1为每一个Region设计了两个名为「TAMS(Top at Mark Start)」的指针。

把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。

与CMS中的「Concurrent Mode Failure」失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间Stop The World。

G1可以通过-XX:MaxGCPauseMillis参数设置垃圾收集的最大停顿时间的JVM参数,单位为毫秒。

在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。

然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。

G1收集器会根据这个设定值进行自我调整以尽量达到这个暂停时间目标。例如,如果设定了-XX:MaxGCPauseMillis=200,那么JVM会尽力保证大部分(但并非全部)的GC暂停时间不会超过200毫秒。

运作过程

  1. 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
  2. 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象
  3. 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
  4. 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

从上述阶段的描述可以看出,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的。

G1在逻辑上仍然采用了分代的思想,从整体来看是基于「标记-整理」算法实现的收集器,但从局部(两个Region之间)上看又是基于「标记-复制」算法实现。

这时候有些点子王可能会想,如果我把-XX:MaxGCPauseMillis,调的非常小,那是不是就回收的更快了?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZmN6LAmN-1693277073940)(https://mmbiz.qpic.cn/mmbiz_jpg/jC8rtGdWScOt4Libmz1bw1WwRF0kfXiaCBqyBkHebh0n4oKickldRJRPAA60ibtlEJfYhrVaekBg3Y8dXrOuwWUeDA/640)]

G1默认的停顿目标为两百毫秒,但如果我们把停顿时间调得非常低,譬如设置为二十毫秒,很可能出现的结果就是由于停顿目标时间太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器收集的速度逐渐跟不上分配器分配的速度,导致垃圾慢慢堆积。

应用运行时间一长,最终占满堆引发Full GC反而降低性能,所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。

CMS VS G1

相比CMS,G1的优点有很多,较为明显的优点就是G1不会产生垃圾碎片

But,G1相对于CMS仍然不是占全方位、压倒性优势的,至少G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。

就内存占用来说,虽然G1和CMS都使用卡表来处理跨代指针,但G1的每个Region都必须有一份卡表,这导致G1的记忆集可能会占整个堆容量的20%乃至更多的内存空间,相比起来CMS的卡表就相当简单,全局只有一份。

在执行负载的角度上,譬如它们都使用到写屏障,CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行同样的卡表维护操作外,为了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。

相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。

由于G1对写屏障的复杂操作要比CMS消耗更多的运算资源,所以CMS的写屏障实现是直接的同步操作,而G1就不得不将其实现为类似于消息队列的结构,把写前屏障和写后屏障中要做的事情都放到队列里,然后再异步处理。

目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间。

文章的最后放一组G1的常用参数:

参数描述
-XX:+UseG1GC手动指定使用G1收集器执行内存回收任务(JDK9后不用设置,默认就是G1)
-XX:G1HeapRegionSize设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000
-XX:MaxGCPauseMillis设置期望达到的最大GC停顿时间指标
-XX:InitiatingHeapOccupancyPercent简称为IHOP,设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45%
-XX:+G1UseAdaptiveIHOP自动调整IHOP的指,JDK9之后可用
-XX:GCTimeRatio这个参数为0~100之间的整数(G1默认是9),值为 n 则系统将花费不超过 1/(1+n) 的时间用于垃圾收集。因此G1默认最多 10% 的时间用于垃圾收集

最后吐槽一句,JVM真的很难,垃圾收集器的内部原理实在太复杂,如果要深究需要长时间的积累。

当然我们不是JVM的专业人员,不需要学的那么深入,这篇文章讲到的内容能基本应付面试和工作场景了。

那本篇文章到这结束啦,我们下篇再见👋🏻。


感谢阅读,如果本篇文章有任何错误和建议,欢迎给我留言指正。

老铁们,关注我的微信公众号「Java 随想录」,专注分享Java技术干货,文章持续更新,可以关注公众号第一时间阅读。

一起交流学习,期待与你共同进步!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/943249.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

部队物资仓库管理系统|DW-S301是一套成熟系统

根据军队物资装备管理的实际需求,集驰电子设计了部队物资仓库管理系统(智装备:DW-S301)。 随着军事装备物资的使用频率与消耗数量日益增加,部队对于仓库保障工作的要求越来越高,同时也带来仓库管理工作任务…

电子仓库预测水浸事件,他怎么做到的?

仓库环境中水浸事件可能导致严重的损失,不仅对货物造成损害,还可能影响设备的正常运行甚至威胁安全。 因此,为了应对这一挑战,引入一套完善的仓库水浸监控系统成为了不可或缺的措施。 客户案例 广东某电子公司是一家领先的电子设…

kafka调优配置

Kafka生产者核心参数配置 来源于尚硅谷 参数名称描述bootstrap.servers生产者连接集群所需的broker地址清单。例如hadoop102:9092,hadoop103:9092,hadoop104:9092,可以设置1个或者多个,中间用逗号隔开。注意这里并非需要所有的broker地址,因…

bpmnjs Properties-panel拓展(ExtensionElements拓展篇)

接上文bpmnjs Properties-panel拓展&#xff08;属性设置篇&#xff09;&#xff0c;继续记录下第三个拓展需求的实现。 需求简述 在ExclusiveGateway标签的extensionElements标签中增加子标签<activiti:executionListener>子标签&#xff0c;可增加复数子标签。子标签…

vLLM 实战

引言 随着人工智能技术的飞速发展&#xff0c;以及今年以来 ChatGPT 的爆火&#xff0c;大语言模型 (Large Language Model, LLM) 受到越来越多的关注。 为了实现 LLM 部署时的推理优化&#xff0c;全球各地有众多团队做出了各种优化框架。本文以加州大学伯克利分校开发的 vLLM…

R语言之基础绘图

文章和代码已经归档至【Github仓库&#xff1a;https://github.com/timerring/dive-into-AI 】或者公众号【AIShareLab】回复 R语言 也可获取。 文章目录 1. 函数 plot( )2.直方图和密度曲线图3.条形图4. 饼图5. 箱线图和小提琴图6. 克利夫兰点图7. 导出图形小结 R 的基础绘图系…

大模型开发05:PDF 翻译工具开发实战

大模型开发实战05:PDF 翻译工具开发实战 PDF-Translator 机器翻译是最广泛和基础的 NLP 任务 PDF-Translator PDF 翻译器是一个使用 AI 大模型技术将英文 PDF 书籍翻译成中文的工具。这个工具使用了大型语言模型 (LLMs),如 ChatGLM 和 OpenAI 的 GPT-3 以及 GPT-3.5 Turbo 来…

【通用消息通知服务】0x4 - 目前进展 阶段复盘

【通用消息通知服务】0x4 - 阶段复盘 达成 基本的API已经写完✍️了(消息查看发送, 模板crud,终端crud,发送渠道crud,计划crud,计划执行查看)拆分server, executor, planner三个入口, 方便针对性水平扩展整体架构初步形成&#xff0c;通过队列实现了事件驱动模型和消息订阅发…

Spring Boot(Vue3+ElementPlus+Axios+MyBatisPlus+Spring Boot 前后端分离)【六】

&#x1f600;前言 本篇博文是关于Spring Boot(Vue3ElementPlusAxiosMyBatisPlusSpring Boot 前后端分离)【六】&#xff0c;希望你能够喜欢 &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章…

Apache RocketMQ 5.0 消息进阶:如何支撑复杂的业务消息场景?

作者&#xff1a;隆基 一致性 首先来看 RocketMQ 的第一个特性-事务消息&#xff0c;事务消息是 RocketMQ 与一致性相关的特性&#xff0c;也是 RocketMQ 有别于其他消息队列的最具区分度的特性。 以大规模电商系统为例&#xff0c;付款成功后会在交易系统中订单数据库将订单…

三---开关稳压器

通过控制系统反馈&#xff0c;当电压上升时通过反馈降低&#xff0c;当电压下降时通过反馈升高&#xff1b;形成一个控制环路&#xff1b;控制电路&#xff1a;PWM&#xff08;脉宽调制&#xff09;&#xff0c;PFM&#xff08;频率控制方式&#xff09;&#xff0c;移相控制方…

2023开学季有哪些电容笔值得买?平价电容笔推荐

在日常生活中&#xff0c;这支电容笔的使用范围很广&#xff0c;不管是和电脑、IPAD、手机一起使用&#xff0c;都可以说是一种很好的办公工具。首先要弄清楚自己的需求&#xff0c;再根据自己的需求来挑选合适的商品。苹果的Pencil有一种独特的重力压感&#xff0c;让人在上面…

【MTK平台】根据kernel log分析wifi scan的时候流程

一 概要: 本文主要讲解根据kernel log分析下 当前路径下(vendor/mediatek/kernel_modules/connectivity/wlan/core/gen4m/)wifi scan的时候代码流程 二. Log分析: 先看Log: 2.1)在Framework层WifiManager.java 方法中,做了一个标记,可以精准的确认时间 这段log可以…

基于Qt5开发图形界面——WiringPi调用Linux单板电脑IO

Qt5——WiringPi Qt5WiringPi示例教程 Qt5 Qt是一种跨平台的应用程序开发框架。它被广泛应用于图形用户界面&#xff08;GUI&#xff09;开发&#xff0c;可以用于构建桌面应用程序、移动应用程序和嵌入式应用程序。Qt提供了丰富的功能和工具&#xff0c;使开发人员可以快速、高…

企业内部搭建wiki的意义大吗?到底有何价值体现?

内部Wiki也叫做企业Wiki&#xff0c;是员工可以存储、共享和协作创作的地方&#xff0c;将企业内部员工知识共享集中到一个地方&#xff0c;并且相关内容与其他团队成员协作完成&#xff0c;它可以包含企业内部的各种知识&#xff0c;从操作指南到培训手册&#xff0c;再到客户…

ipad有必要用手写笔吗?开学季实惠的电容笔推荐

iPad平板的机型经过了一次又一次的升级&#xff0c;增加了更多的功能&#xff0c;如今已有了与笔记本电脑匹敌的能力。而到了如今&#xff0c;科技的发展&#xff0c;iPad也从一个娱乐工具&#xff0c;变成了一个集学习、画画、办公于一体的强大工具。为了提高生产效率&#xf…

Linux内核学习(十二)—— 页高速缓存和页回写(基于Linux 2.6内核)

目录 一、缓存手段 二、Linux 页高速缓存 三、flusher 线程 Linux 内核实现了一个被叫做页高速缓存&#xff08;page cache&#xff09;的磁盘缓存&#xff0c;它主要用来减少对磁盘的 I/O 操作。它是通过把磁盘中的数据缓存到内存中&#xff0c;把对磁盘的访问变为对物理内…

比较器DATESHEET参数

失调电压的理解 Input Offset Voltage 理想情况下&#xff0c;如果运算放大器的两个输入电压完全相同&#xff0c;则输出应为0V。但运放内部两输入支路无法做到完全平衡&#xff0c;导致输出永远不会是0&#xff0c;具体见图1所示。此时保持放大器负输入端不变&#xff0c;而在…

火绒能一键修复所有dll缺失吗?教你快速修复dll文件

关于dll文件的缺少&#xff0c;其实大家应该都是不陌生的吧&#xff0c;毕竟只要是经常使用电脑的人&#xff0c;那么它就一定碰到过各种各样的dll文件缺失&#xff0c;因为很多程序都是需要dll文件来支撑的&#xff0c;如果dll文件丢失了&#xff0c;那么一些程序就会启动不了…

新版白话空间统计(27):从离散点到密度图

点的密度是点分析中一个很重要的方向&#xff0c;有大量的点数据的空间表达&#xff0c;基本上都是通过密度图来进行体现的&#xff0c;比如百度热力图&#xff1a; 又或者是交通车流量热力图&#xff1a; 空间点密度分析&#xff0c;把海量离散的点&#xff0c;变成高度抽象的…