JavaやC#でもメモリリーク

ふだんC/C++をメインに使っているので、JavaC#みたいなGCを備えた言語は便利だなーと感じます。たとえば、関数内で生成したオブジェクトを関数外に返すなんてことは、C++だときちんと後始末されるか非常に神経質になります。C++はnewせずにスタック上にオブジェクトを生成することもできるので、そんなオブジェクトをうっかり関数外に返してしまうととんでもないバグになります。その点、オブジェクトをすべて動的に生成したうえで不要になったらGCが自動的に始末してくれるマネージドな言語は、安全かつ簡便です。


さりとて、JavaC#だってメモリリークと無縁というわけではありません。ではなぜGCがあるにもかかわらずメモリリークが起こるのでしょうか? GCは不要になったオブジェクトの後始末をしてメモリを回収しますが、ここでいう「不要になったら」とは「誰からも参照されなくなったら」という意味です。たとえ実質的に使われなくなったオブジェクトであっても、1カ所からでも参照が残っていたらGCには回収されません。つまり、不要になったオブジェクトへの参照の解除忘れがメモリリークを招くのです。


もっともよくあるパターンはイベント購読の解除忘れでしょう。イベントリスナーは、JavaならaddListener、C#なら+=でイベント源に登録しますが、 イベントリスナーのオブジェクトが不要になったとき、イベント源に登録の解除(JavaならremoveListener、C#なら-= )をしなければいけません。これを忘れるとイベント源はいつまでもイベントリスナーへの参照を持つことになり、イベントリスナーはGCに回収されなくなります。つまりメモリリークです。


より一般的にいうと、よそのコンテナ(キュー、リスト、動的配列、連想配列など)にオブジェクトをつっこんだまま、削除を忘れるとメモリリークになります。前述のイベントリスナーもその一例といえるでしょう。コンテナ自体が短命ならばまだしも軽傷で、コンテナが消滅すれば晴れて一切の参照を失ったオブジェクトたちもGCの回収対象になります。しかし実際には、この手のコンテナは往々にして長命ないし不滅なのです。


他のオブジェクトをコンテナに入れて管理するような、システム側寄りのオブジェクトが長命なのは当然といえるでしょう。長命の最たるものはクラスの静的メンバであるオブジェクトです。クラスは不滅なので、クラスに参照されているオブジェクトもまた不滅となります。

C#で、あるクラスのオブジェクトがリークしてるかどうかは、デストラクタ(ファイナライザ)を実装してメッセージを出力するようにして、GC.Collect()で強制的にガベージコレクションをさせてみると確認できますね。

C#で、アンマネージドリソースを使った場合は、当然ながらGCの対象外なので自己管理が必要ですが、それはまた別の話。