垃圾收集机制的批判

垃圾收集机制的批判

[原创] myan 2004-05-26
在Java版发表这篇文章,似乎械惆衙分赶騄ava了。其实不是,GC是所有新一代语言共有的特征,Python, Eiffel,C#,Roby等无一例外地都使用了GC机制。但既然Java中的GC最为著名,所以天塌下来自然应该抗着。

这篇短文源于comp.lang.java.programmer跟comp.lang.c++上发生的一场大辩论,支持C++和Java的两派不同势力展开了新世纪第一场冲突,跟贴发言超过350,两派都有名角压阵。C++阵营的擂主是Pete Becker,ACM会员,Dinkumware Ltd. 的技术副总监。此君精通C++和Java,开发过两种语言的核心类库,但是却对C++狂热之极,而对于Java颇不以为然。平时谈到Java的时候还好,一旦有人胆敢用Java来批判C++,立刻忍不住火爆脾气跳将出来,以坚韧不拔的毅力和大无畏精神与对手周旋,舌战群儒,哪怕只剩下一个人也要血战到底。这等奇人当真少见!我真奇怪他整天泡在usenet上,不用工作么?他的老板P.J. Plauger如此宽宏大量?Java阵营主角是一个网名Razzi的兄弟,另外有Sun公司大名鼎鼎的Peter van der Linden助阵,妙语连珠,寸土必争,加上人多势众,一度占据优势。C++阵营里大拿虽然很多,但是大多数没有Pete那么多闲工夫,例如Greg Comeau,Comeau公司老板,每次来个只言片语,实在帮不了Pete多大忙。但是自从C++阵营中冒出一个无名小子,网名Courage(勇气),发动对Java GC机制的批判,形势为之一变。C++阵营眼下处于全攻之势,Java阵营疲于防守,只能招架说:“你们没有证据,没有统计资料”,形势很被动。

垃圾收集(GC)不是一直被Java fans用来炫耀,引以为傲的优点么?怎么成了弱点了?我大惑不解,定睛一看,才觉得此中颇有道理。

首先,Java Swing库存在大量资源泄漏问题,这一点SUN非常清楚,称之为bugs,正在极力修正。但是看来这里的问题恐怕不仅是库编写者的疏忽,可能根源在于深层的机制,未必能够轻易解决,搞不好要伤筋动骨。不过这个问题不是那么根本,C++阵营觉得如果抓住对方的弱点攻击,就算是占了上风也没什么说服力。谁没有缺点呢?于是反其道而行之,猛烈攻击Java阵营觉得最得意的东西,Java的GC机制本身。首先来想一想,memory leak到底意味着什么。在C++中,new出来的对象没有delete,这就导致了memoryleak。但是C++早就有了克服这一问题的办法——smart pointer。通过使用标准库里设计精致的auto_ptr以及各种STL容器,还有例如boost库(差不多是个准标准库了)中的四个smart pointers,C++程序员只要花上一个星期的时间学习最新的资料,就可以拍着胸脯说:“我写的程序没有memory leak!”。

相比之下,Java似乎更优秀,因为从一开始你就不用考虑什么特殊的机制,大胆地往前new,自有GC替你收拾残局。Java的GC实际上是JVM中的一个独立线程,采用不同的算法策略来收集heap中那些不再有reference指向的垃圾对象所占用的内存。但是,通常情况下,GC线程的优先级比较低,只有在当前程序空闲的时候才会被调度,收集垃圾。当然,如果JVM感到内存紧张了,JVM会主动调用GC来收集垃圾,获取更多的内存。请注意,Java的GC工作的时机是:1. 当前程序不忙,有空闲时间。2. 空闲内存不足。现在我们考虑一种常见的情况,程序在紧张运行之中,没哟空闲时间给GC来运行,同时机器内存很大,JVM也没有感到内存不足,结果是什么?对了,GC形同虚设,得不到调用。于是,内存被不断吞噬,而那些早已经用不着的垃圾对象仍在在宝贵的内存里睡大觉。例如:

class BadGc {
    public void job1() {
        String garbage = "I am a garbage, and just sleeping in your precious memory, " +
                         "how do you think you can deal with me? Daydreaming! HAHA!!!";
        ....
    }

    public void job2() {...}

    ...
    ...

    public void job1000() {...}

    public static void main(String[] args) {
        bgc = new BadGc();
        bgc.job1();
        bgc.job2();
        ...
        bgc.job1000();
    }
}

运行中,虽然garbage对象在离开job1()之后,就再也没有用了。但是因为程序忙,内存还够用,所以GC得不到调度,garbage始终不会被回收,直到程序运行到bgc.job1000()时还躺在内存里嘲笑你。没辙吧!

好了,我承认这段程序很傻。但是你不要以为这只是理论上的假设,恰恰相反,大多数实用中的Java程序都有类似的效应。这就是为什么Java程序狂耗内存,而且好像给它多少内存吃都不够。你花上大笔的银子把内存从128升到256,再升到512,结果是,一旦执行复杂任务,内存还是被轻易填满,而且多出来的这些内存只是用来装垃圾,GC还是不给面子地千呼万唤不出来。等到你的内存终于心力交瘁,GC才姗姗来迟,收拾残局。而且GC工作的方式也很不好评价,一种方法是一旦有机会回收内存,就把所有的垃圾都回收。你可以想象,这要花很长时间(几百M的垃圾啊!),如果你这时侯正在压下开炮的按钮,GC却叫了暂定,好了,你等死吧!另一种方法,得到机会之后,回收一些内存,让JVM感到内存不那么紧张时就收手。结果呢,内存里始终有大批垃圾,程序始终在半死不活的荡着。最后,GC可以每隔一段时间就运行一次,每次只回收一部分垃圾,这是现在大部分JVM的方式,结果是内存也浪费了,还动不动暂停几百毫秒。难啊!

反过来看看C++利用smart pointer达成的效果,一旦某对象不再被引用,系统刻不容缓,立刻回收内存。这通常发生在关键任务完成后的清理(cleanup)时期,不会影响关键任务的实时性,同时,内存里所有的对象都是有用的,绝对没有垃圾空占内存。怎么样?传统、朴素的C++是不是更胜一筹?

据统计,目前的Java程序运行期间占用的内存通常为对应C++程序的4-20倍。除了其它的原因,上面所说的是一个非常主要的因素。我们对memory leak如此愤恨,不就是因为它导致大量的内存垃圾得不到清除吗?如果有了GC之后,垃圾比以前还来势汹汹,那么GC又有什么好处呢?

当然,C++的smart pointer现在会使用的人不多,所以现在的C++程序普遍存在更严重的memory leak问题。但是,如果我奶奶跟舒马赫比赛车输掉了,你能够埋怨那辆车子么?
书围绕着动态内存自动回收的话题,介绍了垃圾收集机制,详细分析了各种算法和相关技术。   本书共12章。第1章首先介绍计算机存储器管理的演化和自动内存回收的需求,并引入了本书所使用的术语和记法。第2章介绍了3种“经典”的垃圾收集技术:引用计数(reference counting)、标记-清扫(mark-sweep)和节点复制(copying)。 随后的4章更详细地讨论了上述这些垃圾收集方式和标记-缩并(mark-compact)收集。第7章和第8章分别介绍了在现代垃圾收集实现中具有重要地位的分代式(generational)垃圾收集和渐进式(incremental)垃圾收集。第9章和第10章扩展了垃圾收集的领域,讨论了如何让垃圾收集能够在无法得到来自语言编译器的支持的环境(分别是C和C++)中运行。第11章讨论了一个相对较新的研究领域 -- 垃圾收集和硬件数据cache的相互作用。第12章简要地考察了用于分布式系统的垃圾收集。   本书适合对动态内存管理感兴趣的读者阅读,可供专业的研究人员参考。 目录: 第1章 简介 1.1 内存分配的历史 1.1.1 静态分配 1.1.2 栈分配 1.1.3 堆分配 1.2 状态、存活性和指针可到达性 1.3 显式堆分配 1.3.1 一个简单的例子 1.3.2 垃圾 1.3.3 悬挂引用 1.3.4 共享 1.3.5 失败 1.4 为什么需要垃圾收集 1.4.1 语言的需求 1.4.2 问题的需求 1.4.3 软件工程的课题 1.4.4 没有银弹 1.5 垃圾收集的开销有多大 1.6 垃圾收集算法比较 1.7 记法 1.7.1 堆 1.7.2 指针和子女 1.7.3 伪代码 1.8 引文注记 第2章 经典算法 2.1 引用计数算法 2.1.1 算法 2.1.2 一个例子 2.1.3 引用计数算法的优势和弱点 2.1.4 环形数据结构 2.2 标记——清扫算法 2.2.1 算法 2.2.2 标记——清扫算法的优势和弱点 2.3 节点复制算法 2.3.1 算法 2.3.2 一个例子 2.3.3 节点复制算法的优势和弱点 2.4 比较标记——清扫技术和节点复制技术 2.5 需要考虑的问题 2.6 引文注记 第3章 引用计数 3.1 非递归的释放 3.1.1 算法 3.1.2 延迟释放的优点和代价 3.2 延迟引用计数 3.2.1 Deutsch-Bobrow算法 3.2.2 一个例子 3.2.3 ZCT溢出 3.2.4 延迟引用计数的效率 3.3 计数域大小受限的引用计数 3.3.1 “粘住的”计数值 3.3.2 追踪式收集恢复计数值 3.3.3 仅有一位的计数值 3.3.4 恢复独享信息 3.3.5 “Ought to be two”缓冲区 3.4 硬件引用计数 3.5 环形引用计数 3.5.1 函数式程序设计语言 3.5.2 Bobrow的技术 3.5.3 弱指针算法 3.5.4 部分标记——清扫算法 3.6 需要考虑的问题 3.7 引文注记 第4章 标记——清扫垃圾收集 4.1 与引用计数技术的比较 4.2 使用标记栈 4.2.1 显式地使用栈来实现递归 4.2.2 最小化栈的深度 4.2.3 栈溢出 4.3 指针反转 4.3.1 Deutsch-Schorr-Waite算法 4.3.2 可变大小节点的指针反转 4.3.3 指针反转的开销 4.4 位图标记 4.5 延迟清扫 4.5.1 Hughes的延迟清扫算法 4.5.2 Boehm-Demers-Weriser清扫器 4.5.3 Zorn的延迟清扫器 4.6 需要考虑的问题 4.7 引文注记 第5章 标记——缩并垃圾收集 5.1 碎片现象 5.2 缩并的方式 5.3 “双指针”算法 5.3.1 算法 5.3.2 对“双指针”算法的分析 5.3.3 可变大小的单元 5.4 Lisp2算法 5.5 基于表的方法 5.5.1 算法 5.5.2 间断表 5.5.3 更新指针 5.6 穿线方法 5.6.1 穿线指针 5.6.2 Jonkers的缩并算法 5.6.3 前向指针 5.6.4 后向指针 5.7 需要考虑的问题 5.8 引文注记 第6章 节点复制垃圾收集 6.1 Cheney的节点复制收集器 6.1.1 三色抽象 6.1.2 算法 6.1.3 一个例子 6.2 廉价地分配 6.3 多区域收集 6.3.1 静态区域 6.3.2 大型对象区域 6.3.3 渐进的递增缩并垃圾收集 6.4 垃圾收集器的效率 6.5 局部性问题 6.6 重组策略 6.6.1 深度优先节点复制与广度优先节点复制 6.6.2 不需要栈的递归式节点复制收集 6.6.3 近似于深度优先的节点复制 6.6.4 层次分解 6.6.5 哈希表 6.7 需要考虑的问题 6.8 引文注记 第7章 分代式垃圾收集 7.1 分代假设 7.2 分代式垃圾收集 7.2.1 一个简单例子 7.2.2 中断时间 7.2.3 次级收集的根集合 7.2.4 性能 7.3 提升策略 7.3.1 多个分代 7.3.2 提升的阈值 7.3.3 Standard ML of New Jersey收集器 7.3.4 自适应提升 7.4 分代组织和年龄记录 7.4.1 每个分代一个半区 7.4.2 创建空间 7.4.3 记录年龄 7.4.4 大型对象区域 7.5 分代间指针 7.5.1 写拦截器 7.5.2 入口表 7.5.3 记忆集 7.5.4 顺序保存缓冲区 7.5.5 硬件支持的页面标记 7.5.6 虚存系统支持的页面标记 7.5.7 卡片标记 7.5.8 记忆集还是卡片 7.6 非节点复制的分代式垃圾收集 7.7 调度垃圾收集 7.7.1 关键对象 7.7.2 成熟对象空间 7.8 需要考虑的问题 7.9 引文注记 第8章 渐进式和并发垃圾收集 8.1 同步 8.2 拦截器方案 8.3 标记——清扫收集器 8.3.1 写拦截器 8.3.2 新单元 8.3.3 初始化和终止 8.3.4 虚存技术 8.4 并发引用计数 8.5 Baker的算法 8.5.1 算法 8.5.2 Baker算法的延迟的界限 8.5.3 Baker的算法的局限 8.5.4 Baker算法的变种 8.5.5 动态重组 8.6 Appel-Ellis-Li收集器 8.6.1 各种改进 8.6.2 大型对象 8.6.3 分代 8.6.4 性能 8.7 应变复制收集器 8.7.1 Nettle的应变复制收集器 8.7.2 Huelsbergen和Larus的收集器 8.7.3 Doligez-Leroy-Gonthier收集器 8.8 Baker的工作环收集器 8.9 对实时垃圾收集的硬件支持 8.10 需要考虑的问题 8.11 引文注记 第9章 C语言的垃圾收集 9.1 根不确定收集的一个分类 9.2 保守式垃圾收集 9.2.1 分配 9.2.2 寻找根和指针 9.2.3 内部指针 9.2.4 保守式垃圾收集的问题 9.2.5 识别错误 9.2.6 效率 9.2.7 渐进式、分代式垃圾收集 9.3 准复制式收集 9.3.1 堆的布局 9.3.2 分配 9.3.3 垃圾收集 9.3.4 分代式垃圾收集 9.3.5 无法精确识别的数据结构 9.3.6 准复制式收集的效率 9.4 优化的编译器是“魔鬼” 9.5 需要考虑的问题 9.6 引文注记 第10章 C++语言的垃圾收集 10.1 用于面向对象语言的垃圾收集 10.2 对C++垃圾收集器的需求 10.3 在编译器中还是在库中 10.4 保守式垃圾收集 10.5 准复制式收集器 10.6 智能指针 10.6.1 在没有智能指针类层次的情况下进行转换 10.6.2 多重继承 10.6.3 不正确的转换 10.6.4 某些指针无法“智能化” 10.6.5 用const和volatile修饰的指针 10.6.6 智能指针的“泄漏” 10.6.7 智能指针和引用计数 10.6.8 一个简单的引用计数指针 10.6.9 用于灵活的垃圾收集的智能指针 10.6.10 用于追踪式垃圾收集的智能指针 10.7 为支持垃圾收集而修改C++ 10.8 Ellis和Detlefs的建议 10.9 终结机制 10.10 需要考虑的问题 10.11 引文注记 第11章 垃圾收集与cache 11.1 现代处理器体系结构 11.2 cache的体系结构 11.2.1 cache容量 11.2.2 放置策略 11.2.3 写策略 11.2.4 特殊的cache指令 11.3内存访问的模式 11.3.1 标记——清扫技术,使用标记位图和延迟清扫 11.3.2 节点复制垃圾收集 11.3.3 渐进式垃圾收集 11.3.4 避免读取 11.4 改进cache性能的标准方法 11.4.1 cache的容量 11.4.2 块大小 11.4.3 相联度 11.4.4 特殊指令 11.4.5 预取 11.5 失误率和总体cache性能 11.6 专用硬件 11.7 需要考虑的问题 11.8 引文注记 第12章 分布式垃圾收集 12.1 需求 12.2 虚拟共享存储器 12.2.1 共享虚拟存储器模型 12.2.2 共享数据对象模型 12.2.3 分布式共享存储器之上的垃圾收集 12.3 与分布式垃圾收集有关的课题 12.3.1 分类原则 12.3.2 同步 12.3.3 鲁棒性 12.4 分布式标记——清扫 12.4.1 Hudak和Keller 12.4.2 Ali的算法 12.4.3 Hughes的算法 12.4.4 Liskov-Ladin算法 12.4.5 Augusteijn的算法 12.4.6 Vestal的算法 12.4.7 Schelvis-Bledoeg算法 12.4.8 Emerald收集器 12.4.9 IK收集器 12.5 分布式节点复制 12.6 分布式引用计数 12.6.1 Lermen-Maurer协议 12.6.2 间接引用计数 12.6.3 Mancini-Shrivastava算法 12.6.4 SPG协议 12.6.5 “Garbage collecting the world” 12.6.6 网络对象 12.6.7 带权引用计数 12.6.8 世代引用计数 12.7 对actor进行垃圾收集 12.7.1 Halstead算法 12.7.2 标记算法 12.7.3 逻辑上集中式的收集器 12.8 引文注记
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值