JPCERT コーディネーションセンター

MSC04-J. メモリリークしない

プログラムの実行には関係なくなったオブジェクトのガベージコレクションが、プログラミング上のミスにより妨げられることがある。ガベージコレクタは到達不能になったオブジェクトのみを回収するものであり、不要になったオブジェクトが到達可能なまま残されているのはメモリ管理の不手際を意味している。ヒープメモリを消費し尽すと OutOfMemoryError が発生し、通常、プログラムは強制終了させられる。

多量のメモリリークはメモリ不足を招きサービス運用妨害(DoS)となるため、避けなければならない。詳しくは「MSC05-J. ヒープメモリを使い果たさない」を参照のこと。

違反コード (オフバイワンプログラムエラー)

このコードでは、vector オブジェクトでメモリリークが発生する。vector の要素を解放する条件が、n >= 0 とするべきところ、n > 0 となっている。そのため、メソッドの呼び出しごとに vector の要素を1つ、削除しないまま残してしまい、ヒープメモリを使い果してしまう。

public class Leak {
  static Vector vector = new Vector();

  public void useVector(int count) { 	
    for (int n = 0; n < count; n++) {
      vector.add(Integer.toString(n));
    }
    // ...
    for (int n = count - 1; n > 0; n--) { // メモリを解放
      vector.removeElementAt(n);
    }	
  }

  public static void main(String[] args) throws IOException {
    Leak le = new Leak();
    int i = 1;
    while (true) {
      System.out.println("Iteration: " + i);
      le.useVector(1);
      i++;
    }
  }
}
適合コード (>=)

以下のコードでは、ループの条件を n >= 0 に修正している。

public void useVector(int count) { 	
  for (int n = 0; n < count; n++) {
    vector.add(Integer.toString(n));
  }
  // ...
  for (int n = count - 1; n >= 0; n--) {
    vector.removeElementAt(n);
  }
}
適合コード (clear())

可能なかぎり、実行環境に用意されている標準的な手段を使うのがよい。以下のコードでは、 vector.clear() メソッドを使ってすべての要素を削除している。

public void useVector(int count) { 	
  for (int n = 0; n < count; n++) {
    vector.add(Integer.toString(n));
  }
  // ...
  vector.clear(); // vector をクリア
}
違反コード (メソッドのローカル変数でないインスタンスフィールド)

以下の違反コード例では、HashMap インスタンスフィールドを定義し、インスタンスを割り当てているが、doSomething() メソッドでしか使っていない。

public class Storer {
  private HashMap<Integer,String> hm = new HashMap<Integer, String>();
  
  private void doSomething() {
    // hm はここで使われるだけで他からは参照されない
    hm.put(1, "java");
    // ...
  }
}

プログラマはそう思っていなかったかもしれないが、HashMap は、Storer インスタンスの生存期間中ずっと残っている。

適合コード (インスタンスフィールドのスコープを狭める)

以下の適合コードでは、HashMapdoSomething() メソッドのローカル変数として宣言している。ローカル変数 hm は、メソッドが処理を完了すると削除される。このローカル変数が HashMap オブジェクトへの唯一の参照であった場合、ガベージコレクタは、ローカル変数と一緒に HashMap オブジェクトが占めるメモリ領域も回収することになる。

public class Storer {
  private void doSomething() {
    HashMap<Integer,String> hm = new HashMap<Integer,String>();
    hm.put(1,"java");
    // ...
  }
}

インスタンスフィールドのスコープを小さくすることにより、ガベージコレクションが単純になる。今日の世代別ガベージコレクタ(generational garbage collectors)は、生存期間の短いオブジェクトに対して効率よく動作する。

違反コード (失効リスナー, Lapsed Listener)

「失効リスナー」(Lapsed Listener) として知られる以下の違反コード例では、オブジェクトが予期せず残ってしまう[Goetz 2005a]。readSomething() メソッドの処理が完了した後、reader オブジェクトは再び使われることはないのに、変数 buttonreader オブジェクトへの参照を持ち続けている。そのため、ガベージコレクタは reader オブジェクトを回収できない。同じような問題は、内部クラスでも発生する。内部クラスでは、それを含む外側のクラスへの参照を暗黙のうちに持っているからである。

public class LapseEvent extends JApplet {
  JButton button;
  public void init() {
    button = new JButton("Click Me");
    getContentPane().add(button, BorderLayout.CENTER);
    Reader reader = new Reader();
    button.addActionListener(reader);
    try {
      reader.readSomething();
    } catch (IOException e) { 
      // 例外の処理
    }		 
  }
}

class Reader implements ActionListener {
  public void actionPerformed(ActionEvent e)  {
    Toolkit.getDefaultToolkit().beep();
  }
  public void readSomething() throws IOException {
    // ファイルから読み込み
  }
}
違反コード (削除の前に例外発生)

以下の違反コード例では、removeActionListener() メソッドを使って reader を削除しようとしている。

Reader reader = new Reader();
button.addActionListener(reader);
try {
  reader.readSomething();  // 例外が発生すると、次の行の実行をスキップしてしまう
  // reader を削除、しかし実行フローは catch 節に飛んでしまうかもしれない
  button.removeActionListener(reader);  
} catch (IOException e) { 
  // ハンドラに処理を移す
}

readSomething メソッドが例外をスローすると、removeActionListener() は実行されない。

適合コード (finally ブロック)

以下の適合コードでは、reader オブジェクトの参照が必ず削除されるよう、finally ブロックを使っている。

Reader reader = new Reader();
button.addActionListener(reader);
try {
  reader.readSomething();
} catch (IOException e) { 
  // 例外の処理
} finally {
  button.removeActionListener(reader);  // 必ず実行される
}
違反コード (メンバオブジェクトのリーク)

以下の違反コード例はスタック構造を実装している[Bloch 2008]。この実装では、スタックに積まれたデータの参照を、スタックから取り除かれた後も保持し続けている。

public class Stack {
  private Object[] elements;
  private int size = 0;

  public Stack(int initialCapacity) {
    this.elements = new Object[initialCapacity];
  }

  public void push(Object e) {
    ensureCapacity();
    elements[size++] = e;
  }

  public Object pop() { // このメソッドでメモリリークが発生する
    if (size == 0) {
      throw new EmptyStackException();
    }
    return elements[--size];
  }

  /*
   * 少なくとも要素1つは追加できるスペースを確保するため、
   * 配列を拡張する必要があるときはおよそ2倍に拡張する.
   */
  private void ensureCapacity() {
    if (elements.length == size) {
      Object[] oldElements = elements;
      elements = new Object[2 * elements.length + 1];
      System.arraycopy(oldElements, 0, elements, 0, size);
    }
  }
}

オブジェクト参照はスタックから取り除かれた後も残っている。そのような「使わなくなった参照」によって参照されているオブジェクトは到達可能であり、オブジェクトは回収されない。

適合コード (null)

以下の適合コードでは、使わなくなった参照すべてに null を代入している。

public Object pop() {
  if (size == 0) {
    throw new EmptyStackException(); // オブジェクトの一貫性を保証
  }
  Object result = elements[--size];
  elements[size] = null; // 使わなくなった参照を削除
  return result;
}

このようにすると、ガベージコレクタは、スタックから取り除かれたオブジェクトを回収できる。

これらのサンプルコードでの間違いは自明なものであり、実際のコードでは問題にならないと思うかもしれない。しかし、大きなレコードを大量に含むハッシュテーブルなどのようなデータ構造の取り扱いはやはり問題である。配列と同様なデータ構造を使う場合には、不要な要素に null を代入する、という手法をとるのが賢明である。個々のオブジェクト参照やローカル変数に対してはこの手法をとる必要はない。それらに対しては、ガベージコレクタは適切に働くからである[Commes 2007]。

違反コード (強参照、strong reference)

不要になったオブジェクトの扱いに関するよくある間違いは、マップのようなコレクションにおいて、オブジェクトを意図せず保持し続けてしまうことである。以下の違反コード例において、サーバは、確立されたすべてのセキュア接続に関する一時的なメタデータを持っている。

class HashMetaData {
  private Map<SSLSocket, InetAddress> m = Collections.synchronizedMap(
      new HashMap<SSLSocket, InetAddress>());

  public void storeTempConnection(SSLSocket sock, InetAddress ip) {
    m.put(sock, ip);  
  }

  public void removeTempConnection(SSLSocket sock) {
    m.remove(sock);  
  }	
}

このコードでは、マップから関連情報を削除せずにソケットをクローズしてしまう可能性がある。そうすると、removeTempConnection() が呼び出されるまでは、無効になったソケットに関する情報はマップに含まれたままになる。ソケットがクローズされたことを通知する仕掛けがない限り、removeTempConnection() をいつ呼び出してよいかは分からない。さらに、元のオブジェクトやリファレント(Socket 接続)を null にするというやり方は手間がかかる。

適合コード (弱参照、weak reference)

以下の適合コードは、適切なタイミングでガベージコレクションが行われるように、「弱参照」(weak reference) を使っている。

// ...
private Map<SSLSocket, InetAddress> m = Collections.synchronizedMap(
  new WeakHashMap<SSLSocket, InetAddress>()
);

強参照を使うと、Map のようなコンテナオブジェクトのなかに収められたオブジェクトがガベージコレクタに回収されるのを妨げてしまう。Java API の説明によれば、「弱参照オブジェクトは、その弱参照オブジェクトのリファレントがファイナライズ可能になり、ファイナライズされ、そして回収されることを阻止することはない」[API 2006]

WeakHashMap オブジェクトが保持するキーは弱参照を通じて参照される。オブジェクトは、強参照がなくなると回収可能になるが、弱参照を使ってリファレントを参照すれば、リファレントのガベージコレクションが遅れることはない。これは、オブジェクトの生存期間がキーと同じであることが要求されるときにだけ使える手法である。

弱参照を使って不要なオブジェクトがガベージコレクションの対象になることを保証するだけでは、不十分である。エントリを追加できるようにするため、データ構造から余分なエントリを削除しなければならない。そのような削除手法のひとつは WeakHashMapget() メソッドを呼び、返り値が null であるエントリを削除することである(polling、ポーリング)。参照キューを使用すれば、もっと効率がよい[Goetz 2005b]。

適合コード (参照キュー、reference queue)

参照キューはリファレントが回収されるときに通知を行う仕組みを提供する。リファレントがガベージコレクタに回収されても、HashMapWeakReference オブジェクトとそれに対応するマップの値の強参照を持ち続ける(for each entry in the HashMap)。

ガベージコレクタは、オブジェクトへの参照をクリアすると、対応する WeakReference オブジェクトを参照キューに追加する。参照キューに何らかの操作(put()remove() など)が行われるまで、WeakReference オブジェクトは参照キューに存在し続ける。何らかの操作が行われた後、HashMap 中の WeakReference オブジェクトもガベージコレクタに回収される。この手順は、以下のコードのようにして、自前で行うこともできる。

class HashMetaData {
  private Map<WeakReference<SSLSocket>, InetAddress> m = 
      Collections.synchronizedMap(
        new HashMap<WeakReference<SSLSocket>, InetAddress>());
  ReferenceQueue queue = new ReferenceQueue();
  
  public void storeTempConnection(SSLSocket sock, InetAddress ip) {
    WeakReference<SSLSocket> wr = new WeakReference<SSLSocket>(sock, queue);

    // 要素を追加する前に無効になったエントリがないか poll する
    while ((wr = (WeakReference) queue.poll()) != null) {
      // WeakReference オブジェクトとその値(リファレントではない)を削除
      m.remove(wr); 
    }  
    m.put(wr, ip);
  }

  public void removeTempConnection(SSLSocket sock) {
    m.remove(sock);  
  }
}

WeakReference の2引数コンストラクタは Queue を引数にとり、その引数を使って直接キューの操作を行わなければならないことに注意。また、キューに追加を行う前に、無効になったエントリを削除すべきである。

適合コード (ソフト参照)

ソフト参照を使用する方法もある。ソフト参照は、OutOfMemoryError が発生する前にリファレントが回収されること、および、メモリ不足が起こるまでは回収されないことを保証してくれる。

class HashMetaData {
  private Map<SoftReference<SSLSocket>, InetAddress> m = 
        Collections.synchronizedMap(
        new HashMap<SoftReference<SSLSocket>, InetAddress>());
  ReferenceQueue queue = new ReferenceQueue();

  public void storeTempConnection(SSLSocket sock, InetAddress ip) {
    SoftReference<SSLSocket> sr = new SoftReference<SSLSocket>(sock, queue);
    while ((sr = (SoftReference) queue.poll()) != null) {
      // WeakReference オブジェクトとその値(リファレントではない)を削除
      m.remove(sr); 
    }  
    m.put(sr, ip);
  }

  public void removeTempConnection(SSLSocket sock) {
    m.remove(sock);  
  }	
}

弱参照はソフト参照よりもガベージコレクションがアグレッシブに行われる。したがって、メモリを効率的に使うことが重要なアプリケーションでは弱参照を使うべきである。一方、データのキャッシングがより重要となるアプリケーションでは、ソフト参照を使うべきである。

リスク評価

メモリリークを起こす Java アプリケーションは DoS 攻撃される危険がある。

ルール 深刻度 可能性 修正コスト 優先度 レベル
MSC04-J P1 L3
関連ガイドライン
ISO/IEC TR 24772:2010 Memory Leak [XYL]
MITRE CWE CWE-401. Improper release of memory before removing last reference ("memory leak")
参考文献
[API 2006] Class Vector, Class WeakReference
[Bloch 2008] Item 6. Eliminate obsolete object references
[Commes 2007] Memory Leak Avoidance
[Goetz 2005a] Lapsed Listeners
[Goetz 2005b] Memory Leaks with Global Maps; Reference Queues
[Gupta 2005]  
翻訳元

これは以下のページを翻訳したものです。

MSC04-J. Do not leak memory (revision 93)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter