我是靠谱客的博主 称心手机,最近开发中收集的这篇文章主要介绍JVM面试(六)-G1垃圾收集器G1内存模型G1分代模型G1分区模型CSet-G1收集集合G1活动周期阅读参考,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

G1垃圾收集器

  • G1内存模型
    • Region-分区
    • Card-卡片
  • G1分代模型
  • G1分区模型
    • Humongous Region-巨形对象分区
    • Remembered Set(RSet)-已记忆集合
    • RSet(Remembered Set)的维护
      • Barrier-栅栏(屏障)
      • Pre-Write Barrrier-写前栅栏(屏障)
      • Post-Write Barrrier-写后栅栏(屏障)
      • SATB-原始(起始)快照算法(重要!!!)
      • 为什么G1采用SATB而不用Incremental Update(增量更新)?
      • Concurrence Refinement Threads-并发优化线程(重要!!!)
    • Per Region Table (PRT)
  • CSet-G1收集集合
    • CSet of Young Collection-年轻代收集集合
    • CSet of Mixed Collection-混合收集集合
  • G1活动周期
    • G1垃圾收集活动汇总
    • Concurrent Marking Cycle-并发标记周期(重要!!!)
      • Concurrent Marking Threads-并发标记线程
      • Initial Mark-初始标记
      • Root Region Scanning-根分区扫描
      • Concurrent Marking-并发标记
      • Live Data Accounting-存活数据计算
      • Remark-重新标记
      • Cleanup -清除(就是筛选回收阶段)
      • 小结
    • 年轻代收集(Young GC)/混合收集周期(Mixed GC)
      • GC工作线程数
      • Young GC-年轻代收集
        • 并行活动
        • 串行活动
      • Young Collection Following Concurrent Marking Cycle-并发标记周期后的年轻代收集
      • Mixed Collection Cycle-混合收集周期
      • 转移失败的担保机制 Full GC
  • 阅读参考

G1内存模型

在这里插入图片描述

Region-分区

  • G1将 整个堆空间 分成 若干个 大小相等 的 内存区域-Region(物理上可以是不连续的),每次分配对象空间将逐段地使用内存 。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可
  • 每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换
  • 启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区
  • G1保留了分代的概念,Region分为:Eden Region、Survivor Region、Old Region、Humongous Region,但是年轻代和年老代不再是物理上的隔离,他们都是一部分的Region的集合

Card-卡片

  • G1 将Region 划分为 相等大小的一个个区域,这个小的区域大小是512 Byte,称为Card,也就是Region包含Card。假设一个Region是1M,则包含1000多个Card(Byte和M中间还有一个KB),还有空间要给下面的RSet
  • 全局卡片表-Card Table维护着所有的Card,Card Table的结构 是一个 字节(byte)数组,Card Table 用这个数组 映射着 每一个Card
  • Card中(的)对象 的 引用 发生改变时,Card 在 Card Table数组中 对应的值 被标记为dirty,就称这个Card被脏化了
  • Card Table其实就是映射着内存中的对象,Young GC的时候只需要扫描状态是dirty的Card
  • 分配的对象 会占用 物理上连续若干个Card,当 查找 对 分区(Region)内(的)对象 的 引用时 便可通过 卡片 来查找到 该引用对象。每次对内存的回收,都是对指定分区的卡片进行处理
  • 查找一个对象所在的Card只需要应用公式便可得出:Card的Index = (对象的地址 – 堆开始地址) ÷ 512CardIndex = (对象的地址–堆开始地址) ÷ 512

G1分代模型

在这里插入图片描述
分代垃圾收集 可以将关注点 集中在 最近被分配的对象上,而无需整堆扫描,避免长生命周期对象的拷贝,同时独立收集有助于降低响应时间
虽然分区使得内存分配不再要求紧凑的内存空间,但G1依然使用了分代的思想

与其他垃圾收集器类似,G1 将内存 在逻辑上 划分为 年轻代和老年代,其中年轻代又划分为Eden空间和Survivor空间
但年轻代空间并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲分区加入到年轻代空间

整个年轻代内存会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间-XX:G1MaxNewSizePercent(默认60%)之间动态变化,且由参数 目标暂停时间-XX:MaxGCPauseMillis(默认200ms)、需要扩缩容的大小 以及 分区的已记忆集合(RSet)计算得到

当然,G1依然可以设置固定的年轻代大小(参数-XX:NewRatio、-Xmn),但同时暂停目标将失去意义

G1分区模型

在这里插入图片描述
G1 对内存的使用 以 分区(Region)为单位,而对 对象的分配 则以 卡片(Card)为单位

Humongous Region-巨形对象分区

一个 大小达到 甚至超过 分区大小一半 的 对象 称为 巨型对象(Humongous Object)
当线程为巨型分配空间时,不能简单在TLAB进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象
因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region)

G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可 直接在 年轻代收集周期中 被回收

巨型对象会独占一个、或多个连续分区,其中第一个分区被标记为开始巨型(StartsHumongous),相邻连续分区被标记为连续巨型(ContinuesHumongous)

由于无法享受TLAB带来的优化,并且确定一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高,如果可以,应用程序应避免生成巨型对象

设置H(Humongous Region)区的原因 是因为 对于堆中的大对象,默认会被分配到老年代,但如果它是一个短期存在的对象,由于老年代垃圾收集的频率较低,这个对象是不能及时被回收掉的,会对垃圾收集造成负面的影响

设置了H(Humongous Region)区,就能够及时回收,如果一个H区装不下一个大对象,则寻找连续的H区来存储,如果找不到连续的H区,就会启动Full GC

Remembered Set(RSet)-已记忆集合

在这里插入图片描述

G1将堆区划分成多个Region,一个Region不可能是独立的,它其中存储的对象 可能被 其他任意Region(这些Region可能Old区或者Eden区)中的对象所引用

这样一来,在进行YGC的时候,判断Eden区中的一个对象是否存活时,需要去扫描所有的Region(包括Old区,Eden区等),导致了在回收年轻代的时候,还需要扫描老年代,同时扫描表示所有Eden区和Old区的Region,相当于做了一个全堆扫描,这会大大降低YGC的效率,为了解决这个的问题,提出了Remembered Set的概念

  • Remembered Set: RSet,每一个Region都有自己的RSet,RSet里面记录了 引用:其他Region中 指向 本Region中(的)对象 的 所有引用,也就是 谁 引用了 我这个Region里的对象

  • RSet其实是一个Hash Table,Key是 其他的Region 的起始地址,Value是一个集合,Value里面的元素是 Card Table数组中的Index,既 对应着某个Card,映射到对象的Card地址

    比如A对象在RegionA,B对象在RegionB,且B.f = A,也就是A被B引用了,则在RegionA的RSet中需要记录一对键值对,Key是RegionB的起始地址,Value的值 能映射到 B所在的 RegionB 中 Card的地址,所以要查找B对象,就可以通过RSet中记录的 卡片 来查找该对象,见上图

  • G1进行GC时,只要扫描 本Region中 RSet,来确定 引用本分区内的对象 是否存活,进而确定 本分区内的对象(的)存活情况

  • 本分区对象 引用 本分区自己的对象,这种引用不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此年轻代->(引用)老年代 和 年轻代->(引用)年轻代 也不需要在RSet中记录。而对于 老年代->年轻代 的 跨代对象引用 和 老年代->老年代,需要拥有RSet

  • 对于属于年轻代的分区(Eden和Survivor区的Region)来说,RS只保存 来自 老年代的对象 的 引用。这是因为 年轻代回收 是针对 全部年轻代的对象的,反正 所有年轻代内部 的 对象引用关系 都会被扫描,所以RS不需要保存来自年轻代内部的引用

  • 对于属于老年代分区来说,也只会保存来自老年代的引用,这是因为 在老年代进行回收之前 会先进行 年轻代的回收,年轻代回收后Eden区变空了,G1会在 老年代回收过程中 扫描 Survivor区 到 老年代的引用

RSet(Remembered Set)的维护

由于不能整堆扫描,又需要计算 分区确切的活跃度,因此G1需要一个 增量式的 完全标记并发算法,通过维护RSet,得到准确的分区引用信息
在G1中,RSet的维护主要来源两个方面:写栅栏(Write Barrier) 和 并发优化线程(Concurrence Refinement Threads)

Barrier-栅栏(屏障)

在这里插入图片描述
这是从代码角度看的Barrier示意图

栅栏是指 在原生代码片段中,当某些语句被执行时,栅栏代码也会被执行
G1主要在赋值语句中,使用 写前栅栏(Pre-Write Barrrier) 和 写后栅栏(Post-Write Barrrier)
事实上,写栅栏的指令序列开销非常昂贵,应用吞吐量也会根据栅栏复杂度而降低

Pre-Write Barrrier-写前栅栏(屏障)

即将执行一段赋值语句时,等式左侧对象 将修改引用 到 另一个对象,那么 等式左侧对象 原先引用的对象 所在分区(Region) 将因此 丧失一个引用,那么JVM就需要 在赋值语句生效之前,记录 丧失引用 的 对象
JVM并不会立即维护RSet,而是通过批量处理,在将来RSet更新

Post-Write Barrrier-写后栅栏(屏障)

当执行一段赋值语句后,等式右侧对象获取了左侧对象的引用,那么等式右侧对象所在分区(Region)的RSet也应该得到更新
同样为了降低开销,写后栅栏发生后,RSet也不会立即更新,同样只是记录此次更新日志,在将来 批量处理

SATB-原始(起始)快照算法(重要!!!)

原始快照算法(SATB),主要针对 标记-清除垃圾收集器 的 并发标记阶段(第二个阶段),非常适合G1的分区块的堆结构,同时解决了CMS的主要烦恼:重新标记阶段(第三个阶段) 暂停时间长带来的潜在风险

SATB会创建一个对象图,相当于 堆的逻辑快照,从而确保 并发标记阶段 所有的垃圾对象 都能通过快照 被鉴别出来
当赋值语句发生时,应用 将会改变了 它的对象图,JVM 需要记录 被覆盖的对象(丧失掉引用 的 对象),因此 写前栅栏(屏障) 会在 引用变更前,将值 记录在 SATB日志或缓冲区中

每个线程都会独占一个SATB缓冲区,初始有256条记录空间
当空间用尽时,线程会分配新的SATB缓冲区继续使用,而原有的缓冲去则加入全局列表中

最终 在并发标记阶段,并发标记线程(Concurrent Marking Threads,下面会有相关内容) 在 标记(对象)的同时,还会定期检查和处理 全局缓冲区列表的记录,然后根据 标记位图 分片(Region) 的 标记位,扫描引用字段 来更新RSet

此过程又称为并发标记/SATB写前栅栏

为什么G1采用SATB而不用Incremental Update(增量更新)?

因为采用Incremental Update把黑色重新标记为灰色后,之前扫描过的还要再扫描一遍,效率太低
G1有RSet与SATB相配合,Region里记录了RSet,RSet里记录了 其他对象 指向自己的引用,这样就不需要再扫描其他区域,只要扫描RSet就可以了

也就是说 灰色–>白色 引用消失时,如果没有 黑色–>白色,引用会被push到引用栈,下次扫描时拿到这个引用,由于有RSet的存在,不需要扫描整个堆 去查找 指向白色的引用,效率比较高

Concurrence Refinement Threads-并发优化线程(重要!!!)

并发优化线程 异步维护 RSet

G1中使用基于Urs Hölzle的快速写栅栏,将栅栏开销缩减到2个额外的指令
栅栏将会更新一个card table(卡片表)的结构 来跟踪 代间引用

当赋值语句发生后,写后栅栏 会先通过 G1的过滤技术判断 是否是 跨分区的引用更新,并将 跨分区更新对象的卡片(Card) 加入缓冲区序列,即 更新日志缓冲区 或 脏卡片队列

与SATB类似,一旦日志缓冲区用尽,则分配一个新的日志缓冲区,并将原来的缓冲区加入全局列表中

并发优化线程(Concurrence Refinement Threads),只专注扫描 日志缓冲区记录的卡片 来维护更新RSet,线程最大数目可通过-XX:G1ConcRefinementThreads(默认等于-XX:ParellelGCThreads)设置

并发优化线程永远是活跃的,一旦发现全局列表有记录存在,就开始并发处理
如果记录增长很快或者来不及处理,那么通过阈值-X:G1ConcRefinementGreenZone、-XX:G1ConcRefinementYellowZone、-XX:G1ConcRefinementRedZone,G1会用分层的方式调度,使更多的线程处理全局列表

如果并发优化线程也不能跟上缓冲区数量,则Mutator线程(Java应用线程)会挂起应用 并被加进来 帮助处理,直到全部处理完
因此,必须避免此类场景出现

Per Region Table (PRT)

RSet 在内部 使用Per Region Table(PRT) 记录 分区的 引用情况
由于RSet的记录要占用分区的空间,如果一个分区非常"受欢迎",那么RSet占用的空间会上升,从而降低分区的可用空间

G1应对这个问题采用了改变RSet的密度的方式,在PRT中将会以三种模式记录引用

  • 稀疏粒度:记录 引用对象 的 卡片索引(Card的Index)
  • 细粒度:记录 引用对象 的 分区索引,也就是说,map中的value是一个位图,位图的最大位数 代表一个Region 最多能被拆分为多少Card,位图上值为1 则代表 Region上Card内 有对象 引用了 RSet所在Region的对象
  • 粗粒度:只记录引用情况,每个分区对应一个比特位。采用位图实现,位图的最大位数 代表整个Heap 能被拆分为多少个Region,位图上值为1 则代表 其他Region内 有对象 引用了 RSet所在Region的对象。因为Region的大小是一样的,可以通过Heap的起始地址,计算出位图中每个Region的起始地址

由上可知,粗粒度的PRT 只是记录了 引用数量,需要通过整堆扫描才能找出所有引用,因此扫描速度也是最慢的

CSet-G1收集集合

在这里插入图片描述
收集集合(CSet) 代表 每次GC暂停时 回收的 一系列目标分区(Region)
在任意一次收集暂停中,CSet中所有分区(Region)都会被释放,内部存活的对象 都会被转移到 分配的空闲分区(Region)中

因此无论是年轻代收集,还是混合收集(下面的并发标记相关内容中有这两种收集),工作的机制都是一致的

年轻代收集CSet只容纳年轻代分区
而混合收集 会通过 启发式算法,在老年代候选回收分区中,筛选出 回收收益最高的分区 添加到CSet中

候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象

同时,每次混合收集 可以包含候选老年代分区(的数量),可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置 数量上限

由上述可知,G1的收集都是根据CSet进行操作的,年轻代收集与混合收集没有明显的不同,最大的区别在于两种收集的触发条件

CSet of Young Collection-年轻代收集集合

应用线程不断活动后,年轻代空间会被逐渐填满
当JVM 分配对象 到 Eden区域失败(Eden区已满)时,便会触发一次STW式的年轻代收集(下面提到的Young GC)

在年轻代收集中,Eden分区存活的对象将被拷贝到Survivor分区;原有Survivor分区存活的对象,将根据 任期阈值-XX:MaxTenuringThreshold分别晋升到PLAB(晋升本地缓冲区)中,新的Survivor分区 和 老年代分区
而原有的年轻代分区将被整体回收掉

同时,年轻代收集 还负责维护 对象的年龄(存活次数),辅助判断老化(tenuring)对象晋升的时候 是到 Survivor分区 还是到 老年代分区

年轻代收集 首先 先将晋升对象尺寸总和(所有对象大小总和)、对象年龄信息 维护到 年龄表中
再根据 年龄表、Survivor尺寸、Survivor填充容量-XX:TargetSurvivorRatio(默认50%)、最大任期阈值-XX:MaxTenuringThreshold(默认15),计算出一个恰当的任期阈值,凡是超过任期阈值的对象都会被晋升到老年代

CSet of Mixed Collection-混合收集集合

年轻代收集不断活动后,老年代的空间也会被逐渐填充
当老年代占用空间超过整堆比IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会启动一次混合垃圾收集周期

为了满足暂停目标,G1可能不能一口气将所有的候选分区收集掉,因此G1可能会产生连续多次的混合收集与应用线程交替执行,每次STW的混合收集与年轻代收集过程相类似

为了确定 包含到 年轻代收集集合CSet 的 老年代分区,JVM通过参数 混合周期的最大总次数-XX:G1MixedGCCountTarget(默认8)、堆废物百分比-XX:G1HeapWastePercent(默认5%) 进行处理

通过 候选老年代分区总数 与 混合周期最大总次数-XX:G1MixedGCCountTarget,确定 每次包含到CSet 的 最小分区数量
根据 堆废物百分比-XX:G1HeapWastePercent,当收集达到参数值时,不再启动新的混合收集
而每次添加到CSet的分区,则通过计算得到的GC效率进行安排

G1活动周期

G1垃圾收集活动汇总

在这里插入图片描述

按下面的流程顺序整理一下注解:

  • 横轴 Elapsed time:已用时间
  • App activities:应用活跃时间
  • Concurrence Refinement:并发优化线程
  • IHOP Trigger:IHOP触发器
  • Young Collection:(普通的)年轻代收集
  • Young Collection with Initial Mark:带有初始标记的 年轻代收集
  • Concurret Markting Cycle:并发标记周期
  • Concurret Mark:并发标记线程
  • Remark:重新标记线程
  • Clean:清理线程
  • Mixed Collection Cycle:混合收集周期
  • Mixed Collection:混合收集线程

Concurrent Marking Cycle-并发标记周期(重要!!!)

并发标记周期是G1中非常重要的阶段,这个阶段 将会为 混合收集周期 识别 垃圾最多的老年代分区(Region)
整个周期 完成 根标记、识别所有(可能)存活对象,并计算每个分区(Region)的活跃度,从而确定GC效率等级

当达到IHOP阈值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默认45%)时,便会触发 并发标记周期

整个并发标记周期 将由 初始标记(Initial Mark)、根分区扫描(Root Region Scanning)、并发标记(Concurrent Marking)、重新标记(Remark)、清除(Cleanup) 几个阶段组成

其中,初始标记((Initial Mark,随 年轻代收集 Young GC 一起活动)、重新标记(Remark)、清除(Cleanup) 都是是STW的
而并发标记(Concurrent Marking) 如果来不及标记存活对象,则可能(是因为)在并发标记过程中,G1又触发了几次年轻代收集(Young GC)

Concurrent Marking Threads-并发标记线程

下面这个是 并发标记 位图(的)过程
在这里插入图片描述
先说一下
Bottom 和 Top 之间是一个 Region 已使用的部分
Top 到 End 之前是一个 Region 未使用的部分

要标记存活的对象,每个分区(Region)都需要创建 位图(Bitmap)信息 来存储 标记数据,来确定 标记周期内 被分配的对象

G1采用了两个位图Previous Bitmap、Next Bitmap,来存储标记数据

  • Previous Bitmap 是并发标记阶段完成后的最后一个位置,存储 上次的标记数据
  • Next Bitmap 是当前 将要 或 正在 进行并发标记的结构, 在并发标记周期内 不断变化更新

当标记周期结束后 Next位图 便替换 Previous位图,成为上次标记的位图

每个分区(Region) 通过 顶部开始标记(TAMS,这是一个名词,Top at Mark Start),来记录 已标记过的 内存范围
同样的,G1使用两个 顶部开始标记:Previous TAMS(PTAMS)、Next TAMS(NTAMS),记录 已标记的范围

在并发标记阶段,G1会根据参数-XX:ConcGCThreads(默认GC线程数的1/4,即-XX:ParallelGCThreads/4),分配并发标记线程数(Concurrent Marking Threads),进行标记活动
每个并发线程一次只扫描一个分区,并通过"手指"指针的方式优化获取分区

并发标记线程是爆发式的,在给定的时间段拼命干活,然后休息一段时间,再拼命干活

下面这几点是对于上面图的解释说明:

  • 每个并发标记周期,(是)在初始标记(第一个阶段)STW的最后,G1会分配一个空的Next Bitmap 和 一个指向分区顶部(Top)的Next TAMS(NTAMS)标记
  • Previous Bitmap记录上次标记数据
  • 上次的标记位置,即Previous TAMS(PTAMS),在Previous TAMS(PTAMS) 与 分区底部(Bottom)的范围内,所有的存活对象都已被标记
  • 在 Previous TAMS(PTAMS) 与 Top 之间的对象 都将是 隐式存活(Implicitly Live)对象
  • 在并发标记阶段,Next Bitmap 吸收了 Previous Bitmap的标记数据,同时每个分区都会有新的对象分配,则 Top 与 Next TAMS(NTAMS) 分离,前往更高的地址空间

在并发标记的一次标记(构成)中,并发标记线程 将找出 NTAMS与PTAMS之间的所有存活对象,将 标记数据 存储在 Next Bitmap中。同时,在 NTAMS 与 Top之间的对象 即成为 已标记对象
如此不断地更新Next Bitmap信息,并在 清除阶段 与 Previous位图 交换角色

Initial Mark-初始标记

这就是四个阶段中的第一个阶段

初始标记(Initial Mark) 负责标记 所有 能被直接可达 的 根对象(原生栈对象、全局对象、JNI对象)
根 是 对象图 的 起点,因此 初始标记 需要 将Mutator线程(Java应用线程)暂停掉,也就是需要一个STW的时间段

事实上,当达到IHOP阈值时,G1并不会立即发起并发标记周期,而是等待下一次年轻代收集(Young GC),利用年轻代收集的STW时间段,完成初始标记,这种方式称为借道(Piggybacking)

在初始标记暂停(STW)中,分区(Region)的NTAMS 都被设置到 分区顶部Top,初始标记是并发执行,直到所有的分区处理完

回顾一下之前整理的,下面逐句解释:
初始标记 仅仅只是 标记一下GC Roots能直接关联到的对象
并且修改TAMS(Top at Mark Start)指针的值
让下一阶段 用户线程 并发运行时,能正确地 在可用的Region中 分配 新对象

在这里插入图片描述
从上图可以看到 初始标记阶段 Next Bitmap 是清空状态,没有标记任何存活的对象

  • GC Roots 能直接关联到的对象:就是一个 Region 已经使用过的部分,所以在Bottom与Top之间
  • 修改 TAMS 的值:就是让此时的 Previous TAMS(PTAMS) 指向 Bottom ,也就是一个Region内存地址起始值。让此时的 Next TAMS(NTAMS) 指向 Top。Top实际上就是一个Region未分配区域和已分配区域的分界点
  • 可用的Region :对一个Region来说,当上面的 Next Bitmap 为空、2个指针都准备就绪后,这个Region在下一阶段用户程序并发运行时,就是一个正确的Region
  • 下一阶段用户程序并发运行时,在正确的可用的Region中创建新对象是什么意思?下一阶段用户程序并发运行时 指的就是 并发标记阶段

Root Region Scanning-根分区扫描

在初始标记暂停(STW)结束后,年轻代收集也完成了对象复制到Survivor的工作,应用线程开始活跃起来

此时为了保证标记算法的正确性,所有新复制到Survivor分区的对象,都需要被扫描并标记成根,这个过程称为 根分区扫描(Root Region Scanning),同时扫描的Suvivor分区 也被称为 根分区(Root Region)

根分区扫描 必须在 下一次年轻代垃圾收集(Young GC)启动前完成(并发标记的过程中,可能会被若干次年轻代垃圾收集打断),因为每次GC会产生新的存活对象集合

Concurrent Marking-并发标记

和应用线程并发执行,并发标记线程 在 并发标记阶段启动,由参数-XX:ConcGCThreads(默认GC线程数的1/4,即-XX:ParallelGCThreads/4)控制启动数量
每个线程每次只扫描一个分区,从而标记出 存活对象图

在这一阶段会处理Previous Bitmap、Next Bitmap,扫描 标记对象 的 引用字段
同时,并发标记线程 还会定期检查和处理 STAB全局缓冲区列表的记录,更新对象引用信息

参数-XX:+ClassUnloadingWithConcurrentMark会开启一个优化,如果一个类不可达(不是对象不可达),则在重新标记阶段,这个类就会被直接卸载

所有的标记任务 必须在堆满前 就完成扫描,如果并发标记耗时很长,那么有可能在并发标记过程中,又经历了几次年轻代收集。如果堆满前 没有完成 (并发)标记任务,则会触发担保机制,经历一次长时间的串行Full GC

从 GC Roots 开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象 是什么意思?
就是说 在并发标记阶段, GC线程 工作在 Previous TAMS(PTAMS) 和 Next TAMS(NTAMS) 之间,对堆里的对象进行可达性分析(三色标记法),标记完成后, Next Bitmap就有对应有值了(里面放的是地址值),黑色对应的是存活对象,白色对应的垃圾对象,实际标记过程 就是 有一个指针 从PTAMS指针位置 一直移到 NTAMS指针位置
这样就找出存活对象了。

Next TAMS(NTAMS) 与 Top 之间的对象,就是本次并发标记阶段 用户线程 新分配的对象,它们是隐式存活的
**Next TAMS(NTAMS) 与 Top 为什么是重叠的?**并发标记的前一个阶段是初始标记,由于初始标记是 STW 的,所以从上图可以看到:并发标记开始的时候,即初始标记结束的时候, 所以 Next TAMS(NTAMS) 与 Top 是重叠的

随着并发标记过程的进行,Next Bitmap 被填充上了值。而 Next TAMS(NTAMS) 与 Top 之间的区域越来越大,Top越来越往右移动,这就是用户线程在并发标记阶段分配的新对象

在这里插入图片描述
通过上面的图可以看到, GC线程的工作区间 和 用户线程的工作区间是有重叠的,而重叠的部分,就是可能产生“对象消失”的部分,对G1来说,(这部分)就是原始快照(STAB)加 写前屏障(Pre-Wirte Barrier)工作的部分

很显然 GC线程 只会去处理 [PTAMS, NTAMS) 区间 完成标记工作
而应用线程运行则会对[Bottom, Top)区间有影响

应用线程对[Bottom, Top)区间中的 [NTAMS, Top)区间的影响 并不会影响 GC线程的并发标记工作
因为 该部分应用线程 新增的对象 都认为是存活的对象

应用线程对[Bottom, Top)区间中的 [PTAMS, NTAMS)区间的影响 可能会影响 GC线程的并发标记工作
G1通过 写前屏障 来确保标记的正确性
即如果应用线程在[PTAMS, NTAMS)区间内增加了 黑色对象对白色对象的引用,写前屏障内部处理时 会将 白色对象 设置为 灰色对象,使得该对象能 再次被标记时 不会产生漏标

应用线程对[Bottom, Top)区间中[Bottom, PTAMS)区间的影响 可能会影响 GC线程的并发标记工作,猜测应该也是利用写屏障来处理

Live Data Accounting-存活数据计算

存活数据计算(Live Data Accounting) 是 标记操作 的 附加产物,只要一个对象被标记,同时会被计算字节数,并计入分区空间

只有Next TAMS(NTAMS)以下的对象会被标记和计算
在标记周期的最后,Next Bitmap将被清空,等待下次标记周期

Remark-重新标记

重新标记(Remark) 是最后一个标记阶段
在该阶段中,G1需要一个暂停的时间(SWT),去处理 剩下的SATB日志缓冲区和所有更新,找出 所有未被访问的 存活对象,同时安全完成 存活数据计算

这个阶段也是并行(多个GC线程)执行的,通过参数-XX:ParallelGCThreads可设置STW时可用的GC线程数

同时,引用处理 也是重新标记阶段的一部分,所有重度使用引用对象(弱引用、软引用、虚引用、最终引用)的应用 都会在 引用处理上产生开销

在这里插入图片描述
提醒:Next Bitmap是本次并发标记过程中处理的结果,黑色的是存活的,白色的是垃圾的

处理 并发阶段结束后 仍遗留下来的 最后那少量的 SATB 记录 是什么意思?
并发标记阶段, GC线程完成对象图的扫描之后,还会去处理 SATB记录下的 在并发时 有引用变动的对象
在处理 SATB记录的数据的时候,由于 用户线程 可能还是在 继续修改对象图,继续在产生SATB数据,所以还是会有一小部分的 SATB数据,所以才需要一个短暂的暂停

Cleanup -清除(就是筛选回收阶段)

紧挨着 重新标记阶段 的 清除(Clean)阶段 也是STW的
Previous Bitmap、Next Bitmap、以及 Previous TAMS(PTAMS) 和 Next TAMS(NTAMS),都会在清除阶段 交换角色

这个阶段有点像mark-sweep中的sweep阶段
不过不是在堆上sweep实际对象
而是在 marking bitmap 里 统计每个Region 被标记为活的对象 有多少
这个阶段 如果发现 完全没有活对象的Region就会将其整体 回收到 可分配Region列表中

清除阶段主要执行以下操作:

  • RSet梳理,启发式算法会根据活跃度 和 RSet尺寸 对分区 定义不同等级,同时RSet数理也有助于发现无用的引用。参数-XX:+PrintAdaptiveSizePolicy可以开启打印启发式算法决策细节
  • 整理堆分区,为混合收集周期 识别 回收收益高(基于 释放空间 和 暂停目标) 的 老年代分区集合
  • 识别所有空闲分区,即发现无存活对象的分区。该分区可在清除阶段直接回收,无需等待下次收集周期

小结

结合以上的流程,希望能对下面的图更好的理解
在这里插入图片描述
在这里插入图片描述

年轻代收集(Young GC)/混合收集周期(Mixed GC)

年轻代收集和混合收集周期,是G1回收空间的主要活动
当应用运行开始时,堆内存可用空间还比较大,只会在年轻代满时,触发年轻代收集
随着老年代内存增长,当到达IHOP阈值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默认45%)时,G1开始着手准备收集老年代空间
首先经历 并发标记周期,识别出 高收益的老年代分区
但随后G1并不会马上开始一次混合收集,而是让应用线程先运行一段时间,等待触发一次年轻代收集
在这次STW中,G1将保准整理混合收集周期
接着再次让应用线程运行,当接下来的几次年轻代收集时,将会有老年代分区 加入到 CSet中,即触发混合收集,这些连续多次的混合收集 称为 混合收集周期(Mixed Collection Cycle)

GC工作线程数

GC工作线程数 -XX:ParallelGCThreads,默认值并不是固定的,而是根据当前的CPU资源进行计算
如果用户没有指定,且CPU小于等于8,则默认与CPU核数相等
若CPU大于8,则默认JVM会经过计算得到一个小于CPU核数的线程数
当然也可以人工指定与CPU核数相等

Young GC-年轻代收集

同分代垃圾回收器一样,当G1中没有Eden Region能够容纳新要创建的对象时,G1中Young GC被触发
同时每个线程都有自己对应的TLAB,小的对象优先直接在TLAB中创建
G1的Young GC阶段 只会回收 全部的Young Region,Eden Region 与 Survivor Region

同时 如果 老年代内存占比超过了指定的阈值时,Young GC 会一同完成 并发标阶段 的 初始化标记 工作,就是上面的内容
每次Young GC后,G1会根据当前新生代大小、新生代最小值、新生代最大值、目标暂停时间等重新调整新生代的大小

每次收集过程中,既有并行执行的活动,也有串行执行的活动,但都可以是多线程的

并行活动

  • 外部根分区扫描-Ext Root Scanning,此活动对 堆外的根(JVM系统目录、VM数据结构、JNI线程句柄、硬件寄存器、全局变量、线程对栈根)进行扫描,发现那些没有加入到暂停收集集合CSet中的对象。如果系统目录(单根)拥有大量加载的类,最终可能其他并行活动结束后,该活动依然没有结束而带来的等待时间
  • 更新已记忆集合-Update RS:并发优化线程 会对脏卡片的分区 进行扫描 更新日志缓冲区 来更新RSet,但只会处理全局缓冲列表。作为补充,所有被记录但是还没有被优化线程处理的剩余缓冲区,会在该阶段处理,变成已处理缓冲区(Processed Buffers)。为了限制花在更新RSet的时间,可以设置暂停占用百分比-XX:G1RSetUpdatingPauseTimePercent(默认10%,即-XX:MaxGCPauseMills/10)。值得注意的是,如果更新日志缓冲区更新的任务不降低,单纯地减少RSet的更新时间,会导致 暂停中被处理的缓冲区 减少,将 日志缓冲区更新工作 推到 并发优化线程上,从而增加 对Java应用线程资源的争夺
  • RSet扫描-Scan RS:在收集当前CSet之前,考虑到分区外的引用,必须扫描CSet分区的RSet。如果RSet发生粗化,则会增加RSet的扫描时间。开启诊断模式-XX:UnlockDiagnosticVMOptions后,通过参数-XX:+G1SummarizeRSetStats可以确定 并发优化线程是否能够及时处理 更新日志缓冲区,并提供更多的信息,来帮助为RSet粗化总数提供窗口。参数-XX:G1SummarizeRSetStatsPeriod=n可设置RSet的统计周期,即经历多少此GC后进行一次统计
  • 代码根扫描-Code Root Scanning:对代码根集合进行扫描,扫描JVM编译后代码Native Method(方法区)的引用信息(nmethod扫描),进行RSet扫描。事实上,只有CSet分区中的RSet有强代码根时,才会做nmethod扫描,查找对CSet的引用
  • 转移和回收-Object Copy:通过选定的CSet 以及 CSet分区完整的引用集,将执行暂停时间的主要花费的动作:CSet分区 存活对象的转移、CSet分区空间的回收。通过工作窃取机制来负载均衡地选定复制对象的线程,并且复制和扫描对象被转移的存活对象将拷贝到每个GC线程分配缓冲区GCLAB。G1会通过计算,预测分区复制所花费的时间,从而调整年轻代的尺寸
  • 终止-Termination:完成上述任务后,如果任务队列已空,则工作线程会发起终止要求。如果还有其他线程继续工作,空闲的线程会通过工作窃取机制尝试帮助其他线程处理。而单独执行根分区扫描的线程,如果任务过重,最终会晚于终止
  • GC外部的并行活动 GC Worker Other:该部分并非GC的活动,而是JVM的活动导致占用了GC暂停时间(例如JNI编译)

串行活动

  • 代码根更新 Code Root Fixup:根据转移对象更新代码根
  • 代码根清理 Code Root Purge:清理代码根集合表
  • 清除全局卡片标记 Clear CT:在任意收集周期会扫描CSet与RSet记录的PRT,扫描时 会在全局卡片表中 进行标记,防止重复扫描。在收集周期的最后 将会清除 全局卡片表中的已扫描标志
  • 选择下次收集集合 Choose CSet:该部分主要用于 并发标记周期后 的 年轻代收集、以及混合收集中,在这些收集过程中,由于有老年代候选分区的加入,往往需要对下次收集的范围做出界定;但单纯的年轻代收集中,所有收集的分区都会被收集,不存在选择
  • 引用处理 Ref Proc:主要针对软引用、弱引用、虚引用、final引用、JNI引用。当Ref Proc占用时间过多时,可选择使用参数-XX:ParallelRefProcEnabled激活多线程引用处理。G1希望应用能小心使用软引用,因为软引用会一直占据内存空间直到空间耗尽时被Full GC回收掉;即使未发生Full GC,软引用对内存的占用,也会导致GC次数的增加
  • 引用排队 Ref Enq:此项活动可能会导致RSet的更新,此时会通过记录日志,将关联的卡片标记为脏卡片
  • 卡片重新脏化 Redirty Cards:重新脏化卡片
  • 回收空闲巨型分区 Humongous Reclaim:G1做了一个优化:通过查看所有根对象 以及 年轻代分区的RSet,如果确定RSet中巨型对象没有任何引用,则说明G1发现了一个不可达的巨型对象,该对象分区会被回收
  • 释放分区 Free CSet:回收CSet分区的所有空间,并加入到空闲分区中
  • 其他活动 Other:GC中可能还会经历其他耗时很小的活动,如修复JNI句柄等

Young Collection Following Concurrent Marking Cycle-并发标记周期后的年轻代收集

当G1发起并发标记周期之后,并不会马上开始混合收集
G1会先等待下一次年轻代收集,然后在该收集阶段中,确定下次混合收集的CSet(Choose CSet)

Mixed Collection Cycle-混合收集周期

单次的混合收集 与 年轻代收集并无二致
根据暂停目标,老年代的分区 可能不能(在)一次暂停收集中(都)被处理完,G1会发起连续多次的混合收集,称为混合收集周期(Mixed Collection Cycle)

G1会计算 每次加入到CSet中的分区数量、混合收集进行次数
并且 在上次的年轻代收集、以及接下来的 混合收集中,G1会确定 下次加入CSet的分区集(Choose CSet),并且确定 是否结束混合收集周期

转移失败的担保机制 Full GC

转移失败(Evacuation Failure) 是指 当G1无法在堆空间中申请新的分区时,G1便会触发担保机制,执行一次STW式的、单线程的Full GC
Full GC会对整堆 做标记清除和压缩,最后将只包含纯粹的存活对象
参数-XX:G1ReservePercent(默认10%)可以保留空间,来应对晋升模式下的异常情况,最大占用整堆50%,更大也无意义

G1在以下场景中会触发Full GC,同时会在日志中记录to-space-exhausted以及Evacuation Failure

  • 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
  • 从老年代分区转移存活对象时,无法找到可用的空闲分区
  • 分配巨型对象时在老年代无法找到足够的连续分区

由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生

阅读参考

  • 记忆集(remembered set)和写屏障(write barrier)
  • Getting Started with the G1 Garbage Collector
  • G1回收器:我怎么知道你是什么时候的垃圾? 这篇文章好
  • 深入分析G1垃圾收集实现原理 这篇文章好
  • G1垃圾回收的详细过程 -了解 这篇文章好
  • G1垃圾收集器中的region、G1垃圾收集器的主要回收环节概述

最后

以上就是称心手机为你收集整理的JVM面试(六)-G1垃圾收集器G1内存模型G1分代模型G1分区模型CSet-G1收集集合G1活动周期阅读参考的全部内容,希望文章能够帮你解决JVM面试(六)-G1垃圾收集器G1内存模型G1分代模型G1分区模型CSet-G1收集集合G1活动周期阅读参考所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(56)

评论列表共有 0 条评论

立即
投稿
返回
顶部