本文是对 6.S081 课程中 VM applications 论文阅读中对虚拟内存能应用于 Garbage Collection 的笔记。主要记录垃圾回收的一些算法和解释论文的应用点。内容是从最简单的 Copying GC (DFS) 到 cheney 的 Copying  GC,然后讲解 Baker‘s real time incremental GC 到一个基于 Baker 的 Concurrent GC最后分析 VM 怎么赋能 Concurrent GC。本文可以作为增强对虚拟内存的理解和进一步学习 GC 的并发并行的材料。

涉及的论文阅读有:

  • List Processing in Real Time on a Serial Computer Henry G. Baker, Jr. MIT (Baker 算法)
  • Real-time Concurrent Collection on Stock Multiprocessors Andrew W. Appel (改进 Baker)
  • virtual memory primitives for user programs, Andrew W.Appel and Kai Li, 1991 (VM 在 改进 Baker 算法 的应用)

Copying GC

复制 GC 的方法是把内存划分为两个分区,一个叫 to 一个叫 from, 程序运行的时候只使用 to 区, 当 GC 发生的时候,我们先把两个分区反转(此时 Stop the world,程序不再运行,而是收集器在运行)还有用的对象从 from (原来的 to 翻转后)区复制到 to 区,结果就是新的 to 区会是整齐的区域,而垃圾就只留在 from 区了。要完成复制 GC 需要解决的问题有各个对象之间的引用问题。我们使用转发的方法实现对象移动后引用的修改。现在应该尝试在脑海里想一下怎么解决这个问题。

基本要点

首先是 GC root, 在GC中我们只需要管理 heap 上的 objects, 所以栈是不用 collector (进行垃圾回收的程序部分)来负责的而是 mutator (使用内存的程序部分) 进行函数调用的时候自己负责的. 所以我们可以把栈和寄存器上的对象作为 GC root.

第二个要点是分区和转发的概念, 分区是为了实现 mark 和 compaction, the algorithm manage 2 regions in mem, from-space and to-space, everytime a GC happens, it copy the objects in from-space to to-space and forward the pointer.  分区实现旧的空间(杂乱包含有用数据和垃圾)以及新的经过整理的搬家后的区. 转发是实现依赖关系转接的一种方法.

第三个要点是, 所有的涉及要更改的指针都只会在 GC root 下. 所以不涉及直接的指令如何修改指针的问题. 举例说明:尽管是在 C 语言, 我们也不需要去修改指令的指针, 因为我们实际访问必须是通过 C 语言层面上的 root->obj_1 这种意义去访问的, stop the world 返回之后, 也必须要返回到 root->obj_1 这里开始, 而不是汇编层面上的 “load [一个立即数指针值] ”. 而GC返回后 root->obj_1 已经被我们修改过了。也就是说堆的访问必须是至少通过一次访存的间接引用. 这也就解释了 copying GC 如何修正指针. (注意这种情况下程序员不应该直接操作指针, 诸如运算指针然后解引用的操作都禁止.)

然后讲解转发的某种实现, 我们在管理内存的时候因为按 obj 来划分, 当然有一种方案就算每个 obj 都搞一个头也就是成员字段来指定他是不是一个转发, 然后供将来的修正用. Java HotSpot VM里就是这样用一个 _mark  字段来存储转发信息. (具体的转发过程下面讲)

然后是遍历图行为的正确性依据: 根对象可以访问所有对象,一个对象可以访问他的所有成员对象。

复制和转发

搬家过程图解(课程截图),从 GC root 出发,先复制 GC root,然后依次读取 root 下的所有的引用指向的对象,依次复制他们(DFS)

然后看转发的具体实现,转发的含义是把 obj 拷贝到 to space, 然后更新 from 中的内存, 把他修改为一个转发到 to 的指针内容. 如图, 我们 C 发送到 to space 之后, 再拷贝 B 的时候发现 B 有一个指向 to space 中内容的指针, 我们直接访问那个地方进行一次解引用再翻译, B 指向 C 的 from 指针也更新为指向 to space 的新 copy of C 了. 这里有一个问题是如何判断 B 中的指针是指向 to space 的还是 from space 的, 我们可以采取下面讲解循环引用中的 copy 函数返回更新的方法, copy 函数会去重入读取 from 里面的数据, 并解析那里的 forwarding 域然后返回, 涉及一次读取判断。

我们具体实现的时候只需要写一个 DFS 就行了。这里涉及一个循环引用,下面研究一下循环引用的时候算法的正确性 (以DFS版本说明) , 具体的实现我们使用 dfs 来遍历图, 每复制一个对象就先完成转发声明, 然后递归去遍历自己的所有引用, 复制和转发他们. 一种方法是采用可重入的 copy 函数, 如下图.

这样就能写出这样的伪代码,这里的 copy 就是 dfs 的部分,我们记得传进来的这个 p 是在 from 区的就能理解这个代码了,修改 from 区里面的这个对象的转发指针,如果将来有对象的引用进入到这里来的时候他会直接将转发的指针更新到新的指针而不是再复制一遍。

//copying gc
copying_gc(){free = to_space;for(p in roots){*p = copy(*p);p++;}
}//copy
copy(void* p){if(p->copied){return p->forwarding;}copy_data(free, p);for(child in childs){*child = copy(*child)}p->forwarding = free;free += p->size;return p->forwarding;
}

当然我们都知道 DFS 效率不佳,DFS 递归无法不占用栈内存地优化为迭代,所以需要提出 BFS 的 copying GC 算法。这一系列的论文里面讲解的 copying gc (stop the world) 实际上都是讲的 cheney 版, 即迭代 BFS 复制的版本. 下图是论文里讲解 Cheney 算法的图解,我们只要明确这个分区扫描的含义就是一个队列的思路而已,这一点在早期的编程练习里面已经练习足够多了。这里不再讲解。

Cheney 算法提出迭代优化的 graph tracing 的方法, 隐式地划分 scanned/unscanned 区域, 具体是一个三染色的 bfs, 我们上面说过的 dfs 遍历存在重入的过程, 也就是一共有三个状态:遍历到但是等待完成, 遍历完成(可以 free), 未遍历, 所以是对应三染色.

这里的 scan 指针本质就是队列的锚而已, BFS的队列方法就是每次队列出队然后把子加入队列. 这里的队列就是 scanned 区域. 如果读到这里不熟悉队列使用的话可以去复习二叉树层次遍历。

举个例子, Root ->a, b; a->c,d; b->e, f; 则一开始复制 a,b, 然后 scan a 再复制 c,d, 然后 scan b 再复制 e,f. 如果有循环引还是用重入copy解决的, 并且能完成翻译转发地址的操作. scan 指针的好处就是节省了 GC 内存, 不需要开栈也不需要额外的队列模拟.

BFS/DFS选择问题,实际上用 BFS 会导致一些后果,涉及 Cache 优化(可以见博客另一篇分析 Cache 抖动文章)。 BFS 会损失 locality, 这一点可以想出来的, 本来 a 会引用 c d, 结果 c d 排到 a b c 隔了一个 d , 数据集更大一点之后局部性损失更厉害. 并不是一个 Cache friendly 的算法. 所以还要继续改进, 又提出近似的 DFS, 即不用栈实现近似的 DFS. 基本思路是在 Cache 页大小作为页大小来划分 from/to 区, 实现页内BFS(提前BFS=>近似DFS), 大概就是加多一个指针和划分页面, 具体的不赘述了 (因为实际上可能不用做这个优化, 现在的程序跑得很多数据也很快,损失部分局部性也可以通过其他优化填充空闲时间。只是对Cache做太多优化没必要),不过可以留做编程练习.

Baker 的实时 GC 算法

基本要点

大概是实现某种渐进上的 copying 算法, 具体过程是, 每次发生一个原来的 full gc 事件, 就把 root 复制, 但是不进行 dfs 递归复制操作. 有两种行为导致新的转发发生.

  • 每次发生 new 时候, 我们已经在复用 to space 和 from space 了, 但是注意的是我们不能访问 from space 中的指针. 为了加速完成全部的 forwarding, 每次 new 的时候从 from space 里面复制与转发更多的节点.
  • 每次访问一个指向 from space 内存的指针时, 我们每次都要检查指针所在的空间, 如果试图访问一个 from space 内的内存空间(dereference), 我们陷入 trap 并完成更多复制与转发.

直到一个检查点时我们就可以交换 from 和 to 两个空间. 检查点的到达可以在某个时候尝试 copy a few more obj 的时候发现, 由于 incremental copying 也是遵循某种 dfs 策略来进行复制转发的, 只不过具体是剪了大部分枝. 如果某次尝试复制却没有得复制了就说明已经完成了工作, 具体实现还是有很大灵活性的.

  • baker 算法的缺点是需要硬件的支持(检查指针的过程耗费时间),  mutator 和 collector 不能并发执行, 因为 mutator 在访问 from 指针引发复制转发的时候 collector 可能在做 gc 工作. 也不可能加锁限制性能.

细节探究

我们继续讲解实际的 baker 算法的实现, 先是纯的 baker 版本, 又有很多要点. 我们仍然基于 copying 的 cheney 版的 scan 来实现 baker 的 incremental copying.

首先第一点是递进GC的终止点, 我们有 scan 指针, 基于 copying gc 的分析很明显只需要判断 scan==unscan 就行了, prof frans 的 toy 实现里没有隔离 unused 和 new, end, 我们页按这个来讲解, 即我们总是 allocate 新的对象到 上图的 unscanned 的下方(即原 unused 部分).

第二点是 GC 的状态, 整个过程中 GC 域应该存在 3 个状态, 一个是初始状态, 一个是 to 和 from 共用的状态, 一个是准备 flip 的状态. baker 算法根本就不清理垃圾, 他总是让垃圾和过期对象一直存在, 时不时地覆写他们. 下面分析各个状态既 GC 的生命周期.

首先是初始状态, 之后的生命周期里都不会出现这样的干净, 这是只有 to space 在使用, from 是空的. 每次 new 被调用就紧缩地在空间里连续分配一个空间.

直到某个时间节点, to space 里的内存空间耗尽了, 此时我们到达第一个准备 flip 的状态, from 还是空的, 这里发生翻转, 把 to 和 flip 空间互换, 此时需要完成把根节点转发到新的 to space 里. 这时根节点旗下的引用都会依然指向 from 空间内部. 我们把根节点这里声称为 unscanned. 我们直接返回. (这是实时性的体现, 如果是 cheney, 此时需要 STWorld). 即我们甚至没有进行转发就直接返回了.

我们考虑理想情况, 之后 mutator 再也没有访问之前的数据, 我们再 new 多几次后, scan 就会发现 scan 完了, 此时结束 collecting , 到达 checkpoint, 这一轮GC就结束了. 下面具体研究真实情况.

根节点下的引用仍然指向 from space, 此时根据约定, 我们认为 from space 里都是垃圾, 不应该去访问 from space, 所以此时 mutator 的访问都需要进行一层包装. mutator 通过根节点(可能是某个栈上的变量)去访问一个 obj. 我们具体看 6.s081 课程中 prof. frans 写的 toy 是怎么体现这一层包装的:

   struct elem *e;e = new();//…… a long time ……-> gc_flip_point;    ->ret; ->readptr(&e)->val = xxxstruct elem * b = new();

e 是栈上的变量, 所以 e 是根节点能够跟踪并修改的一个引用. 为了在 c 语言里面实现具备引用语言的仿真, 我们每次访问 e 这个引用指向的 object 的时候, 我们都通过 e 在栈上的地址去访问, 并且我们可以修改 e 的值.

此时 e 在根节点下, 我们想要访问 e 引用指向的对象, 引发了一个问题, 我们会在 readptr 包装层里发现 e 是一个指向 from space 的引用, 我们此时把 e 指向的内存转发出来. 这就是 mutator 层面的访问导致的转发.

如果此时发生了 new 行为, 每次 new 的时候我们也要发起一个转发过程, 我们 scan 多个对象, 具体可以用页来衡量, 我们直接 scan 一页的对象, 转发他们引用的对象. 这个例子里, 我们将会去转发根节点的引用的对象.

剩下的细节就不研究了. 我们得出结论, 这样的 incremental copying 仅仅是把单次 STW 的代价分摊到了每次的 new 里面去而已, 而且比 STW 还增加了很多的引用判断, 导致总体性能下降, 仅仅是提升了实时性.

然后是并行性能分析, 我们发现了资源争用的情况, 在这个例子里我们需要维护GC隐式队列结构的不变性(所以 cheney 实际也没有并行性能, 不过他本来就是 STW 的不需要并行), 但是 incremental 不能单次完成, 所以需要有并行的 collector 进程在执行才能保证一定的速度, 不然只能增加周期性的迷你 STW 来补充速度. 我们可以添加一个 collector 在后台运行, 不过这样做就需要添加锁来实现同步以维护GC队列不变性. [5] paper(vm paper ref [5]) 里讲这样引入 fine grained locking, 影响性能.

另一个并行问题是采用了这样的 GC 之后, 并行程序也无法运行了, 多个 mutator 在争用一个 GC 结构(并发产生). 当然这种很容易通过 GC 结构 分 bucket 解决(碎片++).

下面就讲回 vm applications 论文里的 concurrent gc.

Concurrent GC

下面讲解的内容主要基于论文:[Real-time Concurrent Collection on Stock Multiprocessors - Andrew W. Appel],他作为 vm 的一种引用出现在课程本节读的论文[virtual memory primitives for user programs, Andrew W.Appel and Kai Li, 1991]中. (论文作者是虎书作者)。图片来自 6.s081 课程视频。需要掌握上面提到的 cheney copying gc 和 baker 算法的思路。

第一点是 using trap to forward those pointers pointed to from-space to to-space. 每次 trap 我们只 copy root, protect unscaned。之后我们真正使用内存才去完成剩下对象的复制移动。

先讲解怎么解决包装的判断指针问题, 首先要明确我们这里 protect 的是 to space 的 unscanned 部分, unscanned  的内容的意思是其作为一个对象他自己内含指向 from space 的指针. 我们保护这个对象, 使他无法被访问. 回忆 baker 讲解的, 一开始我们 forward 成功的是根对象, 也就是说, 当前的 mutator 无法访问任何已经在堆上的内存了, 他首先企图通过根对象去访问一个对象, 这会导致他在访问根对象的时候就 page fault了, 这时候 signal_handler 必须把整一个页面内的对象引用到所有仍然在 to space 中的对象都 forward 到 to space 上来, 并且修改根对象中的引用, 这样恢复到访问根对象的时候, 再访问根对象中的成员引用, 就都已经是转发成功的(指针)了. 此时再访问那些对象又会引发新的 trap.

trap copy more: add object to new area, unprotect scanned pages

trap is used for forwarding on demand, map2 is used for mutator and collector to get different permission (of unscanned pages)。这里 trap 和 map 2 是论文为虚拟内存总结的原语, trap 已经在操作系统和组成原理里面学习多次很熟悉了。map2 意思是把一个 page map 为两种不同的访问权限,或者map给两个不同的进程,也是操作系统里很熟悉的概念。

虚拟内存的作用

现在来进行这个 vm 赋能 concurent gc 的一些性能分析。

论文讲解了 trap 和保护的工作原理, 还有并发的工作原理, 前面提到并发需要引入 fine grained 的锁操作, paper 讲到这里通过引入 medium-grained 来实现并发, 我读了论文发现他不过是在 trap 和 collector 线程里面加上对 GC 队列的访问锁而已. 那不还是 fine grained 的锁吗, 这一点我还是没能理解. 可能 trap 比包装判断快的原因是  trap 的判断本来就是 CPU 要做的吧, 而且硬件实现更快. 锁的部分根据 paper 给的数据, 的确总体的花费会更贵.

事实上 incremental GC 可以应用于多种 GC 方案里, 这里是改进了 baker 的低效率的方案, 但是要注意 incremental gc 的缺点是降低了整体的 Throughput, 不过这个是预先得到的. 2018 Unity 某个测试版本提供了 incremental gc 的选项也是基于他们原来某个 STW 的一种方案做的(非compact和非分代, 没有copying 快很多). 游戏这种需要稳定速度和实时性的对这个需求的确更大. 普通的应用的算法还有很多很多种, 要具体根据需求选择. chrome 的的 v8 用的是分代年轻copy, 全局的用增量. 这也是基于年轻代的 copying 导致的 stop the world 不是很严重.

学到这里已经可以参考prof frans 的那个 baker 来实现一个编程练习造玩具了。

其他

vm 论文还讲解了 vm 能帮助分代 gc 的应用。由于我没有具体学习分代 gc 的实现流程,这里备注一下一些要点,主要是涉及分代 gc 里面要对不同代对象的性质年龄持续跟踪的问题。

  • younger records -> die soon
  • older -> reuse soon/live long
  • so GC algorithm like copying have to treat different generation differently
    • traverse objects and examine if they are referenced comsume time
    • maybe it can use serveral linkedlist to track diff gen, and use some strategy to alter the gen of obj
  • make younger -- point to --> older
  • maintain a gc list
  • generation flip
  • if a older record points to a younger one, it should be treated as younger record, set the older to ronly, use trap to handle the flip case

并发增量复制垃圾回收 (Concurrent and Incremental Copying GC) 和虚拟内存应用相关推荐

  1. GC算法-增量式垃圾回收

    概述 增量式垃圾回收也并不是一个新的回收算法, 而是结合之前算法的一种新的思路. 之前说的各种垃圾回收, 都需要暂停程序, 执行GC, 这就导致在GC执行期间, 程序得不到执行. 因此出现了增量式垃圾 ...

  2. 简介三种垃圾回收机制:分代复制垃圾回收,标记垃圾回收,增量垃圾回收

    一.分代复制垃圾回收 不同的对象的生命周期是不一样的.因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率. 在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比 ...

  3. JVM面试(四)-垃圾回收、垃圾收集器、GC日志

    垃圾回收.垃圾收集器.GC日志 什么是垃圾?(垃圾的概念) 什么是垃圾回收?(垃圾回收的概念) 为什么要垃圾回收?(垃圾回收的原因) 如何定义垃圾? 引用计数算法 什么是循环引用 可达性分析算法 哪些 ...

  4. java 2分代复制垃圾回收_Java对象的后事处理——垃圾回收(二)

    1 先谈Finalize() finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好.更及时,所以笔者建议大家完全可以忘掉Java语言中有这个方法的存在. --< ...

  5. c语言代码大全复制,垃圾回收算法实现之 - 复制算法(完整可运行C语言代码)...

    GC 复制算法(Copying GC)是 Marvin L. Minsky 在 1963 年研究出来的算法.说得简单点,就是只把某个空间里的活动对象复制到其他空间,把原空间里的所有对象都回收掉.这是一 ...

  6. 4、JVM垃圾回收机制、新生代的GC、GC(Minor GC、FullGC)、GC日志、JVM参数选项、元空间(笔记)

    4.JVM垃圾回收机制 4.1.新生代的GC 4.1.1.串行GC(SerialGC) 4.1.2.并行回收GC(Parallel Scavenge) 4.1.3.并行GC(ParNew) 4.2.G ...

  7. Java 垃圾回收最全讲解(GC过程、可达性分析、方法,7大回收器)

     垃圾回收机制GC  Garbage Collection 堆是Java虚拟机进行垃圾回收的主要场所,其次要场所是方法区.JAVA8 之后Java将堆内存分为2大部分:新生代(1/3).老年代(2/3 ...

  8. 垃圾回收算法与实现系列-GC 标记-清除算法

    导语   在GC 中最重要的算法就是GC标记-清除算法(Mark-Sweep GC).在很多的场景下都还是在使用这个算法来进行垃圾回收操作.就如如同它的名字一样先标记,然后清除.下面就来看看标记清除算 ...

  9. 【Java 虚拟机原理】垃圾回收算法 ( 可达性分析算法 | GC Root 示例 | GC 回收前的两次标记 | finalize 方法示例 )

    文章目录 一.可达性分析算法 二.GC Root 示例 三.GC 回收前的两次标记 四.finalize 方法示例 一.可达性分析算法 在 堆内存 中 , 存在一个 根对象 GC Root , GC ...

最新文章

  1. matlab图像融合评价,MATLAB 图像融合评估算法
  2. hdu 1575Tr A
  3. PC串口DB9接口 示意图 (备忘)
  4. Leetcode--264. 丑数Ⅱ
  5. Dev TextEdit 输入提示
  6. WPF 4 动态覆盖图标(Dynamic Overlay Icon)
  7. 基于asp.net mvc的近乎产品开发培训课程(第四讲)
  8. TiDB 在摩拜单车的深度实践及应用
  9. elasticsearch的javaAPI之query
  10. ROS-Academy-for-Beginners之ORB-SLAM2 双目视觉初探
  11. redis事物的使用
  12. 【Unity步步升】导航网格、寻路算法及AI行为树等应用与实践...
  13. Html5 打砖块游戏,加入道具和速通模式(含源码)
  14. 素数的线性筛 欧拉函数
  15. C++ 面向对象、内存管理
  16. 4399小游戏—宠物连连看经典版2—游戏辅助脚本
  17. 2017.11.17 Demo-stm8+temperature timeing control
  18. html网站底部导航栏怎么做,如何设计一个页面的底部导航?
  19. 01-数据结构和算法入门
  20. 整整7天,梳理 Java开发2022年(图文+代码)面试题及答案

热门文章

  1. Tensorflow C++ API 生成复数算子
  2. 天宇优配|太罕见!“股神”巴菲特又出手了,这次是日本市场
  3. 计算机网络-多路复用
  4. smartqq协议java_基于SmartQQ协议的QQ聊天机器人-3
  5. vue2基础-自定义指令v-focus、v-pin 指令动态传参
  6. 2004年7月16日
  7. mysql毫秒转分钟_[MYSQL]时间毫秒数转换
  8. goLang 纳秒转 毫秒 转 英文时间格式
  9. ECharts中国地图-- 省份文字居中
  10. idea java热部署很慢_IDEA热部署(一)---解析关键配置。