ImageIO.read() が異常終了したけど catch 句で例外が捕捉できなかった

Java プログラムで ImageIO.read() を使って、画像ファイルを BuffereImage オブジェクトとして読み込もうとする処理があった。

// イメージ的にはこんな感じの処理部分
BufferedImage bfImg;
try {
  bfImg = ImageIO.read(file);
}
catch(Exception e) {
  // とりあえず何かしらの Exception なら catch するように書いてあった
  return null;
}

すると、一部の画像でプログラムが異常終了していたのだが、try・catchcatch 句で例外が捕捉できずに異常終了していた。

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^)\

参考