垃圾回收原理

为什么需要 GC

在计算机诞生初期,在程序运行过程中没有栈帧(stack frame)需要去维护,所以内存采取的是静态分配策略,这虽然比动态分配要快,但是其一明显的缺点是程序所需的数据结构大小必须在编译期确定,而且不具备运行时分配的能力,这在现在来看是不可思议的。在 1958 年,Algol-58 语言首次提出了块结构(block-structured),块结构语言通过在内存中申请栈帧来实现按需分配的动态策略。在过程被调用时,帧(frame)会被压到栈的最上面,调用结束时弹出。栈分配策略赋予程序员极大的自由度,局部变量在不同的调用过程中具有不同的值,这为递归提供了基础。但是后进先出(Last-In-First-Out, LIFO)的栈限制了栈帧的生命周期不能超过其调用者,而且由于每个栈帧是固定大小,所以一个过程的返回值也必须在编译期确定。所以诞生了新的内存管理策略——堆(heap)管理。

堆分配运行程序员按任意顺序分配/释放程序所需的数据结构——动态分配的数据结构可以脱离其调用者生命周期的限制,这种便利性带来的问题是垃圾对象的回收管理。C/C++/Pascal 把这个任务交给了程序员,但事实证明这非常容易出错,野指针(wild pointer)、悬挂指针(dangling pointer)是比较典型的错误。在另一些场景中,动态分配的对象传入了其他过程中,这时程序员或编译器就无法预测这个对象什么时刻不再需要,现如今的面向对象语言,这种场景更是频繁,这也就间接促进了自动内存管理技术的发展。

虽然C++11中引入了 智能指针来解决一部分内存管理问题,但这远远不能到达满意的效果,在实际使用的过程中,shared_ptr可能由于循环引用带来严重的内存泄露问题。所以即便是 C/C++ ,也有类似 Boehm GC 这样的第三方库来实现内存的自动管理。可以毫不夸张的说,GC 已经是现代语言的标配了。

GC常用算法

  • Collector,用于进行垃圾回收的线程
  • Mutators,应用程序的线程,可以修改 heap

Mark-Sweep  标记清除算法

该算法主要包括两步,

  1. mark,从 root 开始进行树遍历,每个访问的对象标注为「使用中」
  2. sweep,扫描整个内存区域,对于标注为「使用中」的对象去掉该标志,对于没有该标注的对象直接回收掉

该算法的缺点有:

  1. 在进行 GC 期间,整个系统会被挂起(暂停,Stop-the-world),所以在一些实现中,会采用各种措施来减少这个暂停时间
  2. heap 容易出现碎片。实现中一般会进行 move 或 compact。(需要说明一点,所有 heap 回收机制都会这个问题)
  3. 在 GC 工作一段时间后,heap 中连续地址上存在 age 不同的对象,这非常不利于引用的本地化(locality of reference)
  4. 回收时间与 heap 大小成正比

三色标记

三色标记(tricolor marking)抽象屏蔽了 GC 实现的算法(MS/Copying)、遍历策略(宽度优先/深度优先)等细节,对于理解增量式 GC 十分有帮助。具体来说是在 GC 遍历引用关系图时,对象会被标为三种颜色:

  1. 黑色black,表明对象被 collector 访问过,属于可到达对象
  2. 灰色gray,也表明对象被访问过,但是它的子节点还没有被 scan 到
  3. 白色white,表明没有被访问到,如果在本轮遍历结束时还是白色,那么就会被收回

对于 MS 来说,设置标记位就是着色的过程:有 mark-bit 的即为黑色。对 Copying GC 来说,把对象从 fromspace 移动到 tospace 就是着色过程:在 fromspace 中不可到达的对象为白色,被移动到 tospace 的对象为黑色。 对于增量时 GC 来说,需要在黑白之间有个中间状态来记录「那些之前被 collector 标记黑色,后来又被 mutator 改变的对象」,这就是灰色的作用。 对于 MS 来说,灰色对象是用于协助遍历 queue 里面的对象,即上文中描述的 worklist 里面的对象。对于 Copying GC 来说,灰色对象就是那些在 topspace 中还没被 scan 的对象,如果采用 Cheney 的宽度优先遍历算法 ,那么就是 scan 与 free 指针之间的对象。

增加的中间状态灰色要求 mutator 不会把黑色对象直接指向白色对象(这称为三色不变性 tri-color invariant),collector 就能够认为黑色对象不需要在 scan,只需要遍历灰色对象即可。

违法三色不变性的一个例子

违法三色不变性的一个例子

上图描述了一个违法着色不变性的情况。假设 A 已经被完全地 scan,它本身被标为黑色,字节点被标为灰色,现在假设 mutator 交换了 A–>C 与 B–>D 的指针,现在指向 D 的指针只有 A,而 A 已经被完全地 scan 了,如果继续 scan 过程的话,B 会被置为黑色,C 会被重新访问,而 D 则不会被访问到,在本轮遍历后,D 由于是白色,会被错误的认为是垃圾并被回收掉。

为了解决上面的问题,一般有两类方式来协调 mutator 与 collector 的行为:

  1. 读屏障(read barrier),它会禁止 mutator 访问白色对象,当检测到 mutator 即将要访问白色对象时,collector 会立刻访问该对象并将之标为灰色。由于 mutator 不能访问指向白色对象的指针,也就无法使黑色对象指向它们了
  2. 写屏障(write barrier),它会记录下 mutator 新增的由黑色–>白色对象的指针,并把该对象标为灰色,这样 collector 就又能访问有问题的对象了

读/写屏障本质是一些同步操作——在 mutator 进行某些操作前,它必须激活 collector 进行一些操作。 在实际应用中,调用 collector 只需要一些简单的操作,compiler 可以在输出 mutator 机器码(machine code)的同时,额外输出一些指令(instructions),在进行读/写指针时,会额外执行这些指令。根据读/写屏障复杂度,整个屏障操作可以内联(inline),也可以是个额外的过程调用(out of line procedure call)。


分代式 GC

虽然对象的生命周期因应用而异,但对于大多数应用来说,80% 的对象在创建不久即会成为垃圾1。因此,针对不同 age 的对象「划分不同区域,采用不同的回收策略」也就不难理解了。

参考文章: 深入浅出垃圾回收(一)简介篇 - Keep Coding

   

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值