【Effective Java】Ch 2_破棄オブジェクトの作成:Item 6_不要なオブジェクト参照の消去

4383 ワード

メモリを手作業で管理する言語(C、C++)から自動ゴミ回収の言語に移行すると、プログラミング作業がより楽になります.オブジェクトが切れたら自動回収されるからです.自動ゴミ回収を初めて経験すると不思議に思います.メモリ管理を考慮する必要はありません.そうではありません.
【例】以下の簡単なstack実装を考える.
// Can you spot the memory leak?
public class Stack{
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack(){
        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(elemnts, 2 * size + 1);
    }
 }
このプログラムには明らかなエラーはありません(彼の汎用バージョンではItem 26が表示されます).あらゆる方法を尽くしてテストすると、各テストに成功しますが、問題が隠されています.厳密には、このプログラムは「メモリ漏洩」します.ごみ回収器のアクティビティが増加したり、メモリの消費量が増加したりするにつれて、プログラムのパフォーマンスはますます低下します.極端な場合、このメモリの漏洩はディスク交換を引き起こし、プログラムOutOfMemoryErrorエラーを引き起こすこともありますが、この失敗は珍しいです.
では、プログラムのどこでメモリ漏れが発生したのでしょうか.スタックが先に成長し、次に収縮すると、ポップアップされたスタックのオブジェクトはゴミに回収されず、タイムリーなプログラムにはこれらのオブジェクトへの参照はありません.スタック自体のメンテナは、これらのオブジェクトを指す不要な参照(obsolete reference)を指すためです.不要な参照とは、二度と使用されない参照を指します.この例では、配列の「アクティブ部分」以外のすべての参照は不要で、アクティブ部分はsizeより下付きの要素を指します.
 
ごみ回収をサポートする言語では、メモリオーバーフロー(無意識のオブジェクト保持と呼ばれる)もっと似合う)は隠れています.オブジェクトリファレンスが無意識に保持されている場合、オブジェクトはゴミ回収されないだけでなく、対応するリファレンスの他のオブジェクトもゴミ回収されません.わずかなオブジェクト参照だけが無意識に保持されていても、多くのオブジェクトがゴミ回収メカニズムから除外され、パフォーマンスに潜在的な大きな影響を及ぼします.
この問題を解決する方法は簡単です.参照が無駄になったとき、nullにします.上記のStackクラスでは、popメソッドの正しいバージョンは次のとおりです.
public Object pop(){
    if(size == 0) throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // Eliminate obsolete reference
    return result;
}
のもう1つの利点は、不要なオブジェクトが誤って使用されると、プログラムが静かに誤って実行するのではなく、すぐにNull PointerExceptionを投げ出すことです.プログラムエラーをできるだけ早く検出することは常に有益である.
しかし、プログラマーは初めてこのような問題に悩まされた後、プログラムがオブジェクトの参照を使い切るたびにnullになるかもしれません.これは必要でもないし、適切でもないので、プログラムをめちゃくちゃにします.オブジェクトリファレンスをnullに設定するのは、通常の動作ではなく例外です.不要なオブジェクト参照を除去する最善の方法は、参照を含む変数を役割ドメインの範囲外にすることです.各変数が最もコンパクトな役割ドメイン内に定義されている場合(Item 45)、このような状況は自然に発生します.
では、リファレンスを空にする必要があるのはいつですか?Stackクラスのどのような特徴がメモリの漏洩を可能にしますか?簡単に言えば、クラスが独自にメモリを管理する場合は、不要な参照を空にする必要があります.【例】Stackのストレージプールはelements配列を含む(オブジェクト自体ではなくObject reference cells)の要素です.配列内のアクティブな部分の要素は割り当てられています.配列内の他の部分は自由です.ゴミ回収器はそれを知りません.ゴミ回収器ではelements配列内のすべてのオブジェクトの使用は同等に有効です.配列の非アクティブな部分をプログラマだけが知っていることは重要ではありません.手順シーケンスは、配列要素が非アクティブになるとnullに手動で設定することをゴミ回収器に伝えることができます.
一般的に、クラスが独自のメモリを管理している限り、メモリ漏洩の問題を警戒する必要があります.要素が解放されると、要素に含まれるオブジェクト参照はnullに設定されます.
メモリ漏洩のもう一つの一般的なソースはキャッシュです.オブジェクトをキャッシュに参照すると、オブジェクトが役に立たなくなるまでキャッシュに残ることを忘れがちになります.この問題にはいくつかの解決策がある.このようなキャッシュを実現するには、キャッシュの外にエンティティを参照するキーがあれば、そのエンティティに意味があると判断する場合は、キャッシュをWeakHashMapと定義するだけでよい.エンティティが不要になると、自動的にMapから削除されます.WeakHashMapは、キャッシュされたエンティティのライフサイクルが、値の外部参照ではなくキーの外部参照によって決定される場合にのみ使用できることを覚えておいてください.
より一般的な状況は、キャッシュ・エンティティのライフサイクルが容易に決定されず、時間が経つにつれてエンティティの価値が低下することです.この場合、キャッシュは不定期に不要なエンティティをクリーンアップする必要があります.バックグラウンド・スレッドでクリーンアップすることもできます(TimerまたはS h e d u l edThreadPoolExecutorの場合もあります).また、キャッシュに新しいエンティティを追加するときにクリーンアップすることもできます.【例】LikedHashMapは、removeEldestEntryを使用して後者のスキームを実装します.より複雑な保存の場合はjava.lang.refを直接使用することができます.
 public class LinkedHashMap...{
    void addEntry(int hash, K key, V value, int bucketIndex) {
        createEntry(hash, key, value, bucketIndex);

        // Remove eldest entry if instructed, else grow capacity if appropriate
        Entry<K,V> eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        } else {
            if (size >= threshold)
                resize(2 * table.length);
        }
    }
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }
}

-LinkedHashMapを継承し、removeEldestEntryメソッドを上書きできます.
  
メモリ漏洩の3番目の一般的なソースは、リスナーとその他のコールバックです.APIを実装し、クライアントがコールバックを登録しているが、明示的に登録をキャンセルしていない場合は、いくつかのアクションを取らない限り、これらのコールバックが蓄積されます.コールバックが即時にゴミ回収されることを保証する最善の方法は、値が弱い参照を保存することであり、例えば、WeakHashMapのキーとして保存することである.
メモリの漏洩は通常、明らかな失敗を示さないため、システムに何年も存在する可能性があります.これらの問題は、注意深いコードチェック、またはデバッグツール(heap profiler)の助けを借りてのみ発見できます.そのため、メモリ漏洩が発生する前にこのような問題を予測して、発生しないようにするのは素晴らしいです.