巨问G1 G1 G1
巨问G1 G1 G1
- 由来
- 问:G1与堆内存?
- 两个重要的参数
- G1 如何划分内存
- 2问:G1如何工作?
- G1与Young GC(名词)
- Young GC的触发
- 对象引用标记(GC前)
- 并发标记
- Young GC (Stop The World)
- Old GC
- Old GC的触发
- 堆占用45%
- A. 标记(Old GC期间)
- A1. Tri-Color Marking
- 并发竞争与解决方案
- GC线程与应用线程竞争
- 解决方案
- B. Old GC 的"间歇性" STW
由来
因为"某些原因"需要对JAVA的垃圾回收机制进行研究, 有趣的是JAVA这门语言因自动内存回收快速兴起, 时下对一名互联网开发者的"评分标准"却往往必须包含精通垃圾回收原理这一项(人人精通JVM原理, 轻松改变世界), 网络上关于垃圾回收的博文良莠不齐, 很难分辨到底是一篇好的博文还是东抄西凑的大道理杂烩.
最近看了不少关于G1的知识, 带来了不少收获的同时也带来了大量的疑问,写这篇博客的目的是希望尽可能地用易懂的方式揭开G1的面纱, 同时对G1中的核心知识点进行总结
本文仅代表个人理解, 如果有任何疑惑或者见解欢迎大佬们指出
问:G1与堆内存?
软件开发中, 一个新技术的出现往往伴随一篇有新想法的文章和一个想要实现这篇文章的团队,G1的出现也经历了这样的过程:
- 2004年发版第一篇文章
- 2012年第一次在JDK 7u4落地
- 作为JDK 9 的默认垃圾回收机制
G1做为一款"低中断"的垃圾回收器,诞生的目的是为了长久取代CMS(另一款垃圾回收器,有趣的是最近看到G1和CMS的相关知识在某些地方被糅合在了一起)
如果你从不了解CMS, 相信你会喜欢这张图(摘自):
上图是CMS的经典"亚当" “幸存者” “永久” 堆内存划分, 与CMS不同的是 G1 从本质上对堆内存的划分进行重构, 本文会后续讲解在G1下的内存结构
两个重要的参数
java -Xmx16G -XX:MaxGcPauseMillis=666
在涉猎G1 的的内存划分机制前必须先浅谈上述两个参数,顾名思义,第一个参数-Xmx16G(标记为参数P1) 代表堆内存的最大分配空间为16g, 第二个参数-XX:MaxGcPauseMillis=666(标记为P2)代表G1进行young GC 与 mix GC 时的STW(stop the world)时间. (注意: 非最大停止时间), 两个参数会在后文中被引用,为了方便我将它们分别标记为了P1与P2.
G1 如何划分内存
G1会将堆内存划分为2048个regions, 值得一提的是这个regions 的数量与你分配给它的堆内存空间大小无关.
e.g 分配2G堆内存给G1管理单个region的大小即为1M, 同理4G下单region为2M…
2048个regions -> 4类:
- Eden(young generation)
- Survior(young generation)
- Old(old generation)
- Humongous(特殊对待)
G1新增了一种标记为Humongous region,这种region产生的契机为:该region有一个对象占用了50% 的内存空间 e.g 2G堆内存下一个Object[]大小超过512KB, 4G下超过1M. 注意的是这种region"游离于五行之外"且内存回收效率极低. 潜台词就是:你堆内存分配的越多,这种region出现的频率就越低,G1的内存管理就越高效 ——简单来讲就是机器内存大点内存管理就好点
综上所述, JVM在启动的初期根据参数 P1 & P2 会对regions进行标记, 告诉GC(名次)哪些区域该是Eden regions哪些regions该划分为其他类型的regions, 值得一题的是这些被划分的regions类型会在一次次GC(动词)下变化, 简单的来说,你可以把堆内存理解为2048个小格子(请见谅我的作图):
E: Eden
S: Survior
O: Old
H: Humongous
2问:G1如何工作?
这是个很"#@#@&"的问题, 我希望在 young GC & old GC 两方面分别回答(个人见解)
G1与Young GC(名词)
Young GC的触发
在前面我们知道了JVM启动时会将堆内存划分为2048个regions, 而在创建对象(new)的时候JVM会进行以下步骤:
- 1.计算对象占用的内存空间
- 2.占据一块连续的堆内存空间(Eden region中)
- 3.将该堆内存空间地址分配给该对象
注: 步骤2与3的顺序可能发生变化,本文不详讲,有兴趣的童鞋可以看看相关资料
应用程序在执行的过程中伴随着大量对象的构建(new), 在一次次的执行中JVM启动初期划分的Eden regions会被一一填满, 当所有被分配的Eden regions填满时Young GC就触发了
对象引用标记(GC前)
Young GC(名词)在进行资源回收(Stop The World)前必须知道哪些资源需要被回收而哪些资源正在被应用程序使用.
简单来讲,Young GC 仅仅会对Eden & Survior regions起作用, 在这些regions内的对象, GC 需要确定哪些被外部引用(e.g Old Regions 中有个map对象指向了 Eden regions中的一个对象)而哪些未被引用, 对于未被指向的对象, GC才会考虑是否回收.
关于GC的标记算法很多博文喜欢描述为"GC使用并发标记的方式且不影响程序执行…". 本人认同这种说法, 但是认为这种描述必须伴随着一系列的前置条件, 本文会在该小节依次讲解Young GC(名词) 与标记的关系
并发标记
讲这点前必须先简洁的介绍一下两种与标记相关的数据结构:
- Card Table
- Remembered Set
应用程序在执行阶段伴随着大量的指针变更 e.g obj.field = reference. JVM在执行赋值操作(操作符:=)时会使用"write barrier"技术注入额外的代码,这段代码对开发者(我们)往往是无感知的, 目的是为了将这次指针的变更记录在一个叫做Card Table的队列中, 而Card Table根据队列长度又被划分为了4个区域:
你可以把Card Table 想象成一个队列, 而这个队列被划分了4块,每一次的指针变更的操作被堆积在了这个队列中:
- 白色 : 什么事情都不发生
- 绿色 : 部分 GC Refinement Threads执行
- 黄色 : 全部 GC Refinement Threads执行
- 红色: 应用性能下降,部分资源用于处理该队列
现在,你可能会疑惑这个GC Refinement Threads的作用是什么, 这就要和上面介绍的Remembered Set一起讲解了, 当Refinement Threads进入Runnable状态时, 他们会像"消息队列中的消费者"一样对Card Table队列中指针变更记录进行处理, 目的是为了将指针变更的记录写入Remembered Set用于后续的Young GC(动词).
那么为啥不一开始就让应用程序写入Remembered Set呢?
这就是一个跟性能相关的话题了, 有点像我们在业务开发中用MQ做时间补偿,将数据放在一个队列中往往比直接写入remembered set廉价的多(注意! 这个行为占据了应用程序的线程而不是GC线程).随后通过GC Refinement Threads来进行后台异步写入,何乐而不为?
看到这你可能会好奇这个标记行为到底算同步? 可以说 — 应用程序同步地向Card Table投递指针变更相关数据,而 GC后台线程异步处理了这些数据.
Young GC (Stop The World)
小节A中的标记行为是在Young GC 启动前由应用程序与GC线程共同协作的,而该小节会根据小节A中的内容进一步讲解Young GC(名词)的行为:
上图是基于本人对Young GC理解做的流程图:
- Stop the World: 简单来讲就是Young GC停止了整个应用程序, 主要目的本人认为是为了解决并发带来的数据问题
- Root Scanning: 从static field(方法区中的静态成员属性) & Thread Variable(程序栈帧中的变量)等GC ROOT开始, 逐级扫描这些对象指向的对象
- Process Card Table & Remembered Set : 处理完Card Table堆积的数据, 写入Remembered Set(还记得前面说的Card Table在白色区域不被后台线程处理这件事嘛). 随后处理Remembered Set中的数据
- Object Copy(State of the Art) 根据上述两步进行对象移动,可以说是 Young GC 的精髓, 随后会对这个流程进一步讲解
- Reference Processing 根据引用类型(weak/soft/phantom,final…)对对象进行回收
需要知道的是,在Object Copy中GC能做很多很多事情(画工有点糟糕):
E: Eden
S: Survior
O: Old
H: Humongous
上图是Object Copy后各个regions的分布变化, 可以看出执行完Object Copy后一些Eden & Survior regions被清理腾空 : 无用的对象(没有GC ROOT标记, 且 RS中没有引用信息)被回收,有用的对象被移动整合
Object Copy 时GC能做远远不止上面这些:
- GC 能记录每个region移动花费的时间,用于动态调整Eden Regions的数量来“尊重”你的-XX:MaxGcPauseMillis参数(还记得上文的参数P2嘛)
- GC 能记录每种对象的类型(weak/soft/phantom,final…)用于下一步的Reference Processing
Old GC
Old GC的触发
在一次Young GC(动词)完成或者Humongous region 对象分配后整个堆内存占用超过45%时.
堆占用45%
这个参数可调整, Old GC的执行过程会有一段"时间较长"的并发标记(这次是真正的并发标记)阶段, 在这个阶段应用程序与GC线程并行且能正常分配内存空间, 如果这个参数调整的过高,就可能引发并发标记阶段时间内分配的内存空间超出堆内存能承担的空间总量(OOM)
A. 标记(Old GC期间)
与Young GC的标记不同, Old GC的标记阶段做到了真正的"与应用程序并发执行". 并发标记是如何实现的? 同时应用程序对内存空间的分配会不会对标记的准确性进行影响? 本文会一一叙述
A1. Tri-Color Marking
与CMS不同的是,G1采用Tri-Color Marking技术做到了并发标记:
上图是本人画的Tri-Color标记过程图(从上到下分为3个阶段), 本图假使GC线程在标记阶段全程占据了CPU时间分片. 在这个场景下, 图中最左边的对象被GC Root指向(关于old GC 中的 GC Root本文会随后讲解)
从最左边开始依次从黑->灰->白, 这个逐级变色的过程通过队列实现, GC线程会将"灰色的对象"放入队列中, 当队列中的对象被取出时会对它进行分析,在分析完毕后将该对象"变为黑色",随后将它指向的对象(白色)变为灰色放入队列中等待下一次分析
并发竞争与解决方案
GC线程与应用线程竞争
并发下可能会发生“对象遗失”的场景:
上图模拟了Tri-color在一次并发下线程抢占时间片引发的问题, 需要了解的是
Black -> Grey -> White允许成立, 但是 Tri-Color标记中绝不允许Black ->White(上图中的下半块).
上图中:
- 1.Grey对象被一个"alive对象"标记着
- 2.Grey对象被GC线程放入队列等待处理
- 3.应用程序线程"抢占先机"执行Grey.field = null(导致Grey对象不再指向a对象)
- 4.应用程序用另一个"alive对象"标记着a(White)对象
上述场景就会导致一个很糟糕的问题发生, 如果没有步骤4对JVM而言"一切合理正常",但是 步骤4的产生会发生:
- Grey 对象被分析后"变成Black"(不会被回收)
- 尽管有一个"不会被回收的对象"指向对象a,但是a"保持着白色(被回收)"
- GC 回收中对象a被回收
- a指向的地址被其他对象占用,当应用程序使用a时…JVM Crash!
解决方案
答案很简单 -> 假装Grey.field = null这一步骤没发生过就好
当应用程序执行完Grey.field = null后会有两种场景:
- 场景1: 对象a永远不再被指向
- 场景2: 对象a被其他"alive"对象指向(图中场景)
如果假使Grey.field = null这一步没发生过(实际上依旧用了上述write barrier技术, jvm在每次 xxx=null 执行的时候对这个行为记录了下来):
- 上述场景1中a对象会在下一次Old GC中被回收(没有对象指向它了)
- 上述场景2中a对象保全了自己的内存空间,可以在下一次GC的时候再对它进行指针分析.
B. Old GC 的"间歇性" STW
小节A中的标记行为是在Old GC 启动后由应用程序与GC线程共同协作的,而该小节会根据小节A中的内容进一步讲解Old GC(名词)的行为:
上图时本人基于Old GC的理解做的流程图, 可以看出与Young GC不同的是,一次Old GC伴随着应用程序的暂停与恢复, Old GC先尝试发起一次Young GC 用于内存整理 & Old GC Root定位(还记得之前讲的Object Copy阶段Young GC能做的事嘛).随后就跟图中描述的一样进行并发标记.
值得指出的是Old GC的Remarking阶段, GC线程分析了之前write barrier技术存储的指针删除记录,在本次回收阶段保留这些对象 ,随后在下一步骤清除被无用对象占满的Old Regions
本文对G1的垃圾回收机制进行了一次浅析, 如果有机会本人会在接下来的文章中用案例分析更复杂的场景与混合GC的使用.
巨问G1 G1 G1相关推荐
- 气密测试内螺纹快速密封接头 格雷希尔快速连接器 G1/8 G1/4 G3/8 G1/2 G3/4内螺纹封堵接头
格雷希尔GripSeal快速接头气密测试内螺纹式快速密封接头用于内螺纹部件的气密性测试或压力测试,瞬间连接,可显著提高测试过程的连接效率和可靠性,利用快速接头前端O型圈实现端面密封,对测试件端面的粗糙 ...
- JVM垃圾收集器——G1
导航 引言 一.G1 介绍 1.1 适用场景 1.2 设计初衷 1.3 关注焦点 1.4 工作模式 1.5 堆的逻辑结构 1.6 主要收集目标 1.7 停顿预测模型 1.8 拷贝和压缩 1.9 与 C ...
- 深入解析G1垃圾收集器与性能优化
本文详细介绍G1垃圾收集器的参数配置,如何进行性能调优, 以及怎样对GC性能进行分析和评估. 文章目录 0. G1简介 1. 垃圾回收阶段简介 2. 纯年轻代模式的垃圾收集 3. 混合模式的垃圾收集 ...
- java9 g1垃圾收集器_Java 9中默认为G1垃圾收集器的情况
java9 g1垃圾收集器 在前面的几篇文章中,我已经在InfoQ上介绍并讨论了"垃圾第一垃圾收集器" -G1:一个由所有垃圾收集器来统治它们以及调整垃圾第一垃圾收集器的技巧 . ...
- [JVM 相关] Java 新型垃圾回收器(Garbage First,G1)
回顾传统垃圾回收器 HotSpot 垃圾收集器实现 Serial Collector(串型收集器) 使用场景,大多数服务器是单核CPU. 适用收集场景:1. 新生代收集(Young Generatio ...
- GC之G1垃圾收集器
GC之G1垃圾收集器 目录 以前收集器的特点 G1是什么 G1特点 G1底层原理 G1回收步骤 和CMS相比的优势 小总结 1. 以前收集器的特点 年轻代和老年代是各自独立且连续的内存块 年轻代收集必 ...
- G1垃圾收集器深度剖析
G1垃圾收集器深度剖析 一.G1垃圾收集器概述 1.1 思考 开始学习前,抛出两个常见面试问题:1.G1的回收原理是什么?为什么G1比传统的GC回收性能好?2.为什么G1如此完美仍然会有ZGC?简单的 ...
- JVM优化系列-JVM G1 垃圾收集器
导语 G1回收器是在JDK1.7中正式使用的一种全新的垃圾回收器,它的目标是为了取代CMS回收器.G1回收器拥有独特的垃圾回收策略,和之前的任意的一种垃圾回收器都有所不同,但是从分代策略上来说依然 ...
- java g1的并行_Java 11好用吗
原标题:Java 11好用吗 开源规划调度引擎 OptaPlanner 官网发布了一个 Java 11 GC 性能基准测试报告. 当前使用量最大的 Java 版本是 8,所以测试者用 Java 8 与 ...
最新文章
- 基于TableStore的数据采集分析系统介绍
- JAVA常见算法题(三十一)---冒泡排序
- MeteoInfoLab脚本示例:多Y轴图
- POSIX 信号量和互斥锁
- Linux调试分析诊断利器——strace
- notepad 怎么配置编译c语言,Notepad++ 配置c/c++编译环境
- 校园卡系统mysql与java结合_基于Java+JSP+Mysq+Servletl的校园卡一卡通管理系统
- 20220915使用python3下载ts格式的视频切片文件
- 本科最高5w!毕业生落户指南!18城市人才引进补贴
- 出现HTTPS证书错误原因
- 对比Ruby和Python的垃圾回收
- 2018ICPC焦作站赛后总结
- SpringBoot的ResultFul增删改查
- XPS如何在线转Word格式
- 16bit的pcm双声道转单声道
- LintCode 644. 镜像数字 JavaScript算法
- 蜘蛛池的作用与工作原理(公羊优链蜘蛛池)
- android 当服务器
- ROOT(a Data analysis Framework)-Note6: iSTEP day3-Random
- python中列表的声明,查询,修改,删除 del 和添加 append,insert