「Ruby有析构函数吗? 」一个问题引发的思考

Published on:

昨天,在QQ群里闲聊,有人扯出一个问题:Ruby有析构函数吗?

  • 我: 没有。
  • 他: C++里有啊。
  • 我: C++有GC吗?
  • 他: C++没有自带GC,但是有库。
  • 我: 那你用这个库吗?
  • 他: 不常用。。
  • 我: 那不就对了嘛, C++里原本没有实现GC,所以需要在对象死亡的时候用析构函数去释放空间,这样避免内存泄漏。但是Ruby里有GC的。
  • 他: 那为什么Python里也有析构函数?Python也有自己的GC呀。
  • 我: 。。。

我曾暂时无语。但是这个问题引起了我的兴趣,是啊,为什么Python有呢?

这个世界上任何事物的存在都有其合理性。 更何况,Python之父不会无缘无故的加上析构函数。 Python有GC, 对象在被GC回收的时候,执行析构函数。而Ruby也有GC,可是为什么没有呢? 难道是因为Python GC和Ruby GC的算法不同吗?

Ruby GC,每个Rubyist都知道, 从1.8时代的标记清除方式,到2.1的分代GC算法,GC的清除都不是实时性的,要预测一个对象什么时候释放,是比较困难的。 所以,只能是定时的去执行GC,或者设置阀值,大于xxMB就会执行一次GC, 在一些框架里,比如Goliath,默认会设置为每100个请求执行一次GC。

但是Python,是利用了引用计数算法的GC, 引用计数算法的一个优点,就是对象在引用数为0的时候,就马上就被释放。但是引用计数也有缺点,比如循环引用的问题,这会产生bug。所以,为了避免这种缺陷,Python也引入了分代回收算法,来检测有循环引用的对象,然后把这些对象的引用设置为0, 我们刚才说了,当引用数为0的时候, 对象会马上被释放。 所以,正是因为Python使用了引用计数算法,所以才能提供一个析构函数供程序员使用,在对象被清理的时候做一些工作。 当然这个跟C++还是有区别的,在Python里,使用了内存对象池技术,当你调用析构函数的时候,只是把对象归还到内存池中,而不是马上释放给系统内存。当然,Ruby同样也使用了内存池技术。

这个问题,至此,有了一个相对比较圆满的答案,解释了,为什么Python有析构函数,而Ruby没有。

问题还没有完

这个时候,另外一个人说了: Ruby是有析构函数的,不信你去看看ObjectSpace.define_finalizer方法。

ObjectSpace.define_finalizer说明:
Adds aProc as a finalizer, to be called after obj was destroyed.

也就是说, 这个方法会在对象被销毁以后执行一个指定的Proc。

比如这样用:

class Devil::Image
  attr_reader :name, :file

  def initialize(name, file)
    @name = name
    @file = file

    ObjectSpace.define_finalizer( self, proc { IL.DeleteImages(1, [name]) } )
  end
end

这段代码是一个开源gem:Devil里的,使用了ObjectSpace.define_finalizer,在image对象被回收的时候,删除掉相应的图片。

但是这里有个bug,会引起内存泄漏:

self,会被proc引用,所以,会导致image对象永远无法被回收,因为这个对象一直被proc所引用。
这个问题其实是proc闭包引起的问题。

所以,如果要使用它的话,一定要小心:

def initialize(name, file)
  @name = name
  @file = file

  ObjectSpace.define_finalizer( self, self.class.finalize(name) )
end

def self.finalize(name)
  proc { IL.DeleteImages(1, [name]) }
end

我们用类方法来避免上面所说的内存泄漏问题。 但是,ObjectSpace.define_finalizer是个出了名的难用的方法,「出了名难用」这个词,不是我说的,虽然我是刚知道这个方法的存在,但是在我查找一些资料的时候,看到别人说的。

严格意义来说,ObjectSpace.define_finalizer不是一个析构函数

我们回到传统的析构函数的行为来看, ObjectSpace.define_finalizer方法不算一个真正意义的析构函数。 且不说C++,拿Python来说,Python里的析构函数,不光是在对象被del的时候执行, 在对象跳出作用域外(object goes out of scope)也会被执行。 那么「啥叫对象跳出作用域外」?

看这个例子:

if true
   a = Array.new[1,2,3]
   b = a
end

上面的a,就是一个跳出作用域外的对象。 而对于这个对象a,只能等着GC回收了。而不会马上就调用ObjectSpace.define_finalizer。

Rails框架中的运用

Rails中,只在actionview/lib/action_view/template.rb 使用过这个方法。

# Among other things, this method is responsible for properly setting

# the encoding of the compiled template.

#

# If the template engine handles encodings, we send the encoded

# String to the engine without further processing. This allows

# the template engine to support additional mechanisms for

# specifying the encoding. For instance, ERB supports <%# encoding: %>

#

# Otherwise, after we figure out the correct encoding, we then

# encode the source into <tt>Encoding.default_internal</tt>.

# In general, this means that templates will be UTF-8 inside of Rails,

# regardless of the original source encoding.

def compile(mod) #:nodoc:

  encode!
  ...
  ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
end

...
  
# This finalizer is needed (and exactly with a proc inside another proc)

# otherwise templates leak in development.

Finalizer = proc do |method_name, mod| # :nodoc:

  proc do
    mod.module_eval do
      remove_possible_method method_name
    end
  end
end

用在这里主要是为了给模板进行编码,编码完毕之后,执行相应的代码防止在开发环境下templates内存泄漏。

仅此一处使用了ObjectSpace.define_finalizer。

总结

  1. Python之所以有完整的析构函数,是由其设计思想决定的,比如GC使用了引用计数算法。引用计数算法的优点就是当引用数为0的时候, 对象会马上被释放。
  2. Ruby里有析构函数: ObjectSpace.define_finalizer,但是这个析构函数在对象跳出作用域外是不会被执行的,只能等着GC来回收对象时候才能被执行。
  3. ObjectSpace.define_finalizer在使用的时候如果不小心,会引入一些bug,比如上面例子中的导致内存泄漏。 所以Rubyist一般不太常用这个方法。整个Rails框架中,仅有一处使用了这个方法。

Comments

comments powered by Disqus