G1GC 是什么?

G1GC(Garbage First Garbage Collection)是在 OpenJDK 7 中引入的 GC 算法,其最大的特点就是非常重视实时性

一些基本概念

实时性

程序具有实时性,是指程序必须能在最后期限(deadline)之前完成,其中最后期限可以自由指定。实时性分为两种:

  • 硬实时性(hard real-time):每次处理的时间都不能超过最后期限,比如医疗机器人控制系统、航空管制系统。
  • 软实时性(soft real-time):稍微超出几次最后期限也没有什么问题的系统,例如网络银行系统。

G1GC 具有软实时性,为了实现软实时性,必须具备以下功能:

  • 设置期望暂停时间(最后期限)
  • 可预测性:预测下次 GC 会导致应用程序暂停多长时间。根据预测出的结果,G1GC 会通过延迟执行 GC拆分 GC 目标对象等手段来遵守上面设置的期望暂停时间。

G1GC 有什么特点?

Java 中已经有很多种 GC 算法了,为什么还要增加 G1GC 算法呢?

  • 以往的 GC 都是尽可能缩短最大暂停时间,但是缩短最大暂停时间很容易导致吞吐量下降。
  • 以往的 GC 无法预测暂停时间,GC 时可能会使应用程序长时间暂停的风险。
  • G1GC 的目的就是高效地实现软实时性,能够让用户设置期望暂停时间。在确保吞吐量比以往的 GC 更好的前提下,实现了软实时性。
  • G1GC 能最大程度利用服务器上多处理器的优势,而且在处理巨大的堆时,也不会降低 GC 的性能。

G1GC 的堆结构是什么样的?

G1GC 堆的内部被划分为大小相等的区域,所有区域排成一排。G1GC 以区域为单位进行 GC。用户可以随意设置区域大小,但是内部会将用户设置的值向上调整为 2 的指数幂,并以该正数作为区域的大小(如下图)。

G1GC 的执行过程是什么样的?

  • 并发标记(concurrent marking):和应用程序并发执行,针对区域内所有的存活对象进行标记。
  • 转移(evacuation):释放堆中死亡对象所占的内存空间。

白色区域是空闲区域,灰色区域是使用中的区域。

  • 左图表示的是在选中区域后开始将存活对象复制到空闲区域的操作
  • 右图表示的是转移后堆的状态。

为了方便演示,图中的区域以二维的方式排列,但是在内存中其实如下图是排列成一排的。

并发标记

并发标记是什么

简单标记,所有可从根直接触达的对象都会被添加标记。带标记的是存活对象,不带标记的是死亡对象。

在并发标记中,存活对象的标记和应用程序几乎是并发进行的,步骤更加复杂。并发标记并不是直接在对象上添加标记,而是在标记位图上添加标记。

标记位图

下图表示堆中的一个区域,位图中黑色表示已标记,白色表示未标记。

每个区域有两个标记位图:

  • next:本次标记的标记位图。
  • prev:上次标记的标记位图,保存了上次标记的结果。

标记位图中的每个比特都对应关联区域内的对象的开头部分。图中区域部分:

  • bottom:区域内众多对象的末尾
  • top:区域中对象的开头
  • nextTAMS:本次标记开始时的 top(TAMS-Top At Marking Start)
  • prevTAMS:上次标记开始时的 top

执行步骤

  1. 初始标记阶段:暂停应用程序,标记可由根直接引用的对象。
  2. 并发标记阶段:与应用程序并发进行,扫描 1 中标记的对象所引用的对象。
  3. 最终标记阶段:暂停应用程序,扫描 2 中没有标记的对象。本步骤结束后,堆内所有存活对象都会被标记。
  4. 存活对象计数:对每个区域中被标记的对象进行计数,并发执行。
  5. 收尾工作:暂停应用程序,收尾工作,并为下次标记做准备。

步骤 1——初始标记阶段

在初始标记阶段,GC 线程首先创建标记位图 next。其中 nextTAMS 是标记开始时,top 所在的位置。位图的大小也和 top 对齐,是 (top-botton)/8 字节。

等所有区域的标记位图都创建完成后,标记由根直接引用的对象(根扫描)。此时是需要暂停应用程序的,这是为了防止扫描过程中根被修改。

如果一个对象本身被标记,但是子对象没有被扫描,我们称之为未扫描对象,上图用灰色标识,C 持有子对象 A 和 E,但是 A 和 E 并未被扫描。

步骤 2——并发标记阶段

在并发标记阶段,GC 线程扫描在 1 阶段标记过的对象,完成对大部分存活对象的标记。

上图表示并发标记结束的状态,对象 C 的子对象 A 和 E 都被标记了。E 对应了标记位图中多个位,只有起始的标记位(mark bit)会被涂成黑色。

因为并发标记是和应用程序并发执行的,所以在这个阶段可能会产生的对象,上图中 J 和 K 就是在并发标记期间新创建的对象,直接会被 GC 当成存活对象。

同时因为是并发执行,应用程序可能会改变了对象之间的引用关系,需要使用写屏障技术来记录对象间引用关系的变化。并发标记阶段也会标记和扫描被写屏障感知变化的对象。

STAB

STAB(Snapshot At The Beginning,初始快照)是将并发标记阶段开始时对象间的引用关系,以逻辑快照的形式保存起来。标记过程中新生成的对象是“已完成扫描和标记”的,其子对象不会被标记。那如何区分是标记过程中新生成的对象呢?初始标记阶段记录的 nextTAMS 和 当前 top 之间的对象,所以并不需要专门为新生成的对象创建标记位图。

还有个很重要的问题,在并发标记过程中,对象的域发生了写操作怎么办?此时必须以某种方式记录被改写之前的引用关系。

G1GC 使用SATB 专用写屏障。在一个对象的域发生写操作时,这个对象会被放入 SATB 本地队列(SATB 本地队列满后,会被添加到全局的 SATB 队列结合)。在并发标记阶段,GC 线程会定期检查 SATB 队列集合的大小,对队列中的全部对象进行标记和扫描。如果获取到已经被标记的对象,这些对象不会再次被标记和扫描。

步骤 3——最终标记阶段

主要扫描 SATB 本地队列(队里里仍然存放了待扫描对象)。因为 SATB 本地队列会被应用程序操作,所以需要暂停应用程序。

上图中 SATB 本地队列中还有对象 G 和 H 的引用,扫描后对象 G 和 H,以及对象 H 的子对象 I 都会变成黑色。

步骤 4——存活对象计数

扫描各个区域的标记位图 next,统计区域内存活对象的字节数,存到区域内的 next_marked_bytes 中。下图中存活对象 A、C、E、G、H 和 I,一共 6 个对象,其中 E 真实大小是 16 个字节,其余 5 个对象分别是 8 个字节,所以 next_marked_bytes 是 56 个字节。

在计数的过程中,又新创建了对象 L 和 M,nextTAMS 和 top 之间的对象都会被当做存活对象处理,没有特意进行计数。

步骤 5——收尾工作

收尾工作所操作的数据中有些是和应用程序共享的,所以需要暂停应用程序。

收尾阶段主要做了两件事情:

  • GC 线程逐个扫描每个区域,将标记位图 next 的并发标记结果移动到标记位图 prev 中,再重置标记,为下次并发做准备。
  • 在扫描过程中,计算每个区域的转移效率,并按照该效率对区域进行降序排序。

上图中 prevTAMS 被移动到了 nextTAMS 原来的位置,表示“上次并发标记开始时 top 的位置”。next.next_marked_bytes 也会被重置,同时 nextTAMS 移动到 bottom 的位置,其会在下次并发标记开始时,移动到 top 的最新位置。

转移效率

指转移 1 个字节所需的时间。通俗理解就是,区域内死亡对象越多,存活对象就越少;而存活对象越少,那么转移所需的时间就越少。

计算公式为:死亡对象的字节数 / 转移所需时间

并发标记总结

并发标记结束后,可以得到:

  • 并发标记完成时,存活对象和死亡对象的区分(此时在标记位图 prev)
  • 存活对象的字节数(prev_marked_bytes)

如果新的对象是在并发标记结束后被创建的,因为新对象是分配在 prevTAMS 和 top 之间的,所以后被当成存活对象处理。

转移

转移是什么?

将所选区域内的所有存活对象都转移到空闲区域,因此被转移区域就只剩下死亡对象。重置之后,该区域就会成为空闲区域。

转移专用记忆集合

上节介绍的SATB 队列集合是记录标记过程中对象之间引用关系的变化,这里的转移专用记忆集合记录区域间的引用关系,这样不用扫描所有区域的对象,也能查到待转移对象所占区域内的对象被其他区域引用的情况。

G1GC 是通过卡表(card table)来实现转移专用记忆集合的。

卡表

是元素大小为 1B 的数组,堆中大小适当的一段存储空间(通常是 512B)对应卡表中的 1 个元素。在堆大小是 1GB 时,卡表大小为 2MB。

堆中对象所对应的卡片在卡表的索引值 = (对象的地址 - 堆的头部地址) / 512

因为卡片的大小是 1B,所有可以表示很多状态,状态有很多,在后面只介绍两种:

  • 净卡片
  • 脏卡片

转移专用记忆集合的构造

每个区域都有一个转移专用记忆集合,是通过散列表实现的:

  • 键:引用本区域的其他区域的地址
  • 值:数组,数组元素是引用方的对象所对应的卡片索引

在上图中,区域 B 中的对象 b 引用了区域 A 中的对象 a。因为对象 b 不是区域 A 中的对象,所以必须记录这个引用关系。在转移记忆集合 A 中,以区域 B 的地址为键记录了卡片的索引 2048(对象 b 对应的卡片索引),此时对象 b 对对象 a 的引用被准确记录了下来。

转移专用写屏障

那 GC 是如何感知域的变化呢?是通过转移专用写屏障,当对象修改时,会被转移专用写屏障记录到转移专用记忆集合中。

每个应用程序线程都持有一个转移专用记忆集合日志的缓冲区,其中存放的是卡片索引的数组。当对象 b 的域被修改时,写屏障就会感知,并会将对象 b 所对应的卡片索引添加到转移专用记忆集合日志中。

转移专用记忆集合维护线程

是和应用程序并发执行的线程,是基于上述日志维护转移专用记忆集合。主要步骤:

  • 从转移专用记忆集合日志的集合中取出转移专用记忆集合日志,从头开始扫描
  • 将卡片变为净卡片
  • 检查卡片所对应存储空间内的所有对象的域
  • 向域中地址所指向的区域的记忆集合中添加卡片

热卡片

频繁发生修改的存储空间所对应的卡片就是热卡片。热卡片可能会多次进入转移专用记忆集合日志,被多次处理成脏卡片,增加维护线程的负担。

可以通过卡片计数器,发现热卡片,当某个卡片变成脏卡片的次数超过阈值,可以等到转移的时候再处理。

转移的执行步骤

  • 选择回收集合:参考并发标记提供的信息,选择要转移的区域。
  • 根转移:将回收集合内由根直接引用的对象,及被其他区域引用的对象转移到空闲区域中。
  • 转移:以根转移的对象为起点,扫描子孙对象,将所有存活对象一并转移。此时回收集合内所有存活对象都转移完成了。

步骤 1——选择回收集合

选择待回收区域的标准:

  • 转移效率要高
  • 转移的预测停顿时间在用户的容忍范围内

在并发标记阶段结束时,堆中区域已经按照转移效率降序了。这里就是按照排好的顺序依次计算各个区域内的预测暂停时间,当所有已选区域预测的暂停时间和快要超过用户的容忍范围时,后续区域的选择就会停止,当前所选的区域就是 1 个回收集合。

步骤 2——根转移

根转移的对象包括:

  • 由根直接引用的对象
  • 并发标记处理中的对象
  • 由其他区域对象直接引用的回收集合内的对象

  1. 对象 a 转移到空闲区域。
  2. 对象 a 在空闲区域中的新地址写入到转移前所在区域中的旧位置。
  3. 将对象 a 引用的所有位于回收集合内的对象,都添加到转移队列中。转移队列临时保存待转移对象的引用方。因为对象 a 引用了对象 b,两个都是要转移的对象,地址都会变化。
  4. 针对对象 a 引用的位于回收集合外的对象,更新转移专用记忆集合。对象 c 所在区域不在回收集合内,但是区域 C 的转移专用记忆集合记录了 a 对应的卡片,在 a 转移之后,需要更新区域 C 的转移专用记忆集合。
  5. 针对对象 a 的引用方,更新转移专用记忆集合。

步骤 3——转移

完成根转移后,被转移队列引用的对象会依次转移。当转移队列清空后,转移就完成了。此时回收集合内所有存活对象都转移完成了。

分代 G1GC 模式

G1GC 有 2 中模式:

  • 纯 G1GC 模式:pure garbage-first mode
  • 分代 G1GC 模式:generational garbage-first mode

本文上面讲的都是纯 G1GC 模式。

两种 GC 的区别

和纯 G1GC 模式相比,分代 G1GC 模式主要有以下两个不同点。

  • 区域是分代的
  • 回收集合的选择是分代的

在分代 G1GC 模式中,区域被分为新生代区域老年代区域两类。 和其他分代 GC 算法一样,分代 G1GC 的对象也保存了自身在各次转移中存活下来的次数。新生代区域用来存放新生代对象,老年代区域用来存放老年代对象。

G1GC 中新生代 GC 是完全新生代 GC,老年代 GC 是部分新生代 GC。二者区别在于完全新生代 GC 将所有新生代区域选入回收集合,而部分新生代 GC 将所有新生代区域,以及一部分老年代区域选入回收集合。

新生代区域

新生代区域可以进一步分为两类:

  • 创建区域:存放刚刚生成,一次也没有转移过的对象
  • 存活区域:存放至少转移过一次的对象

转移专用写屏障不会应用在新生代区域的对象上。为什么这样做是可以的呢?因为转移专用记忆集合维护的是区域之间的引用关系,所以在转移时不用扫描整个区域就能找到待转移对象所在区域的存活对象。而在分代 G1GC 模式中,所有新生代区域都会被选入回收集合,所有对象的引用都会被检查,这些信息就没有记录在转移专用记忆集合中了。

分代对象转移

存活对象保存了自己被转移的次数,这个次数就是对象的年龄

  • 年龄<阈值:转移到存活区域
  • 年龄>=阈值:转移到老年代区域

执行过程

如上图,完全新生代 GC 不会选择老年代区域,而是将所有新生代区域都选入回收集合,然后统一转移回收集合的对象。晋升的对象会被转移到老年代区域,其余的转移到存活区域。

如上图,部分新生代 GC 除了所有新生代区域外,还会选择一些老年代区域进入回收集合。其余都和完全新生代 GC 一样。

GC 的切换

如果新生代的区域数太多,可能导致 GC 暂停时间上限的增加,无法保证软实时性。分代 G1GC 模式需要计算出合理的最大新生代区域。该值的设置是在并发标记结束后。

参考并发标记中标记出的死亡对象个数,预测出下次部分新 生代 GC 的转移效率。然后,根据过去的完全新生代 GC 的转移效率, 预测出下次完全新生代 GC 的转移效率。如果预测出完全新生代 GC 的 转移效率更高,则切换为完全新生代 GC。

GC 的执行时机

当新生代区域数达到上限时,会触发转移的执行。,当转移完成并通过以下 4 项检查,会执行并发标记:

  • 不在并发标记执行过程中
  • 并发标记的结果已被上次转移使用完
  • 已经使用了一定量的堆内存
  • 相比上次转移完成后,堆内存的使用量有所增加

G1 算法总结

关系图

图中并列的箭头表示可能会并行执行。

优点

  • 软实时性
  • 充分发挥高配置机器的性能,缩减 GC 暂停时间
  • 区域内不会产生内存碎片

缺点

  • 被限定为“搭载多核处理器、拥有大容量内存的机器”,适用受限。
  • 尽管区域内不会出现碎片化,但是会出现以区域为单位(整个堆)的碎片化。

参考

《深入 Java 虚拟机-JVM G1GC 的算法与实现》

GitHub LeetCode 项目

项目 GitHub LeetCode 全解,欢迎大家 star、fork、merge,共同打造最全 LeetCode 题解!

Java 编程思想-最全思维导图-GitHub 下载链接,需要的小伙伴可以自取~!!!

原创不易,希望大家转载时请先联系我,并标注原文链接。

JVM G1GC的算法与实现的更多相关文章

  1. JVM垃圾回收算法解析

    JVM垃圾回收算法解析 标记-清除算法 该算法为最基础的算法.它分为标记和清除两个阶段,首先标记出需要回收的对象,在标记结束后,统一回收.该算法存在两个问题:一是效率问题,标记和清除过程效率都不太高, ...

  2. JVM垃圾回收算法(最全)

    JVM垃圾回收算法(最全) 下面是JVM虚拟机运行时的内存模型: 1.方法区 Perm(永久代.非堆) 2.虚拟机栈 3.本地方法栈 (Native方法) 4.堆 5.程序计数器 1 首先的问题是:j ...

  3. JVM垃圾回收算法及回收器详解

    引言 本文主要讲述JVM中几种常见的垃圾回收算法和相关的垃圾回收器,以及常见的和GC相关的性能调优参数. GC Roots 我们先来了解一下在Java中是如何判断一个对象的生死的,有些语言比如Pyth ...

  4. 七年开发经验详解JVM的GC 算法

    概述 GC 是 JVM 自带的功能,它能够自动回收对象,清理内存,这是 Java 语言的一大优势,但是GC绝不仅伴随着Java,相反,GC历史比Java更悠久.关于GC,我认为有四个问题需要解决: 为 ...

  5. JVM 垃圾回收算法和垃圾回收器

    JVM 垃圾回收算法和垃圾回收器. 一.垃圾回收的区域 栈:栈中的生命周期是跟随线程,所以一般不需要关注. 堆:堆中的对象是垃圾回收的重点. 方法区:这一块也会发生垃圾回收,不过这块的效率比较低,一般 ...

  6. 面试刷题25:jvm的垃圾收集算法?

    垃圾收集是java语言的亮点,大大提高了开发人员的效率. 垃圾收集即GC,当内存不足的时候触发,不同的jvm版本算法和机制都有差别. 我是李福春,我在准备面试,今天的问题是: jvm的垃圾回收算法有哪 ...

  7. 记录JVM垃圾回收算法

    垃圾回收算法可以分为三类,都基于标记-清除(复制)算法: Serial算法(单线程) 并行算法 并发算法 JVM会根据机器的硬件配置对每个内存代选择适合的回收算法,比如,如果机器多于1个核,会对年轻代 ...

  8. JVM 垃圾回收算法

    在说垃圾回收算法之前,先谈谈JVM怎样确定哪些对象是“垃圾”. 1.引用计数器算法: 引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当 ...

  9. JVM学习九:JVM之GC算法和种类

    我们前面说到了JVM的常用的配置参数,其中就涉及了GC相关的知识,趁热打铁,我们今天就学习下GC的算法有哪些,种类又有哪些,让我们进一步的认识GC这个神奇的东西,帮助我们解决了C 一直挺头疼的内存回收 ...

随机推荐

  1. hud 3308 LCIS 线段树 区间合并

    题意: Q a b 查询[a, b]区间的最长连续递增子序列的长度 U a b 将下表为a的元素更新为b 区间合并一般都有3个数组:区间最值,左区间最值和右区间最值 具体详见代码 #include & ...

  2. buu 不一样的flag

    一.查壳 二.拖入ida,分析 从这里和51到53行的代码,基本判断这是一个迷宫题,并且是5行5列的一个迷宫.我当时感觉到一个奇怪的地方是 第一个,我自己想明白是因为可能是int型,数字占了4个字节, ...

  3. s3cmd的使用

    目录 1. 安装s3cmd 2. 配置s3cmd 3. 使用s3cmd [前言] s3cmd 是用于创建S3桶,上传,检索和管理数据到对象存储命令行实用程序. 本文将指导linux下安装s3cmd程序 ...

  4. C语言:编程求任意月份的天数

    闰年问题,因为二月份的天数与闰年有关.闰年的判断依据是:若某年能被4整除,但不能被100整除,则这一年是闰年:若某年能被400整除,则这一年也是闰年 #include <stdio.h> ...

  5. C#中使用jieba.NET、WordCloudSharp制作词云图

    目录 词云简介 准备工作 基本算法 算法实现 运行测试 参考资料 词云简介 "词云"由美国西北大学新闻学副教授.新媒体专业主任里奇·戈登(Rich Gordon)于2006年最先使 ...

  6. PAT甲级:1036 Boys vs Girls (25分)

    PAT甲级:1036 Boys vs Girls (25分) 题干 This time you are asked to tell the difference between the lowest ...

  7. 使用bind部署DNS主从服务器

    说明:这里是Linux服务综合搭建文章的一部分,本文可以作为单独搭建主从DNS服务器的参考. 注意:这里所有的标题都是根据主要的文章(Linux基础服务搭建综合)的顺序来做的. 如果需要查看相关软件版 ...

  8. 第二篇 -- Django写一个接口并用Jmeter进行测试

    第一节学习了Jmeter的下载和安装,那么第二节就来看看具体怎么使用. 本篇介绍的是使用Jmeter进行http接口测试,那么接口程序使用Django开发的一个小接口. 一.Django编写接口 这一 ...

  9. 基于单机redis的分布式锁实现

    最近我们有个服务经常出现存储的数据出现重复,首先上一个系统流程图: 用户通过http请求可以通知任务中心结束掉自己发送的任务,这时候任务中心会通过MQ通知结束服务去结束任务保存数据,由于任务结束数据计 ...

  10. Verilog定点数计算

    https://blog.csdn.net/maxwell2ic/article/details/80620991 https://blog.csdn.net/mdpsdhr/article/deta ...