メモリリークを回避するため廃れたオブジェクトの参照は取り除かなければならない。
C や C++ から、ガーベージコレクション(GC)を持つ言語に切り替えると、メモリ管理について考える必要がないように感じることがあるが、それは間違いだ。
次のスタック実装はメモリリークしている。
public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack () { this.elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push (Object e) { ensureCapacity(); elements[size++] = e; } public Object pop () { if (size == 0) { throw new EmptyStackException(); } return elements[size--]; } private void ensureCapacity() { if (elements.length == size) { elements = Arrays.copyOf(elements, 2 * size + 1); } } }
この実装では、スタックに要素をプッシュし、その後にポップしたあと、スタックから取り出されたオブジェクトは決して GC されない。 なぜならこのスタック実装では、ポップした後もそのオブジェクトに対する廃れた参照を保持しているからだ。
廃れた参照
廃れた参照は、それを通してオブジェクトが参照されることない参照のことである。
上述のスタック実装では、ポップされた後の size 以上に位置する要素は二度と再利用されない。ポップした要素を利用するクライアント側で参照を破棄していても、elements 要素からそのオブジェクトへの参照が残っているので GC されない。
GC される言語でメモリリークが発生した場合、参照されないオブジェクト自身がメモリ上に残るだけでなく、その参照されていないオブジェクトが参照しているオブジェクトも回収されないので影響は甚大である。
null を設定する
この問題に関する正しい対策は廃れた参照を取り除くため、適切に null を設定すること。
public Object pop () { if (size == 0) { throw new EmptyStackException(); } Object result = elements[size--]; elements[size] = null; // 廃れた参照を取り除く return result; }
このようにすると、スタックを実装している配列からの参照が取り除かれるので、クライアント側で参照がなくなると適切に GC されるようになる。 加えて、実装ミスで size 以上にアクセスした場合に NullPointerException が発生するようになり、バグを明示的にするプラスの効果もある。
nullを設定するべき時
オブジェクトに null を設定することは普通と言うよりはむしろ例外であるべきだ。null を設定するべきなのは以下のような場合。
- 独自にメモリを管理するような機構をもったクラス
- キャッシュ機構を持つクラス
- リスナーやコールバックをもつクラス
オブジェクト参照を一旦キャッシュに保持すると、オブジェクトへの参照がそこにあることを忘れがちになる。そこで、弱い参照をもつ WeakHashMap で保持することが推奨されている。 加えて、一般的には古くて利用しなくなったキャッシュは、それを破棄する機構をいれるべきである。
コールバックやリスナーも同様で、クライアントが削除を忘れた場合にはリークしてしまうため、同じく WeakHashMap で保持するべき。
まとめ
メモリリークはそれ自身が明らかなエラーとして現れない。注意深いコードレビュー、またはヒーププロファイラというデバッグツールで発見できる場合がある。ただ、理想的には事前にメモリリークを予想することを学び、それが発生する前に防ぐべきである。
感想
参照系の話はどこかでちゃんと勉強したいなー。どこかにいいかんじでまとまってないだろうか。