巨问G1 G1 G1

由来

因为"某些原因"需要对JAVA的垃圾回收机制进行研究, 有趣的是JAVA这门语言因自动内存回收快速兴起, 时下对一名互联网开发者的"评分标准"却往往必须包含精通垃圾回收原理这一项(人人精通JVM原理, 轻松改变世界), 网络上关于垃圾回收的博文良莠不齐, 很难分辨到底是一篇好的博文还是东抄西凑的大道理杂烩.

最近看了不少关于G1的知识, 带来了不少收获的同时也带来了大量的疑问,写这篇博客的目的是希望尽可能地用易懂的方式揭开G1的面纱, 同时对G1中的核心知识点进行总结

本文仅代表个人理解, 如果有任何疑惑或者见解欢迎大佬们指出

问:G1与堆内存?

软件开发中, 一个新技术的出现往往伴随一篇有新想法的文章和一个想要实现这篇文章的团队,G1的出现也经历了这样的过程:

  • 2004年发版第一篇文章
  • 2012年第一次在JDK 7u4落地
  • 作为JDK 9 的默认垃圾回收机制

G1做为一款"低中断"的垃圾回收器,诞生的目的是为了长久取代CMS(另一款垃圾回收器,有趣的是最近看到G1和CMS的相关知识在某些地方被糅合在了一起)

如果你从不了解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

你可以把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

上图是基于本人对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 Marking

上图是本人画的Tri-Color标记过程图(从上到下分为3个阶段), 本图假使GC线程在标记阶段全程占据了CPU时间分片. 在这个场景下, 图中最左边的对象被GC Root指向(关于old GC 中的 GC Root本文会随后讲解)
从最左边开始依次从黑->灰->白, 这个逐级变色的过程通过队列实现, GC线程会将"灰色的对象"放入队列中, 当队列中的对象被取出时会对它进行分析,在分析完毕后将该对象"变为黑色",随后将它指向的对象(白色)变为灰色放入队列中等待下一次分析

并发竞争与解决方案
GC线程与应用线程竞争

并发下可能会发生“对象遗失”的场景:
Tri-Color Marking

上图模拟了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的使用.

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值