散列中的ActiveRecord对象不是垃圾收集 - 一个错误或一种缓存功能?

时间:2012-06-22 03:35:35

标签: ruby ruby-on-rails-3.1 garbage-collection ruby-1.9.3

我有一个名为Student的简单ActiveRecord模型,表中有100条记录。我在rails控制台会话中执行以下操作:

ObjectSpace.each_object(ActiveRecord::Base).count
# => 0

x = Student.all

ObjectSpace.each_object(ActiveRecord::Base).count
# => 100

x = nil
GC.start

ObjectSpace.each_object(ActiveRecord::Base).count
# => 0     # Good!

现在我执行以下操作:

ObjectSpace.each_object(ActiveRecord::Base).count
# => 0

x = Student.all.group_by(&:last_name)

ObjectSpace.each_object(ActiveRecord::Base).count
# => 100

x = nil
GC.start

ObjectSpace.each_object(ActiveRecord::Base).count
# => 100     # Bad!

任何人都可以解释为什么会发生这种情况以及是否有一种聪明的方法可以在不知道底层哈希结构的情况下解决我知道我可以这样做:

x.keys.each{|k| x[k]=nil}
x = nil
GC.start

并且它将正确地从内存中删除所有Student对象,但我想知道是否存在一般解决方案(我的现实问题是广泛传播并且具有比上面显示的散列更复杂的数据结构)。

我正在使用Ruby 1.9.3-p0和Rails 3.1.0。

更新(已解决)

Per Oscar Del Ben在下面的解释中,在有问题的代码片段中创建了一些ActiveRecord :: Relation对象(它们实际上是在两个代码片段中创建的,但由于某种原因,它们仅在第二个代码片段中“行为异常”。阐明原因?)。它们通过名为@records的实例变量维护对ActiveRecord对象的引用。可以通过ActiveRecord :: Relation上的“reset”方法将此实例变量设置为nil。您必须确保在所有关系对象上执行此操作:

ObjectSpace.each_object(ActiveRecord::Base).count
# => 100

ObjectSpace.each_object(ActiveRecord::Relation).each(&:reset)

GC.start
ObjectSpace.each_object(ActiveRecord::Base).count
# => 0

注意:你也可以使用Mass.detach(使用ruby-mass gem Oscar Del Ben引用),虽然它会比上面的代码慢得多。请注意,上面的代码不会从内存中删除一些ActiveRecord :: Relation对象。但这些似乎相当微不足道。您可以尝试:

Mass.index(ActiveRecord::Relation)["ActiveRecord::Relation"].each{|x| Mass.detach Mass[x]}
GC.start

这将删除一些ActiveRecord :: Relation对象,但不是所有这些对象(不知道为什么,剩下的那些没有Mass.references。很奇怪)。

2 个答案:

答案 0 :(得分:11)

我想我知道发生了什么。 Ruby的GC不会释放不可变的对象(比如符号!)。 group_by返回的键是不可变的字符串,因此它们不会被垃圾回收。

<强>更新

似乎问题不在于Rails本身。我尝试单独使用group_by,而有时这些对象不会被垃圾收集:

oscardelben~/% irb
irb(main):001:0> class Foo
irb(main):002:1> end
=> nil
irb(main):003:0> {"1" => Foo.new, "2" => Foo.new}
=> {"1"=>#<Foo:0x007f9efd8072a0>, "2"=>#<Foo:0x007f9efd807250>}
irb(main):004:0> ObjectSpace.each_object(Foo).count
=> 2
irb(main):005:0> GC.start
=> nil
irb(main):006:0> ObjectSpace.each_object(Foo).count
=> 0
irb(main):007:0> {"1" => Foo.new, "2" => Foo.new}.group_by
=> #<Enumerator: {"1"=>#<Foo:0x007f9efb83d0c8>, "2"=>#<Foo:0x007f9efb83d078>}:group_by>
irb(main):008:0> GC.start
=> nil
irb(main):009:0> ObjectSpace.each_object(Foo).count
=> 2 # Not garbage collected
irb(main):010:0> GC.start
=> nil
irb(main):011:0> ObjectSpace.each_object(Foo).count
=> 0 # Garbage collected

我已经深入了解了GC的内部结构(这非常容易理解),这似乎是一个范围问题。 Ruby遍历当前作用域中的所有对象并标记它认为仍在使用的对象,之后它通过 all 堆中的对象并释放那些 not的对象已被标记。

在这种情况下,我认为哈希仍然被标记,即使它超出了范围。造成这种情况的原因有很多。我会继续调查。

更新2:

我发现了保留对象引用的内容。为此,我使用了ruby mass宝石。事实证明,Active Record关系会跟踪返回的对象。

User.limit(1).group_by(&:name)
GC.start
ObjectSpace.each_object(ActiveRecord::Base).each do |obj|
  p Mass.references obj # {"ActiveRecord::Relation#70247565268860"=>["@records"]}
end

不幸的是,在关系上调用reset似乎没有帮助,但希望现在这已经足够了。

答案 1 :(得分:2)

我不知道答案

但我尝试检查http://blog.headius.com/2010/07/browsing-memory-jruby-way.html

上给出的堆

已附上截屏,https://skitch.com/deepak_kannan/en3dg/java-visualvm 这是一个简单的程序

class Foo; end
f1 = Foo.new
f2 = Foo.new
GC.start

然后使用上面给出的jvisualvm。在irb中运行这个。
好像jruby正在跟踪对象的范围。如果存在对该对象的任何非弱引用,则该对象将不会获得GC