2014-10-05-python-memory-management-comparison

Posted on October 5, 2014

内存管理大概是时下讨论语言问题最热门的话题之一了。 计算机专业的面试聊天要是扯不上GC,会有种不够专业的感觉。 C/C++强大的内存管理却广受诟病,因为人们其实并不想关心内存管理,他们更关心在可接受的条件下直接使用自动管理的内存,他们不想把时间浪费在精细的内存泄露问题上。

如果内存不是那么紧缺,系统延迟要求不是那么严格,人们会主动/甚至应该回避内存管理问题。 这也是Java/C#等后起的语言都带有自动内存管理(GC)的原因。 有了这些工具的帮助,程序员便可以把精力集中在解决现实问题上,而不是被这些计算机体系的基础问题所困扰。

C/C++赋予程序员能力来管理系统中内存的使用,申请多少内存,就释放多少内存, 直接访问内存地址。 如前所述,这些灵活性的代价也是巨大的,如果并不熟练,或者稍有考虑不周都会给程序带来严重的问题。

Java的出现就帮程序员规避了这些问题, Java采用标记清除策略来对内存进行回收, 程序需要内存的时候尽管从JVM堆上进行分配。 Java会定时或者在特定事件触发的情况下,对堆内存进行回收。 回收的标准也很简单:定义当前仍然被Java主程序使用的Root对象,从这些Root对象出发,通过引用链能够到达的对象就称为可达对象。 可达对象显然是程序仍然在使用的对象,因此系统对不可达的对象进行回收,释放它们所占用的空间。 Root对象定义为线程栈的局部对象以及线程栈所在对象的静态成员等。 这当然是很泛泛的描述, 如果致力于提高自动内存管理的效率,那么有很多的话题可以展开。 总而言之,Java的垃圾回收机制成功的解决了这个问题,并且这种做法为后来的C#语言等语言所借鉴改进。

Python语言也尝试为程序员提供这样的便利语言特性,Python首先采用的是引用计数的方法。 这种方法其实更为直观, 每一个对象实例都维护一个引用计数来追踪自身被引用的情况。 当对象引用发生变化的时候,引用计数相应发生变化,当引用计数降为0的时候,表明对象不再被使用,因此可以被释放。

引用计数的优点是: 内存回收的速度快,不需要追踪整个引用链。 标记清除通常引入很大的代价, 因为为了保证捕捉到的引用是正确的,通常需要停止所有当前线程的执行(stop the world)。 而引用计数则没有这样的限制。

引用计数的缺点是: 首先,每个对象多了一个冗余的内存布局,这就是较大的开销了。 然后引用计数不能解决称为“循环引用”的情况。 如果两个对象相互引用,并且都不被使用了,那么他们的引用计数始终为1,永远不会被释放。

从我自己的角度出发,我觉得标记清除带来的延迟代价比较大。 引用计数带来的内存额外代价比较大。 对于引用计数的循环引用问题, 我觉得把这个问题推给程序员,让他们自己保证对象不会产生循环引用不就是了? 事实上这种策略也是有用例的。 ObjectiveC就采用了这种策略。 然而这始终是给编程提供便利和牺牲某些性能之间的折衷。 使用MarkSweep的Java显然深入人心, 人们才不会自己去管理什么循环依赖? 不然他们为什么花费那么多内存代价来获得内存管理的便利? 他们直接自己管理内存泄露好了。 事实是标记清除经过不断的发展,那些问题在多数场景下也并不是那么突出。

能够理解Python语言一开始使用了引用计数作为自动内存管理的策略, 然而因为采用了这种策略, 为了修复剩下的那点不便利性, 在1.2以后的版本中Python引入了循环检测算法来消除引用计数残存的缺陷。 这样Python为了进行自动内存管理引入了两种方法。 正是因为引用计数的这种缺陷性,一般的内存管理都会采用标记清除类的算法。 而Python却没有办法抛弃这种设计: 原因一是前向兼容,如果对核心的内存管理进行修改,势必影响大量已有的程序库的使用; 原因之二是Python [extension module][5] 因为这些扩展模块的存在,Python并不能很清楚的定义当前的Root对象,自然无法标记清除垃圾对象。

Jython和IronPython成功的抛弃了引用计数,转向了标记清除算法。然而这也是为什么他们会遇到严重的前向兼容问题而无法被广泛使用。

==

本文提到的Python均为CPython运行时

[1]:http://stackoverflow.com/questions/4484167/details-how-python-garbage-collection-works
[2]:http://svn.python.org/view/python/trunk/Modules/gcmodule.c?revision=81029&view=markup
[3]:http://www.digi.com/wiki/developer/index.php/Python_Garbage_Collection
[4]:https://docs.python.org/2/library/gc.html
[5]:http://arctrix.com/nas/python/gc/
[6]:http://stackoverflow.com/questions/26200802/why-python-cannot-determine-roots-and-use-mark-sweep