Rubyでオブジェクトのメモリが解放されるところを見る

Tsukasa OISHI

RubyにはObjectSpaceというモジュールがあって、Heap内のオブジェクトを見たりすることができます。デバッグ目的で使うものだと思いますが、過去にとあるライブラリの中で使われていてRubyがsegmentation faultで落ちたりして痛い目を見たことがあります。怖いやつです。

たとえばObjectSpace.count_objectsはHeap内のオブジェクトを種類ごとにカウントしてHashで返してくれます。

def diff_object
  GC.disable
  old = ObjectSpace.count_objects
  yield if block_given?
  ObjectSpace.count_objects.each do |k,v|
    before = old[k] || 0
    diff = v - before
    puts "[#{k}] #{before} -> #{v} (#{diff})" if diff != 0
  end
  GC.enable
  puts "---"
end

ブロックを渡すとそのブロック内の処理でどのくらいオブジェクトに変化があったのかがわかります。

class Tsu
end

diff_object
diff_object
diff_object do
  100.times { Tsu.new }
end

の結果は

[FREE] 19855 -> 19827 (-28)
[T_STRING] 4687 -> 4714 (27)
[T_HASH] 31 -> 32 (1)
---
[FREE] 19825 -> 19824 (-1)
[T_HASH] 32 -> 33 (1)
---
[FREE] 19826 -> 19725 (-101)
[T_OBJECT] 42 -> 142 (100)
[T_HASH] 31 -> 32 (1)
---

Hashがひとつ増えているのは、ObjectSpace.count_objectsの返り値のHashだと思いますが、最初のブロックなしでの計測でStringオブジェクトが27増えているのはなんでしょうね。

どうして急にObjectSpaceの話をしだしたのかというと、とある処理でメモリが肥大化してしまう現象があって、GCが動いてるのかを調べたくなったからなのです。とある処理ってActiveRecord::Baseオブジェクトまわりなのですが、たとえばActiveRecord::Base.cacheブロック内で処理していると、オブジェクト自体がキャッシュとして保持されるのでGCの対象から外されているのではないかと疑ったりしたわけです。

オブジェクトが解放されたかどうかは、ObjectSpace.define_finalizerを使って調べることができます。

module GcChecker
  def self.included(base)
    base.extend ClassMethods
    base.after_initialize :gc_hook
  end

  module ClassMethods
    def finalize
      proc { Rails.logger.info "#{name} gc!" }
    end                                                                                               
  end

  def gc_hook                                                                                         
    ObjectSpace.define_finalizer(self, self.class.finalize)                                           
  end                                                                                                 
end

このブログのArticleモデルに組み込んでみます。

class Article < ActiveRecord::Base
  include GcChecker
  ...
end

でいろいろ調べてみたのですが、まだ現象の原因はよくわかっていません。まだまだ修行が足りません。