ImageIO.read() が異常終了したけど catch 句で例外が捕捉できなかった
Java プログラムで ImageIO.read()
を使って、画像ファイルを BuffereImage オブジェクトとして読み込もうとする処理があった。
// イメージ的にはこんな感じの処理部分
BufferedImage bfImg;
try {
bfImg = ImageIO.read(file);
}
catch(Exception e) {
// とりあえず何かしらの Exception なら catch するように書いてあった
return null;
}
すると、一部の画像でプログラムが異常終了していたのだが、try・catch
の catch
句で例外が捕捉できずに異常終了していた。
try・catch
で例外がキャッチできないってなんだー?と思ったのだけど、結論としては Java における「例外」をちゃんと理解していなかったことが分かった。
ImageIO.read()
でメモリリークが発生する
ImageIO.read()
は引数の File オブジェクトを画像ファイルとして読み込み、BufferedImage オブジェクトに復号して返す。この時、元の画像ファイルは JPG や GIF であっても、BufferedImage オブジェクトは内部的にはビットマップとして保持しているようなのだ。
だから、元画像のファイルサイズは小さくとも、ピクセルサイズ (幅や高さ) が大きいと、メモリが枯渇する。これにより、処理がそこで終わってしまっていたようだ。
この時の OutOfMemoryError は Error
ImageIO.read()
でメモリリークが発生する (しやすい) という情報はウェブ上ですぐ見つかったが、これが Catch できない理由が分からなかった。
そこでふと思い出して、catch
句を以下のように書き換えてみた。
BufferedImage bfImg;
try {
bfImg = ImageIO.read(file);
}
catch(Throwable e) {
// ↑Exception から Throwable にした
return null;
}
すると、OutOfMemoryError
をキャッチすることができた。
メモリリーク・メモリ枯渇によって JVM から投げられるこの OutOfMemoryError
は、Exception 型のサブクラスではなく、Error 型のサブクラス。だから、Exception e
で拾おうとしても拾えなかったのである。
そして、Throwable
は、Exception 型と Error 型の両方のスーパークラス。こいつが全ての生みの親 (?)。こいつを catch
句に指定してやれば、子クラスである Error 型、そしてそのサブクラスである OutOfMemoryError
も拾える、というワケ。
例外処理というと「Exception」が全てと思いがちだが、これは「ある程度開発者が何らかの手立てができる NG パターン」を表すモノと思うと良い。NullPointerException なら呼び方が間違っていて、Null チェックするなり正しく呼べるようにするなり開発者が手立てを打てる、そういう感じ。
一方、「Error」はというと、この OutOfMemoryError
のように、基本的には Java プログラム内、アプリケーション内で何か手立てができるモノではない、というモノが投げられる。誰のせいでもない、とも言えるだろうか。いや、結局は JVM の起動引数でヒープサイズを適切に指定しなかった開発者の問題かもしれないけど、もしかしたら「これ以上ヒープサイズを拡張できないサーバマシン」なのかもしれないし、責任の所在が Exception よりはハッキリさせられない印象にあるモノ。
解決策はというと
今回の場合、サーバのヒープサイズを拡張する許可が折りなかったので (!?)、処理方法を大幅に変えて、「ImageIO.read()
や BufferedImage オブジェクトを使用せずに画像を扱う」ことにした。ナンテコッタイ…/(^o^)\