【算法总结】垃圾收集算法

转载 1

垃圾收集算法是个很大的话题。首先要明确的是,垃圾收集算法和语言不一定是绑定的。比如 Java,不同的 JVM 实现可能采用不同的算法。其次,垃圾收集算法数量庞大,一一列举是不可能的,篇幅所限这里只能给个非常概略的介绍。如果希望对垃圾收集相关算法有个全景式的了解,请参阅本人的译作,垃圾收集 (豆瓣)


==== 转入正文的分割线 ====

从各种垃圾收集算法最基本的运行方式来说,大概可以分成三个类型:

1. 引用计数(reference counting):

基本思路是为每个对象加一个计数器,记录指向这个对象的引用数量。每次有一个新的引用指向这个对象,计数器加一;反之每次有一个指向这个对象引用被置空或者指向其他对象,计数器减一。当计数器变为 0 的时候,自动删除这个对象。

引用计数的优点是 1)相对简单,不需要太多运行时(run-time)的支持,可以在原生不支持 GC 的语言里实现。2)对象会在成为垃圾的瞬间被释放,不会给正常程序的执行带来额外中断。它的死穴是循环引用,对象 A 包含一个引用指向对象 B ,同时对象 B 包含一个引用指向对象 A,计数器就抓瞎了。另外,引用计数对正常程序的执行性能有影响(每次引用赋值都要改计数器),特别是在多线程环境下(改计数器要加锁同步)。

现在仍然主要采用引用计数的例子有 Apple 的 ARC,C++ 新标准里的 std::shared_ptr。

2. 标记-清扫(mark-sweep)。

基本思路是先按需分配,等到没有空闲内存的时候从寄存器和程序栈上的引用出发,遍历以对象为节点、以引用为边构成的图,把所有可以访问到的对象打上标记,然后清扫一遍内存空间,把所有没标记的对象释放。

标记-清扫没有无法处理循环引用的问题,不触发 GC 时也不影响正常程序的执行性能。但它的问题是当内存耗尽触发 GC 时,需要中断正常程序一段时间来清扫内存,在内存大对象多的时候这个中断可能很长。

采用或者部分采用标记-清扫的例子非常多,不一一列举了。

3. 节点复制(copying)。

基本思路是把整个内存空间一分为二,不妨记为 A 和 B。所有对象的内存在 A 中分配,当 A 塞满的时候,同样从寄存器和程序栈上的引用出发,遍历以对象为节点、以引用为边构成的图,把所有可以访问到的对象复制到 B 去,然后对调 A 和 B 的角色。

相对于标记-清扫,节点复制的主要缺点是总有一半空间空闲着无法利用,另一个比较隐晦的缺点是它使用内存的方式与现有的内存换页、Cache 换入换出机制有潜在的冲突。但它有个很大的优点: 所有的对象在内存中永远都是紧密排列的,所以分配内存的任务变得极为简单,只要移动一个指针即可。对于内存分配频繁的环境来说,性能优势相当大。另外,由于不需要清扫整个内存空间,所以如果内存中存活对象很少而垃圾对象很多的话(有些语言有这个倾向),触发 GC 造成的中断会小于标记-清扫。

同样的,采用或者部分采用节点复制的例子也非常多,不一一列举了。

==== 基本算法介绍完毕的分割线 ====

以上三种基本算法各有优缺点,也各有许多改进的方案。目前工程实践上最为成功的方案应该要算分代(generational)垃圾收集。它的基本思路是这样的:程序中存在大量的临时对象,分配出来之后很快就会被释放,而同时如果一个对象分配出来之后相当长的一段时间内都没回收,那么极有可能它的生命周期很长,尝试收集它会是无用功。所以可以把内存有意识地按“对象年龄”分成若干块,不妨记为老中青(XD),所有的分配都在青代进行,青代塞满只对青代做 GC,然后把存活下来的对象移动到中代,直到中青代都塞满,再把存活下来下来的对象移动到老代 —— 这只是个思路上的例子,实践中分代式垃圾收集算法的方案五花八门,而且常常同时使用了不止一种基本算法(比如青代用节点复制,老代用标记清扫啥的)。


转载 2

JVM 最新一代的GC,Garbage First,
基本机制就是把堆划分为很多固定大小的region,每个region都有一个对应的remembered set结构,用来记录指向这个区域中的地址的其他区域的指针。 在垃圾回收时,选择记录最少的一个区域进行,按找这种方式选择出来的区域,通常是有用数据最少、垃圾最多的区域。将其中活跃的对象通过copy的方式复制到其他的区域中(准确说来会是old region,对于old region会有一定的几率进行回收)。这也就是“Garbage-first ”名称的由来。如此一来就可以以region为单位整区域回收内存(传统的GC是以单个对象内存为单位)。

下面两篇文章都有详尽的描述:
infoq.com/cn/news/2008/ 
blogjava.net/BlueDavy/a

转载 3

纵观GC的历史共有三种基本算法:
1、标记清除:
在此之上又有变形算法——标记压缩。
2、复制收集:
复制收集利用两个内存区域,避免了标记清除算法在清楚阶段还需要遍历大量死亡节点的过程,减少了时间开支。
3、引用计数
所有对象保存着自己被别人引用的次数,减少一次,则自身减少1,同时自己引用的对象的引用计数也减少1。计数为0的为死亡节点可以释放。引用计数不需要遍历全部对象,实现最为简单。释放空间是立即释放。缺点是如果有循环引用(引用关系连成环了)则永远也释放不了,同时不适合并行处理。


基于以上方法现代改良的GC算法:
1、分代回收:
基本假说:大部分新产生的对象会在较短时间内成了垃圾,而经过一定时间依然存活的对象一般寿命较长。
依据此假说,那么只要扫描新生代的对象就可以实现大部分垃圾的回收。扫描新生代对象称为“小回收”,一般采用复制收集算法,另外为了加速扫描在扫描过程中如果遇到老生代对象,不递归扫描。然后将小回收剩下的对象划到老生代。
如果有老生代对新生代的引用,则用一个记录集来实时记录,称为“写屏障”。小回收中,记录集也当做一个扫描“根”来对待。
一定时候会对老生代,新生代一起进行一次“大回收”。
实际上,很多语言的GC会设置好几代,而不是两代。
2、增量回收:
为了保持程序实时性,将一个GC不一次执行,而是分成多个小步骤在程序间隙分步执行。显然这种算法需要实时维护程序中对象的引用关系,在每一步骤执行时都需记录引用状态变更(即“写屏障”)。
3、并行回收:
充分利用多核,让GC并行在多核上进行回收,将占用程序运行的时间缩小到最小。

GC大统一理论:
IBM研究者说:任何一种GC算法都是引用计数和跟踪回收两种思路的组合。

——以上,整理自松本行弘《代码的未来》

目前来看,JAVA的GC是应用技术最多,同时也是性能最好的。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值