[tech][Java]Unix系JavaでRuntime.execをつかうときの落とし穴

JavaではJVMやライブラリの実装がWindows、MacOS、Linux等で共通の実装だけを採用する方針だったりします。
なので、シンボリックリンクをはったりといったような、使いたくなるような気の利いたOSの機能はAPIでは用意されていません。しかたがないので、lnコマンドといったような外部コマンドを呼び出したい気分になるのですが、Unix系JavaでRuntime.exec()を使って外部コマンドを呼び出す際には注意が必要です。意外な落とし穴が待っています。

もし今サーバーがUnix系OSでJavaでRuntime.exec()を使おうと思っているなら、今すぐ別の方法を検討してください。後でとんでもない目にあいますよ。

いったいどういうことなんでしょうか?とりあえず、こんなプログラムを考えてみます。

package org.kazumi007;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class ExecuteCommand {

	public static void main(String[] args) {
		// 400Mbyte確保
		int[] list = new int[2000000];
		list[0] = 1;
		String cmd = "sleep 1";
		while (true) {
			try {
				Process process = Runtime.getRuntime().exec(cmd);
				InputStream is = process.getInputStream();
				BufferedReader br = new BufferedReader(
						new InputStreamReader(is));
				String line = null;
				while ((line = br.readLine()) != null) {
					// なにもしない
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

	}
}

内容は単純です。巨大な配列を確保してsleepコマンドを1秒おきに起動しているだけの単純なプログラムです。これのどこが問題なのでしょうか?

実行中のJavaプロセスのメモリをみてみましょう・・・。何度か取得した結果の一部です。

$ ps -a -o vsz,comm,args,pid,ppid |grep java
75732 grep grep java 22292 20554
1300640 /usr/bin/java /usr/bin/java -Xmx512m org.kazumi007.ExecuteCommand 22086 10150
1300640 /usr/bin/java /usr/bin/java -Xmx512m org.kazumi007.ExecuteCommand 22290 22086

OSX 10.6.1のjava version "1.6.0_15"でテストしました。

同じメモリサイズのJavaのプロセスが2つありますね。なにこれ?!

実はUnix系のJVMはRuntime.execが実行されると、内部でfork()を呼び出しプロセスのコピーを作成し、exec()でコマンドを実行しているのです。この実装のために、コマンドを生成すると必ず起動元のプロセスと同じサイズの空きメモリがなければなりません。さらに自体を悪くするのが、Windowsでは実装が異なるので通常開発環境では問題にならないのです。本番ではOutOfMemoryErrorが出るんだけど、再現しない・・・。ということになってしまうのです。

大切なことなので、もう一度書きます。

Unix系JavaでRuntime.execを使うなら、起動元のプロセスで使用しているメモリと同量の空き容量が必要です。可能な限り、Javaから外部コマンドを呼び出さずに別の方法を検討しましょう。

これってJavaで開発するときの常識なんですか?