Python の GC のデバッグ機能

Python のデーモン型のプログラム(具体的に言うと Loggerhead という bzr のリポジトリブラウザ)が大量(具体的に言うと100MB以上)メモリを食っていたので、それを調べた時のメモ。

まず、本当にそれだけのメモリを利用しているのかどうかを meliae というメモリプロファイラで調べたんだけど、18MBくらいしか使ってなかった。 meliae の使い方はまた今度に回すとして、今回はそんなにメモリを使うはずがないのにメモリを使ってしまっているケースについて。

この場合は、循環参照を大量に作ることによってメモリブロックがたくさん確保され、循環参照コレクタが実行された後も(メモリブロック内に1つでも生き残っているオブジェクトがいるなどの理由で)OSにメモリブロックが返されないという原因が考えられる。(循環参照以外の原因でメモリが大量に確保されてる可能性もあるんだけどね)


今日の話は、その循環参照の見つけ方。

とりあえず、 gc モジュールのドキュメントを読むこと。短いのですぐ読める。

gc.set_debug(gc.DEBUG_LEAK) が標準的な方法なんだけど、 list とか dict とかが大量に表示されてウザい場合は、自分で表示するオブジェクトをフィルタリングしてやる。次のコードでは、5秒ごとにGCを実行して、そこで回収されたオブジェクトのうち list, str, dict を除いたオブジェクトを表示してから開放している。

def gc_thread():
    while True:
        time.sleep(5)
        gc.collect()
        for g in gc.garbage:
            if isinstance(g, list) or isinstance(g, basestring) or isinstance(g, dict):
                continue
            try:
                print g
            except Exception:
                pass
        g = None
        del gc.garbage[:]

gc.set_debug(gc.DEBUG_LEAK)
th = threading.Thread(target=gc_thread)
th.daemon=True
th.start()

大抵の場合、 list,dict 以外のオブジェクトが循環参照のどこかに挟まっているので、そこから循環参照の原因を探るのが早い。

list, dict だけで循環参照が構築されている場合は、まあ、なんか頑張る。

循環参照の原因を見つけて、それが短期間のうちに何度も生成してほったらかしにされている類のものだった場合は、要らなくなったタイミングで循環参照を手動で切断してやれば、メモリ使用量を削減できるかもしれない。

小さい循環参照がたくさんできるのではなく、大きな循環参照がときどきできるだけなら、手動で頑張ってもあんまりメモリ使用量は削減できない。これはそういうプログラムなんだと諦める。

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。