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

Ami*_*itA 29 ruby garbage-collection ruby-on-rails-3.1 ruby-1.9.3

我有一个简单的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!
Run Code Online (Sandbox Code Playgroud)

现在我做以下事情:

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!
Run Code Online (Sandbox Code Playgroud)

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

x.keys.each{|k| x[k]=nil}
x = nil
GC.start
Run Code Online (Sandbox Code Playgroud)

它会正确地从内存中删除所有的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
Run Code Online (Sandbox Code Playgroud)

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

Mass.index(ActiveRecord::Relation)["ActiveRecord::Relation"].each{|x| Mass.detach Mass[x]}
GC.start
Run Code Online (Sandbox Code Playgroud)

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

Osc*_*Ben 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
Run Code Online (Sandbox Code Playgroud)

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

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

更新2:

我发现了什么是保持对象的引用.为此,我使用了红宝石质量宝石.事实证明,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
Run Code Online (Sandbox Code Playgroud)

不幸的是,呼吁reset这种关系似乎没有帮助,但希望现在这已经足够了.