OutOfMemoryErrorの原因と対応(1)

OutOfMemoryErrorの原因と対応(1)

Javaアプリケーションサーバーを使っていると、OutOfMemoryErrorに遭遇することが時々ある。最近はサーバーの物理メモリサイズが2GBを超えることもあるのに、やっぱりこのエラーは発生する。

OutOfMemoryErrorは文字通り、メモリが足りないという意味だ。だけど、当然ながら物理メモリを増設する必要があるというわけではない(本当にそうするしかない場合もあるけど・・・)。通常はサーバーチューニングやアプリケーションロジックの見直しでこのエラーに対処する。

今回はそのうち、アプリケーション側の原因によって発生したOutOfMemoryErrorをターゲットとし、アプリケーションの見直しにより対処する方法について話したいと思う。

Javaの関連記事:

Javaのメモリ管理の特徴は・・・

本題に入る前に、まずはJavaでのメモリ管理の特徴を挙げてみたい。

  1. Javaアプリケーションから利用可能なメモリは、ヒープと呼ばれるJVM上のメモリ空間のみ
  2. JVM上で使用可能なメモリ空間には上限値あり(Javaアプリケーション起動時に決定される。SunのJVMではデフォルトが64MB)
  3. どこからも参照されなくなったオブジェクトは、GC(ガベージコレクション)によりメモリ上から開放される
  4. サーバー側でも全てのリクエストが同一のメモリ空間を共有して利用する

CやC++のように自分でメモリ管理をする必要がないから、メモリ管理のためのコーディングは随分と楽になった。が、メモリ空間に上限があったり、スレッド間でメモリが共有される、という特性から、メモリが足りなくなるという現象が現実に発生する。

Javaのこのようなメモリ管理の特徴が分かると、OutOfMemoryError発生のからくりが理解しやすいんじゃないかなと思う。

メモリが足りなくなる3つのケース

ではまずOutOfMemoryErrorを発生原因別に3タイプに分類してみる。

(A)サイズオーバー型
巨大な領域確保によって一気にヒープの最大サイズをオーバー
(B)メモリリーク型
開放されないオブジェクトが溜まり続けることで使用中メモリが徐々に増加し、メモリが枯渇
(C)マルチスレッド型
長時間処理により”死んだ”スレッドがメモリを食い尽くす

アプリケーションによる原因によるものなら、ざっとこれくらいだろう。では各タイプの特徴、そして回避策にはどのようなものがあるのか?

(A)大量データでOutOfMemoryError – サイズオーバー型

大量データを扱う際に発生するOutOfMemoryErrorがこのタイプ。ヒープの最大サイズを超えるメモリを使用したために発生するエラーで、現象・原因ともに単純だ。

発生しやすい箇所と言えば、管理ツールの集計機能とかCSVダウンロード機能、ファイルアップロード機能、バッチ処理などだろう。

サンプル

例としてDBから取得した情報をCSV形式に加工して処理するプログラムを考えてみる。

Javaで文字列を加工するときは、StringBufferクラスやStringBuilderクラスを主に使用する。これらのクラスを使用して巨大な文字列を作成しようとすると、メモリ不足の危険性が高い。というのも、文字列全体がヒープメモリ上に存在する必要がある、巨大な文字列となるとヒープメモリ上に収まりきらない可能性があるためだ。文字列が巨大化し、ヒープ上に入りきらなくなるとOutOfMemoryErrorが発生する。

たとえば以下のCSV出力サンプルは、DBのレコード件数が数百万規模となるとOutOfMemoryErrorが発生する。

StringBuffer csv = new StringBuffer();
ResultSet result = statement.executeQuery(
  "SELECT ITEM_ID,ITEM_NAME FROM ITEM_MASTER");
try {
  while (result.next()) {
    String itemId = result.getString("ITEM_ID");
    String itemName = result.getString("ITEM_NAME");
    csv.append(itemId);
    csv.append(",");
    csv.append(itemName);
    csv.append("rn");
  }
} finally {
  result.close();
}
FileWriter writer = new FileWriter("item.csv");
try {
  writer.write(csv.toString());
} finally {
  writer.close();
}

このように想定されるデータ件数が数十万~数百万規模となる場合は、順次処理(ストリーミング)で行うのがよい。たとえばCSV出力ならDBから1レコード取得する毎に1行だけファイル出力する。ファイルアップロードなら、1KB毎にPOSTデータを読み込み、1KB毎にファイルに出力する。この方法なら、StringBufferやListなどに全データを投入する必要がなく、1レコード分とか1KB分とか一部のデータを使用するだけで済む。

ほとんどのケースでは順次処理に変更するだけで、この手のメモリ不足は解決できる。

(B)長期間経過後にOutOfMemoryError – メモリリーク型

サーバーを起動してから数日~数ヶ月後に発生するタイプのOutOfMemoryがコレ。小さなメモリリークが蓄積していき、ヒープ領域を少しずつ食いつぶしていく。最後には空き領域がなくなり、OutOfMemoryErrorが発生するというものだ。

このタイプのOutOfMemoryErrorは、static変数やServletのインスタンス変数に対し、誤った使い方をしている場合に起こりやすい。static変数やServletのインスタンス変数は、Webアプリケーション内で共有され、アプリケーションサーバー停止まで開放されることはない。これを知らないままオブジェクトの更新を行ってしまうと、メモリリークが発生することがある。

サンプル

たとえば、次のようにServletのインスタンス変数にListオブジェクトが使用されていたとする。このListにaddメソッドでエントリーを追加していった場合、リクエスト処理が完了しても追加したエントリーはGCの対象とならない。するとエントリーは開放されず、アプリケーションサーバーを停止するまでヒープ上に残り続ける。

public class LeakServlet extends HttpServlet {

  private List<String> debugMessages = new ArrayList<String>();

  protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
    String foo = request.getParameter("foo");
    String bar = request.getParameter("bar");
    if (foo == null) {
      debugMessages.add("パラメータfooがありません。");
    }
    if (bar == null) {
      debugMessages.add("パラメータbarがありません。");
    }
  }
}

この現象に対する対策としては、以下が考えられる。

  • 更新対象オブジェクトはServletのインスタンス変数やstatic変数などにせず、リクエスト毎に毎回インスタンス化する
  • 更新対象オブジェクトをどうしてもstatic変数で定義する必要がある場合は、エントリー数が無尽蔵に増えるのを防ぐため、保持できるエントリー数に上限チェックつける

まとめ

今回紹介したOutOfMemoryErrorの回避方法をまとめておく。

  • 大量データの処理は順次方式(ストリーミング方式)で実装する
  • 更新を行うオブジェクトの場合、static変数やServletのインスタンス変数は使わない(どうしても必要な場合は上限値を設定する)

次回はスレッド型のOutOfMemoryErrorと、OutOfMemoryErrorの原因を調査する方法について紹介する。

(参考サイト)

Page Top