Trouble 11: Javaから別プロセスを動かせません
<Q11-1>Runtime.exec()でシェルスクリプトを繰り返し実行するとRuntime.exec()がブロックしたままになってしまいます。
| <発生環境> |
| OS |
RedHatLinux6.2J |
| JDK |
JDK1.3.1 |
| Vender |
Sun |
- <A11-1>
- 根本的な原因と対策方法は不明のまま。
発生パターンとして、小さなシェルスクリプトを連続して実行すると現象が発生しやすい傾向があります。
この発生パターンから、回避策として一回の処理を1つのシェルスクリプトにまとめてRuntime.exec()の呼び出し回数を減らしてみたところ、現象が発生しなくなりました。
ただしこの回避策は、根本的な原因を解決したものではないので、すべてのケースで有効である確証は取れていません。
<Q11-2>Javaからシェルスクリプトを起動することができません。
| <発生環境> |
| OS |
Solaris |
| JDK |
JDK1.3 |
| Vender |
Sun |
- <A11-2>
- 以下のように、シェルコマンドからシェルスクリプトを起動するようにすることで解決します。
String cmd[] = {"/bin/sh", "-exec", "hogehoge.sh"};
Process proc_ = Runtime.getRuntime().exec(cmd);
この方法でも、標準出力に対して出力を行うシェルスクリプトを起動するとうまくいかないようです。これはWindowsのバッチ起動でも同様の問題が起きています。
この問題を回避するため、標準出力に出力する代わりに、テンポラリファイルに出力するなどの工夫が必要です。
<Q11-3>java.util.Timerクラスに登録したTimerTaskの1つを、Thread.sleep()メソッドで停止させたところ、他のTimerTaskが実行されなくなってしまいました。
| <発生環境> |
| OS |
Any |
| JDK |
JDK1.3.0 |
| Vender |
Any |
- <A11-3>
- Timerクラスが、TimerTaskを順に処理しているためです。
Timerクラスは、登録されたTimerTaskを連続して実行するための唯一のスレッドを持ちます。
よって、現在実行中のTimerTaskの処理が終わらない限り、次のTimerTaskが実行されることはありません。
このため1つのTimerTaskがスリープしている間は、その後に実行される予定のTimerTaskは永遠に待たされることになります。
よって、TimerTaskでは停止もしくは長い処理を行うべきではありません。
もし必要な場合は、TimerTaskから別のスレッドを起動してその中で実行する様にすることで問題を回避できます。
<Q11-4>java.util.TimerTaskをcancel()していないのにIllegalStateException("Timer already cancelled.")が発生してしまいます。
TimerTaskを定期的に登録するようなシステムで、あるとき以下の例外が発生しました。
IllegalStateException : Timer already cancelled.
(以下、java.util.Timer#schedule() で終わるスタックトレース)
java.util.Timer#cancel()を呼び出した後にjava.util.Timer#schedule()を呼び出すとこの例外が発生するらしいのですが、そもそもcancel()を呼び出すコードは実装していません。
| <発生環境> |
| OS |
Any |
| JDK |
1.4.2 |
| Vender |
Any |
- <A11-4>
- java.util.TimerTask#run()内でキャッチされない例外が発生している可能性があります。
TimerはTimerTaskを実行するためにTimerThreadというスレッドを持っています。
このTimerThreadが登録されたTimerTaskを実行するのですが、TimerTask#run()内でキャッチされない例外が発生すると、TimerThreadが例外をキャッチし、Timerに対してcancel()処理を行ないます。この結果、全てのTimerTaskがキャンセルされます。
よって覚えのないIllegalStateExceptionが発生する場合、TimerTask#run()内で例外が発生している可能性が考えられます。
対処方法としては、以下の方法が考えられます。
1.TimerTaskのrun()メソッドを、例外が発生しないようなコードに書き換える。
2.TimerTaskのrun()メソッド内の例外を正しくキャッチ/処理する。
3.schedule()を呼び出す箇所でIllegalStateExceptionをキャッチし、Timerを再生成するようにする。
<Q11-5>Runtime#exec(String) で IOException が発生して、外部コマンドが実行できません。
| <発生環境> |
| OS |
Solaris、Linux |
| JDK |
1.4.1、1.4.2 |
| Vender |
Sun |
- <A11-5>
- コマンドを指定するパスに空白文字が含まれていませんか?
Runtime#exec(String)では空白文字は引数との区切り文字に解釈されてしまいます。
代わりに Runtime#exec(String[]) を使って下さい。
以下は"/my bin"というディレクトリの"touch"コマンドを引数"test.txt"で実行した例です。
失敗例:
String command = "/my bin/touch test.txt";
Runtime.getRuntime().exec(command);
(実行すると、「java.io.IOException: /my: not found」が発生します)
成功例:
String[] command = {"/my bin/touch", "test.txt"};
Runtime.getRuntime().exec(command);
<Q11-6>Runtime.exec()で起動したJavaアプリケーションがしばらくして停止してしまいます。
| <発生環境> |
| OS |
Win98OSR2 |
| JDK |
JDK1.3.0 |
| Vender |
Sun |
- <A11-6>
- Runtime.exec()で起動された子プロセスが標準出力に出力を行っているためです。
親-子関係になったVM間のバッファサイズは1024バイトであり、それ以降を出力しようとするとブロックし、子プロセスの動作が停止してしまいます。
そのため、子プロセスは標準出力に表示しないか、もしくは親プロセスでVM間のバッファのストリームを読み捨てる必要があります。
<Q11-7>java.lang.Runtime.exec()を複数回呼ぶと、execが立ち上げたスレッドが無限ループに入ることがあります。2回で起きることもあるし、100回以上ループする場合もあります。
しかし、Windows2000上では1000回ループさせても問題はありませんでした。
| <発生環境> |
| OS |
Red Hat Linux 6.2 |
| JDK |
JDK1.3β |
| Vender |
IBM |
- <A11-7>
- SunのVMに変更し、execを使用するのは1つのVMにつき1回のみとすることで、解決できます。
<Q11-8>Runtime#exec()を使用すると、OutOfMemoryErrorが発生することがあります。起動しようとしている外部プログラムはごく小さなもので、それに対して十分な空きメモリはあります。
Runtime#exec()を使用すると、OutOfMemoryErrorが発生することがあります。起動しようとしている外部プログラムはごく小さなもので、それに対して十分な空きメモリはあります。
| <発生環境> |
| OS |
Solaris |
| JDK |
1.4.1 |
| Vender |
Sun |
- <A11-8>
- 起動元JavaVMが“太っている”可能性があります。
JavaVMのUNIX実装(Solaris、Linux)では外部プロセスの起動にfork()、exec()システムコールを使用します(※)。従って大量のメモリを使用しているなど、起動元プロセスが太った状態にあると、外部プログラムのサイズによらずforkに失敗することがあります。
ps、prstatコマンドなどで起動元プロセスのメモリサイズを確認してください。
起動元プロセスのメモリ大量消費が確認できた場合、
(1)swap領域を増やす
(2)物理メモリを増設する
(3)起動元プロセスをシェイプアップする
などの対処を行なう必要があります。
※実際にはfork1()を使用するため、単純にプロセス全体のコピーが作成されるわけではありません。
注意:本文書の内容に誤りがあり、またこの文書によって不利益を被っても、
エスエムジー株式会社は一切関知いたしません。