29

我有一个简单的 ActiveRecord 模型Student,表中有 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。

更新(已解决)

根据下面 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。奇怪)。

4

2 回答 2

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遍历当前范围内的所有对象并标记它认为仍在使用的对象,然后遍历堆中的所有对象并释放尚未标记的对象。

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

更新 2:

我找到了保留对象引用的内容。为此,我使用了ruby​​ mass gem。事实证明,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该关系似乎没有帮助,但希望现在这些信息已经足够了。

于 2012-06-26T02:00:08.433 回答
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

于 2012-06-24T11:37:11.697 回答