G1 收集器
基础知识
性能指标
在调优Java应用程序时,重点通常放在两个主要目标上:响应性 或 吞吐量。
响应性Responsiveness
是指应用程序对请求的数据做出响应的速度:
- 桌面用户界面对事件的响应速度
- 网站返回页面的速度
- 数据库查询的返回速度
吞吐量Throughput
专注于最大程度地提高应用程序在特定时间段内的工作量:
- 在给定时间内完成的事务次数
- 批处理程序在一小时内可以完成的作业数
- 一小时内可以完成的数据库查询数
较长的暂停时间Pause Time
对于注重响应性的应用程序是不可接受的,但对于注重吞吐量的应用程序来说可以接受的。前者重点是在短时间内做出响应,后者则侧重与长时间运行的处理效率。
GC 基础
GC Root
可达性分析是 Java GC 算法的基础,基本思路就是以一系列名为 GC Roots
对象作为起始点,通过引用关系遍历对象图,如果一个对象到 GC Roots
间没有任何可达路径相连时,则说明此对象可以被回收。
可以作为 GC Roots
的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中JNI(即一般说的native方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
三色标记
可达性分析中重要的一环就是遍历整个堆,并标记其中的存活对象。一种常用的标记算法是 三色标记法tri-color marking
:
每个对象可能为以下 3 种颜色之一:
- white — 未被标记
- gray — 本身已标记,但部分引用的对象完成标记(动图的黄色对象)
- black — 本身已标记,且所有引用的对象完成标记(动图的蓝色对象)
标记算法从 GC Roots 出发遍历堆,可达对象先标记 gray,然后再标记 为 black。
遍历完成之后所有可达对象都是 black 的,此时所有标记为 white 的对象都是可以回收的。
当实现并发标记算法时,必须防止 white 对象被漏标,否则可能导致不该回收的对象被回收。
分代收集
传统垃圾收集器将堆分成三个部分:年轻代YoungGen = Eden + Survivor
,老年代OldGen
和永久代PermGen
,每个区域内存连续且大小固定。
- 年轻代:一次性使用的临时对象(例如:方法中构造的临时对象)
- 老年代:被长期引用的常驻对象(例如:缓存对象、单例对象)
- 永久代:JVM 运行过程中一直存在的对象(例如:字符串常量、类信息)
将堆内存进行划分后,可以按照对象生命周期长短,在不同区域使用不同的回收算法,提高 GC 的效率。
算法分类
Mark and Sweep标记-清除
用一个空闲列表free-list
记录失效对象占用的内存区域,方便后续重新分配给新对象。
- 回收原理简单,GC 停顿时间短
- 维护空闲列表需要一定的空间开销
- 内存碎片较多,可能导致内存分配失败
Mark-Sweep-Compact标记-整理
将所有存活对象移动到内存区域的开头,剩余的连续内存区域都是可用的空闲空间。
- 通过指针碰撞查找空闲空间,分配速度快
- 内存碎片少,内存分配失败概率低
- 复制对象会导致较长时间的 GC 停顿
Mark and Copy标记-复制
将内存划分为活动区间与空闲区间,前者用于动态分配对象,后者用于容纳 GC 存活对象。
GC 时只需将存活对象从前者复制到后者,然后交换两者的角色即可。
- 标记和复制在同一阶段同时进行,当存活对象少时回收效率极高
- 需要预留一个空闲空间用于容纳存活对象,造成内存浪费
CMS 回顾
CMS Concurrent Mark-Sweep
是一个采用 标记-清除 算法的老年代收集器。
它通过与应用程序线程并发执行大多数垃圾回收工作,来最大程度地减少由于 GC 导致的暂停。
通常情况下,CMS 收集器不会复制或压缩活动对象,这意味着无需移动活动对象即可完成垃圾回收。
然而过多的内存碎片可能造成分配失败,最终导致 FullGC。可以通过分配更大的堆来规避这一问题。
CMS 对老年代的回收可以分为以下几个步骤:
Initial Mark (STW)
初始标记
- 标记 GC Roots 直接可达的老年代对象
- 遍历新生代存活对象,标记直接可达的老年代对象
Concurrent Mark
并发标记
GC 线程遍历 Initial Mark 阶段标记出来存活的老年代对象,然后递归标记这些可达的对象。
该阶段与应用线程并发运行,期间会发生新生代对象晋升、老年代对象引用关系更新,需要对这些对象进行重新标记,避免发生遗漏。
CMS 用一个
card-table
管理老年代,并发标记过程中,某个对象的引用关系发生了变化,则将对象所在的内存块标记为 Dirty Card。CMS 使用增量更新
incremental update
解决并发修改导致的漏标问题:把 black 对象重新标记为 grey,下次重新扫描其引用。Preclean
预清理
这一阶段主要是处理 Concurrent Mark 阶段中引用关系改变,导致没有标记到的存活对象的。通过并发地重新扫描这些对象,预清理阶段可以减少 Remark 阶段的 STW。
这个阶段会处理前一个阶段被标记为 Dirty Card 的部分,将其中变化了的对象作为 GC Root 再进行扫描并重新标记。
Abortable Preclean
可终止的预清理
这个阶段作用与 Preclean 类似,但可以通过设置 扫描时长(默认5秒)或 Eden 区使用占比(默认50%)控制本阶段的结束时机。
增加这一阶段的原因,是期待这期间能发生一次 YoungGC 清理无效的年轻代对象,减少 Remark 阶段扫描年轻代的时间。
Remark (STW)
重新标记
:这个阶段同时扫描 YoungGen 与 OldGen,重新标记整个老年代中所有存活对象。
由于之前的 Concurrent Mark 与 Preclean 阶段是与用户线程并发执行的,年轻代对老年代的引用可能已经发生了改变,Remark 要花很多时间处理这些改变,会导致长时间的 STW。
此外,即使新生代的对象已经不可达了,CMS 也会使用这些不可达的对象当做的 GC Roots 来扫描老年代,导致部分失效的老年代对象无法被及时回收。
可以加入参数 -XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次 YoungGC,回收掉年轻代的对象无用的对象。这样进行年轻代扫描时,只需要扫描 Survivor 区的对象即可,一般 Survivor 区非常小,这大大减少了扫描时间。
Concurrent Sweep
并发清理
至此,老年代所有存活的对象已经被标记完成。这个阶段主要是清除那些没有标记的对象并且回收空间。
被回收的空间会被添加到 空闲列表中,以供以后分配。这一过程可能会对空闲空间进行合并,但是不会移动存活对象。
由于该阶段是与应用线程并发运行的,自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,无法在当次收集中处理掉它们。只好留待下一次GC时再清理掉。这一部分垃圾就称为 浮动垃圾。
Resetting
重置
清除数据结构,并重置定时器,为下一轮 GC 做准备。
G1 算法
设计目的
G1 Garbage-First
是一种服务器端的垃圾收集器:
- 可以与应用程序线程并行运行,减少 STW
- 整理空闲空间减少内存碎片,但不引入较长的 GC 暂停时间
- 提供可预测的GC暂停时间,无需牺牲很多吞吐量
G1 能够在大内存的多处理器计算机上,保证 GC 暂停时间可控,并实现高吞吐量。
其最终目的是取代 CMS 成为服务端 GC 更好的解决方案:
- 采用 标记-整理 算法,可以避免使用细粒度的空闲列表进行分配。简化了收集器设计并消除了潜在的碎片问题。
- 使用 增量回收
incremental collecting
算法,其 GC 暂停时间比 CMS 更具可预测性,并允许用户指定期望的暂停时间。
基本概念
G1 将堆划分为一组大小相等的且连续的堆区域Region
:
G1 中新生代与老年代不再连续,每个区域可以在 Eden、Survivor 与 Old 之间切换角色。此外,还有一类被称为 Humongous 的巨型区域,用于容纳体积 ≥ 标准区域大小的50%的对象。JVM 通常会将内存划分为 2000个区域,每个大小从 1 到 32Mb 不等,由 JVM 在启动时通过 -XX:G1HeapRegionSize 指定。
每个区域会被进一步细分成多个卡片Card
,每个大小为 512Kb,用于实现细粒度的引用统计。
分区设计可以避免一次收集整个堆,每次 GC 只收集区域的一个子集 CSetcollection set
,其中必然包含所有 Young 区域,同时可能包括部分 Old 区域:
根据回收区域的不同,可以将 GC 分为:
- YoungGC:CSet 只包含 Young 区域
- MixedGC: CSet 同时包含 Young 与 Old 区域
- FullGC: 回收整个堆(可用空间耗尽时触发,单线程执行)
G1 根据存活对象的字节数统计每个区域的 活跃度liveness
,然后根据期望停顿时间来确定该 CSet 的大小,并保证那些垃圾多(活跃度低)的区域会被优先回收,故此得名 垃圾优先。
G1 的执行过程可以表示为由 3 个阶段组成的循环:
Young GC
堆中一开始只有 YoungGen,因此只会触发 YoungGC,将 Eden 与 Survivor 区域中的活动对象复制到另一个空闲的 Survivor 区域。


G1 中将 将存活对象复制到其他区域 的过程称为 疏散Evacuation
。为了减少停顿时间,疏散工作由多个 GC 线程并行完成。
YoungGC 过程中会根据预期目标停顿时间 -XX:MaxGCPauseMillis 动态调整新生代的大小,通过 -XX:G1NewSizePercent 参数可以人为干预这一过程,但会让预期停顿时间参数失效。
当堆的整体占用空间足够大时(超过45%),就会进入 Concurrent Marking 阶段。通过 -XX:InitiatingHeapOccupancyPercent 选项可以配置这一行为。
Concurrent Marking
与 CMS 类似,G1 中的并发标记包括多个阶段,其中一些阶段是并发的,另一些阶段则会 STW。
Initial Mark (STW)
初始标记
扫描并标记 GC Root 对象直接可达的老年代存活对象。
Initial Mark 并没有独立的执行阶段,而是嵌入 YoungGC 中执行的,其停顿时间会被分摊,因此实际的开销非常低。
Root Region Scan
扫描根区域
扫描 Root Region 并标记所有可达的老年代存活对象。
此处的 Root Region 就是先前 YoungGC 中生成的 Survivor 区域,其包含的对象都会被视为 GC Root。
为了避免移动对象对标记产生影响,该过程必须在下次 YongGC 启动前完成。
Concurrent Mark
并发标记
启动并发标记线程,扫描并标记整个堆中的存活对象(线程数可以通过 -XX:ConcGCThread 进行配置)。
为了避免重复标记,G1 使用 SATB
snapshot-at-the-beginning
算法解决漏标问题:应用线程对在 Concurrent Mark 执行期间进行的所有并发更新,都应保留先前的已知标记信息。该约束是通过预写屏障
pre-write barrier
实现:Concurrent Mark 扫描过程中,当应用线程修改某个字段时,会将先前的引用对象存储在日志缓冲区log buffers
中,然后交由并发标记线程处理。为了避免移动对象对标记产生影响,该过程必须在下次 YoungGC 启动前完成。所有的标记任务必须在堆满前完成,如果堆满前没有完成标记任务,则会触发担保机制,经历一次长时间的串行 FullGC。
Remark (STW)
重新标记
启动并行标记线程,完成对整个堆中存活对象的标记(线程数可以通过 -XX:ParallelGCThread 进行配置)。
该阶段会暂停所有应用线程,避免发生引用更新,并完成对SATB 日志缓冲区中剩余对象的标记,找出所有未被访问的存活对象。
该阶段还执行一些额外的清理操作,例如:
- 卸载不可达的类(通过 -XX:+ClassUnloadingWithConcurrentMark 开启)
- 处理引用对象(弱引用、软引用、虚引用、最终引用)
Cleanup
清理垃圾
整理统计信息并识别出高收益的老年代分区,为 MixedGC 做准备。
主要工作有:
- RSet 梳理(后续说明)
- 识别回收收益高的老年代分区 (基于释放空间和暂停目标)
- 直接回收的没有活跃对象的空闲分区
此外还会执行一些清理工作,为下一次 Concurrent Marking 做好准备。
Mixed GC
MixedGC 主要流程与 YoungGC 类似,不同的地方在于 CSet 中包含了 Old 区域。
需要注意的是,Concurrent Marking 结束后,并不一定会立即触发 MixedGC,中间可能会穿插多次的 YoungGC。
当收集某个区域时,我们必须知道是否有来自非收集区域引用,来确定它们的活动性:
- 从非收集区域到收集区域的 incoming reference 是重要的(被非收集区引用的对象必须存活)
- 从收集区域到非收集区域的 outgoing reference 是可忽略的(非收集区域不参与GC)
但查找整个堆非常耗时,同时也失去了增量收集的优势。为了解决这一问题,G1 为每个区域维护了一个 RSetremembered set
,用于记忆从其他区域指向自己的引用。
收集过程
在执行收集时,RSet 中引用信息会扮演局部 GC Roots 的角色,避免耗时的引用查找,保证每个区域的 GC 能够独立进行:
注意,象如果 Old 区域中对在 Concurrent Marking 阶段被确定为垃圾,即使有外部引用,该对象也会被作为垃圾回收。
接下来发生的事情与其他收集器所做的相同:多个并行GC线程找出哪些对象是活动的,哪些对象是垃圾:
最后,释放空闲区域,将活动对象移到 Survivor 区域,并在必要时创建新对象:
RSet 维护
为了维护 RSet,在应用线程对字段执行写操作时,会触发写后屏障post-write barrier
:
为了减少写屏障带来的开销,该过程是异步的:
Dirty Card Queue
,然后由 Refine 线程将其拾取并将信息传播到被引用区域的 RSet。如果应用线程插入速度过快,会导致 Refine 线程来不及处理,那么应用线程将接管 RSet 更新的任务,从而导致性能下降。
总结
并发标记 与 增量收集 是 G1 实现高性能与可预测回收的关键。
对于 CPU 资源充足且对延迟敏感的服务端应用来说,G1 算法能够在大堆上提供良好的响应速度。
作为代价,额外的写屏障与更活跃GC线程,会对应用的吞吐量产生负面影响。
参考资料
- https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
- https://plumbr.io/handbook/garbage-collection-algorithms
- https://medium.com/@hansrajchoudhary_88463/evolution-of-garbage-collection-on-java-garbage-first-garbage-collection-a3f39b1a9ae0
- https://juejin.cn/post/6844903960550047757#heading-10
- https://segmentfault.com/a/1190000021761004
G1 收集器的更多相关文章
- JAVA G1收集器 第11节
JAVA G1收集器 第11节 上两章我们讲了新生代和年老代的收集器,那么这一章的话我们就要讲一个收集范围涵盖整个堆的收集器——G1收集器. 先讲讲G1收集器的特点,他也是个多线程的收集器,能够充分利 ...
- G1收集器-原创译文[未完成]
G1收集器-原创译文 原文地址 Getting Started with the G1 Garbage Collector 目的 本文介绍了如何使用G1垃圾收集器以及如何与Hotspot JVM一起使 ...
- CMS收集器和G1收集器优缺点
首先要知道 Stop the world的含义(网易面试):不管选择哪种GC算法,stop-the-world都是不可避免的.Stop-the-world意味着从应用中停下来并进入到GC执行过程中去. ...
- JVM垃圾收集器-G1收集器
G1收集器是当前收集器技术发展的最前沿成果,在JDK1.6_Updata14中提供了Early Access版本的G1收集器以供适用.G1收集器是垃圾收集器理论进一步发展的产物,它与前面的CMS收集器 ...
- JVM-如何判断对象存活与否与CMS收集器和G1收集器的区别
JVM如何判断对象存活? 1.计数器 2.可达性分析 (很多主流语言采用这种方法来判断对象是否存活) 计数器:每当有一个地方引用该对象时,计数器 +1:引用失效则 -1: 优点:实现简单,判定效率 ...
- G1收集器的收集原理
G1收集器的收集原理 来源 http://blog.jobbole.com/109170/ JVM 8 内存模型 原文:https://blog.csdn.net/bruce128/article/d ...
- CMS垃圾收集器与G1收集器
1.CMS收集器 CMS收集器是一种以获取最短回收停顿时间为目标的收集器.基于“标记-清除”算法实现,它的运作过程如下: 1)初始标记 2)并发标记 3)重新标记 4)并发清除 初始标记.从新标记这两 ...
- G1收集器
转载:https://blog.csdn.net/zhou2s_101216/article/details/79202893 http://blog.jobbole.com/109170/ http ...
- 垃圾收集器之:G1收集器
G1垃圾收集器是一种工作在堆内不同分区上的并发收集器.分区既可以归属于老年代,也可以归属新生代,同一个代的分区不需要保持连续.为老年代设计分区的初衷是我们发现并发后台线程在回收老年代中没有引用的对象时 ...
- CMS收集器和G1收集器
CMS收集器 CMS收集器是一种以获取最短回收停顿时间为目标的收集器.基于"标记-清除"算法实现,它的运作过程如下: 初始标记 并发标记 重新标记 并发清除 初始标记.从新标记这两 ...
随机推荐
- IntelliJ IDEA 2020.2.3永久破解激活教程 - 2020.10.27
申明:本教程 IntelliJ IDEA 破解补丁.激活码均收集于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除 不花钱 的方式 IDEA 2020.2 激活到 2089 年 注意:教程适 ...
- 算法基础——KMP字符串匹配
原题链接 题目: 给定一个模式串S,以及一个模板串P,所有字符串中只包含大小写英文字母以及阿拉伯数字. 模板串P在模式串S中多次作为子串出现. 求出模板串P在模式串S中所有出现的位置的起始下标. 输入 ...
- FL Studio音乐编曲入门教程
有很多小伙伴给我们留言说使用FL Studio20一段时间后,虽然对这款音乐编曲软件的功能有了一个了解,但对它整个的编曲过程还不是太熟悉.所以今天我就给大家带来了FL Studio20这款音乐编曲软件 ...
- 前后端分离之前端vue
npm install --global vue-clivue init webpack my-project cd my-project npm install npm run dev ...
- ios开发中如何调用苹果自带地图导航
前段时间一直在赶项目,在外包公司工作就是命苦,天天加班不说,工作都是和工期合同挂钩的,稍微逾期就有可能被扣奖金,不谈这些伤脑筋的事情了,让我们说说iOS开发中如何调用苹果手机自带的地图. 学习如逆水行 ...
- 【震惊】手把手教你用python做绘图工具(一)
在这篇博客里将为你介绍如何通过numpy和cv2进行结和去创建画布,包括空白画布.白色画布和彩色画布.创建画布是制作绘图工具的前提,有了画布我们就可以在画布上尽情的挥洒自己的艺术细胞. 还在为如何去绘 ...
- 当vue.js与其他js文件同时引用导致页面不显示的问题
作为一个萌新,昨天学习的过程中遇到了vuejs与其他js在共同页面时引用时冲突的问题 具体如下 虽然注意到了前后顺序,但是页面还是出不来东西 我知道现实开发中可能不是这么引用,但是学习中是这么引入的, ...
- 初学者值得拥有Hadoop单机模式环境搭建
单机模式Hadoop环境搭建 Hadoop环境搭建流程图 具体过程 文章目录 单机模式Hadoop环境搭建 Hadoop环境搭建流程图 具体过程 1.搭建准备工作 (1)关闭防火墙 (2)关闭seli ...
- 第7.18节 案例详解:Python类中装饰器@staticmethod定义的静态方法
第7.18节 案例详解:Python类中装饰器@staticmethod定义的静态方法 上节介绍了Python中类的静态方法,本节将结合案例详细说明相关内容. 一. 案例说明 本节定义了类Sta ...
- 第8.23节 Python中使用sort/sorted排序与“富比较”方法的关系分析
一. 引言 <第8.21节 Python中__lt__.gt__等 "富比较"("rich comparison")方法用途探究>和<第8.2 ...