ObjectSpace.reachable_objects_from()の活用の続き - GC Advent Calendar

Garbage Collection Advent Calendarの3日目の記事です。

2日目の@nari3さんの記事を読んで、これまだ続きが書けるよとtwitterでつぶやいたら書くことになってしまいました。口は災いの元。

さて、@nari3さんの記事ですが面白いですね。メモリリークしたところが分かって便利ですね。でも、ちょっと考えてみてください。はたしてメモリリークしたオブジェクトに都合よくどこに所属しているか書いてあるでしょうか?そういうことで、どこの変数に格納されたオブジェクトかも探すようにしました。

これです。

#!ruby -W0
require 'objspace'

def find_pilferer(leak_id, bind)
  eval("local_variables", bind).each do |nm|
    val = eval(nm.to_s, bind)
    reaches = ObjectSpace.reachable_objects_from(val)
    if (reaches and reaches.map(&:object_id).include?(leak_id)) or
        val.object_id == leak_id then
      p [nm, val]
    end
  end
  
  global_variables.each do |nm|
    val = eval(nm.to_s)
    reaches = ObjectSpace.reachable_objects_from(val)
    if (reaches and reaches.map(&:object_id).include?(leak_id)) or
        val.object_id == leak_id then
      p [nm, val]
    end
  end
  
  ObjectSpace.each_object do |o|
    o.instance_variables.each do |nm|
      val = o.instance_variable_get(nm)
      reaches = ObjectSpace.reachable_objects_from(val)
      if (reaches and reaches.map(&:object_id).include?(leak_id)) or
        val.object_id == leak_id then
        p [o.object_id, nm, val]
      end
    end

    if o.is_a?(Module) then
      o.class_variables.each do |nm|
        val = o.class_variable_get(nm)
        reaches = ObjectSpace.reachable_objects_from(val)
        if (reaches and reaches.map(&:object_id).include?(leak_id)) or
            val.object_id == leak_id then
          p [o.object_id, nm, val]
        end
      end
    end
  end
end

# Test
def foo
  @bar = ["I am bar"]
  @baz = ["I am baz"]
  leak_obj = "I am leaky..(T_T)"

  @bar << leak_obj
  @baz << leak_obj
  return leak_obj.object_id
end

leak_id = foo
@baz.pop
a = [[]]
b = a
a_id  = a[0].object_id
a = nil

find_pilferer(leak_id, binding)
find_pilferer(a_id, binding)

class Foo
  @@h = {:a => []}
  h_id = @@h[:a].object_id
  @@a = @@h
  @@h = nil
  find_pilferer(h_id, binding)
end

実行するとこんな感じです

[268671270, :@bar, ["I am bar", "I am leaky..(T_T)"]]
[:b, [[]]]
[269218790, :@@a, {:a=>[]}]

さすがにレシーバーが分かりませんが、インスタンス変数とかローカル変数とかの名前はわかります。find_pilfererに探したいobject idとbindingを渡すと変数名も探してくれます。

1行目に-W0なんて物騒なオプションが付いてますがこれが無いとグローバル変数を探すときに五月蠅いので黙らせてます。

いやー、GC Advent CalendarというよりRuby黒魔術Calendarって感じですが、マイルドです。マイルドな分制限が多くて、明示的にbindingを渡さないといけないところはその1つです。あと、callerのメソッドのローカル変数に格納されていると探せないとかクロージャーで閉じ込められた変数だとだめかもしれないとかいろいろあります。
これらの制限は多分乗り越えられるのですが、真っ黒な魔術を使うことになるのでこの辺でやめておきます。
それでは、他人のふんどしで相撲を取るmiura1729でした。