当前位置: 首页 > 科技观察

Ruby和Python中的垃圾收集

时间:2023-03-21 13:16:55 科技观察

注:本文基于我在布达佩斯RuPy大会上的演讲。与其仅仅发布幻灯片,我认为在我还拥有它们的时候将它们写成博客更有意义。另外,我以后会贴出RuPy大会的视频链接。我计划在RubyConf上做一个类似的演讲,除了Python部分,并比较MRI、JRuby和Rubinius垃圾收集器的工作原理。如果你想更深入地了解Ruby垃圾收集器及其内部原理,你可以在我即将出版的新书中找到答案《Ruby Under a Microscope》。如果算法和业务逻辑是人的大脑,那么垃圾回收机制是人体的哪个器官?在“RubyPython”会议上,我认为比较Ruby和Python的内部垃圾收集机制会很有趣。在开始之前,我们为什么要讨论垃圾回收机制?毕竟这是最迷人、最令人兴奋的科目之一,不是吗?你们中有多少人对垃圾收集感到兴奋?(很多参会者都举手了!)最近Ruby社区有一个帖子是关于如何通过修改RubyGC设置来提高单元测试速度的。这很棒!通过减少GC垃圾收集来加速测试是一件好事,但不知何故GC并没有真正让我兴奋。就像乍一看无聊的技术帖。事实上,垃圾回收是一门引人入胜的学科:垃圾回收算法不仅是计算机科学史上的重要组成部分,也是前沿研究课题。例如,MRIRuby解释器使用的“MarkSweep”算法已有50多年的历史。同时,Rubinius解释器中使用的垃圾收集算法是Ruby中的另一种实现。这个算法是2008年才开发出来的。但是,“垃圾收集”这个名字很不合适。应用程序的核心垃圾收集系统不仅仅是“回收垃圾”。其实它主要完成三个重要的任务:为新对象分配内存标记垃圾对象收集垃圾对象占用的内存或您的应用程序的智能。同样,你认为垃圾收集器会在身体的哪个部位?(我从会议听众那里得到了很多有趣的答案:肾脏、白细胞)我认为垃圾收集器是应用程序的核心。正如心脏为身体的其他部分提供血液和营养一样,垃圾收集器为程序提供内存和对象。如果你的心脏停止跳动,你活不了几秒钟。如果垃圾收集器停止运行或变慢,就像动脉阻塞一样,您的程序将变慢并死亡!一个简单的例子是通过例子检验理论的好方法。这是一个用Python和Ruby编写的简单类,我们可以将其用作一个简单示例:同时,我对这两种代码的相似程度感到惊讶:Python和Ruby在表达相同语义方面几乎没有什么区别。但是两种语言的内部实现是否相同?自由对象列表在上面的代码中,我们调用Node.new(1)后ruby会做什么呢?也就是说,Ruby是如何创建一个新对象的?令人惊讶的是,Ruby做的很少!事实上,在代码运行之前,Ruby解释器会预先创建数千个对象,并将它们放在一个链表中,这个链表被称为“自由对象列表”(freelist)。自由对象的链表(`freelist`)在概念上如下所示:每个白色方块都可以被认为是一个预先创建的、未使用的Ruby对象。当我们调用Node.new时,Ruby只是获取一个对象并将其引用返回给我们:在上图中,左侧的灰色方块表示我们代码使用的活动Ruby对象,而其余白色是未使用的对象块代码。(注:当然,该图是一个简化版的实现。实际上,Ruby会使用另一个对象来保存字符串“ABC”,第三个对象来保存Node的定义,其他对象来保存处理后的代码抽象语法编号“AST”,等等。)如果我们再次调用Node.new,Ruby只是返回对另一个对象的引用。JohnMcCarthy于1960年首次在Lisp中实现了垃圾收集。这种使用预先创建的对象的链表的简单算法是50多年前由传奇计算机科学家JohnMcCarthy发明的,他实现了它。原始的Lisp解释器。Lisp不仅是第一种函数式编程语言,而且包含了计算机科学领域的许多突破性进展。其中之一是通过垃圾收集进行自动内存管理。Ruby的标准版本,也称为“Matz的Ruby解释器”(MRI),使用类似于JohnMcCarthy于1960年实现的Lisp的垃圾收集算法。与Lisp一样,Ruby预先创建对象并在您执行操作时返回对对象的引用创建对象或值。Python中分配对象内存从上面我们可以看出,Ruby会预先创建对象,并将其保存在空闲对象列表(freelist)中。蟒蛇呢?当然,Python出于各种原因也使用了自由对象链表(它使用链表循环来确定对象),并且Python为对象和值分配内存的方式往往与Ruby不同。假设我们使用Python创建一个Node对象:Python不同于Ruby。当你创建一个对象时,Python会立即向操作系统申请内存。(Python实际上实现了自己的内存分配系统,它在操作系统内存堆上提供了另一层抽象,但今天没有事件可以深入研究它。)当我们创建第二个对象时,Python会再次向操作系统申请.更多内存:看起来很简单,当我们创建一个Python对象时,需要一个事件来分配内存。Ruby将无用的对象扔得到处都是,直到下一次垃圾收集通过#p#RubyDevelopers住在乱七八糟的房间回到Ruby,随着我们分配越来越多的对象,Ruby会继续为我们从中获取预先分配的对象自由对象列表(freelist)。因此,空闲对象列表将变得越来越短:或更短:请注意,我为n1分配了一个新值,而Ruby将保留旧值。“ABC”、“JKL”和“MNO”等节点对象仍将保留在内存中。Ruby不会立即清理旧对象,即使该程序不再使用!作为一名Ruby开发人员就像生活在一个凌乱的房间里,地板上到处都是衣服,厨房的水槽里是脏盘子。作为一名Ruby开发人员,您必须处理大量垃圾对象。当你的程序不再使用任何对象时,Python会立即清理。Python开发者住在整洁的房子里垃圾收集机制在Python和Ruby中有很大的不同,让我们回到前面三个Python中Node对象的例子:在内部,每当我们创建一个新对象时,Python都会持有一个C语言结构数字称为参考技术。最初,Python将其值设置为1。值为1表示每个对象都有指向它的指针或引用。假设我们创建了一个新对象,JKL:如前所述,Python将“JKL”的引用计数设置为1。还要注意我们将n1更改为指向“JKL”,不再引用“ABC”,并减少了引用计数的“ABC”为0。至此,Python垃圾收集器将立即执行!每当一个对象的引用计数变为0时,python将立即释放该对象并将其内存返回给操作系统。上图中,Python会回收“ABC”对象的内存。请记住,Ruby只会将旧对象留在那里,不会释放它们占用的内存。这种垃圾收集算法叫做“引用计数”,是乔治·柯林斯在1960年发明的。非常巧合的是,同年,约翰·麦卡锡叔叔发明了“自由对象链表算法”。正如MikeBernstein在Ruby大会上所说,“1960年是垃圾收集器的时代……”。作为一名Python开发人员就像住在一个整洁的房间里。你知道,你的室友有点爱干净,他会把你用过的所有东西都洗干净。你把脏盘子放好,脏玻璃杯一到水槽他就擦干净。现在再看一个例子,假设我们让n2和n1指向同一个节点:如上图左侧所示,Python减少了“DEF”的引用计数,并立即回收了“DEF”对象。同时可以看出,由于n1和n2都引用了“JKL”对象,所以它的引用计数变成了2。标记收集算法最终得到一个乱七八糟的房间,会堆积缓慢的垃圾,生活不能总是这样。一个Ruby程序运行一段时间后,空闲对象列表最终会被耗尽。上图中所有预分配的对象都用完了(方块都变灰了),链表上也没有对象可用了(没有留下白色方块)。这时,Ruby使用了一种由JohnMcCarthy发明的称为“标记回收”的算法。首先,Ruby会停止程序的执行,Ruby使用“停止世界,然后收集垃圾”的方法。Ruby然后扫描所有指向对象和值的指针或引用。同样,Ruby遍历虚拟机内部使用的指针。它标记了指针可以到达的每个对象。在下图中,我用“M”来指出这些标记:上面标有三个“M”标记的对象是活动对象,仍然被我们的程序使用。在Ruby解释器内部,通常使用“空闲位图”数据结构来存储一个对象是否被标记:Ruby将“空闲位图”存储在一个单独的内存区域,以更好地利用Unix的“写时复制”特性。更详细的信息可以参考我的另一篇文章《为什么Ruby2.0的垃圾回收器让我们如此兴奋》。如果活动对象被标记,那么其余的就是垃圾对象,这意味着它们不再被代码使用。在下图中,我用白色的方块来表示垃圾对象:接下来,Ruby会清理未使用的、垃圾对象,并将它们链接到空闲对象列表(freelist):在解释器内部,这个过程非常快,而Ruby确实not对象实际上并没有从一个地方复制到另一个地方。相反,Ruby会把垃圾对象组成一个新的链表,链接到空闲对象链表(freelist)中。现在,当我们想要创建一个新的Ruby对象时,Ruby会返回给我们垃圾回收的对象。在Ruby中,对象可以重生,享受多重生命!MarkCollectionAlgorithmvs.ReferenceCountingAlgorithm乍一看,Python的垃圾收集算法对Ruby来说相当令人吃惊:既然可以住在整洁干净的房间里,为什么还要住在凌乱的房间里呢?为什么Ruby不使用Python的算法而是周期性的强行停止程序的运行来清理垃圾?然而,实现引用计数并不像看起来那么简单。下面是很多语言不愿意像Python那样使用引用计数算法的一些原因:首先,实现难度大。Python必须为每个对象留出一定的空间来保存引用计数。这会产生一些轻微的内存开销。但更糟糕的是,更改变量或引用等简单操作会导致复杂操作,因为Python需要增加一个对象的计数,减少另一个对象的计数,并可能释放一个对象。其次,它会减慢速度。虽然Python的垃圾回收过程在程序执行过程中非常流畅(当你把脏盘子放入水槽时,它会立即被清洗),但它运行起来并不是很快。Python总是在更新引用计数。当你停止使用一个巨大的数据结构时,比如一个包含很多元素的序列,Python必须一次释放很多对象。减少引用计数可能是一个复杂的递归过程。***,它并不总是很好用。正如您将在我演讲的下一部分,下一篇文章中看到的,引用计数无法处理包含循环引用的循环引用数据结构。下次……下周我将发布剩余的演讲内容。我将讨论Python如何处理循环引用数据结构,以及垃圾收集器如何在即将推出的Ruby2.1中工作。原文链接:PatShaughnessy翻译:伯乐在线-geekerzp翻译链接:http://blog.jobbole.com/60900/