Java6 と Java7 の挙動の違い(バグ?)

Java6 と Java7 で挙動が違ったところがありました。


再現コードは単純で…。

public class Test {
    public static void main(String[] args) {
        try {
            main(args);
        } catch (Error e) {
            e.printStackTrace();
        }
    }
}

無限再帰するだけのコードです。
最終的には StackOverflowError が発生し、スタックトレースが出るはずなんですが…。

(Java7 での実行結果)
Exception in thread "main"
Exception: java.lang.NoClassDefFoundError thrown from the UncaughtExceptionHandler in thread "main"

なぜか、NoClassDefFoundError になりました。*1
スタックトレースは出ないです。


Java6 だと、予想通りの結果でした。*2

(Java6 での実行結果)
java.lang.StackOverflowErrorjava.lang.StackOverflowErrorjava.lang.StackOverflowErrorjava.lang.StackOverflowErrorjava.lang.StackOverflowErrorjava.lang.StackOverflowErrorjava.lang.StackOverflowErrorjava.lang.StackOverflowErrorjava.lang.StackOverflowErrorjava.lang.StackOverflowErrorjava.lang.StackOverflowErrorjava.lang.StackOverflowErrorjava.lang.StackOverflowError
    at java.nio.Buffer.<init>(Buffer.java:170)
    at java.nio.CharBuffer.<init>(CharBuffer.java:259)
    at java.nio.HeapCharBuffer.<init>(HeapCharBuffer.java:52)
    at java.nio.CharBuffer.wrap(CharBuffer.java:350)
    at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:246)
    at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:106)
    at java.io.OutputStreamWriter.write(OutputStreamWriter.java:190)
    at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:111)
    at java.io.PrintStream.write(PrintStream.java:476)
    at java.io.PrintStream.print(PrintStream.java:619)
    at java.io.PrintStream.println(PrintStream.java:773)
    at java.lang.Throwable.printStackTrace(Throwable.java:461)
    at java.lang.Throwable.printStackTrace(Throwable.java:451)
    at Test.main(Test.java:7)
    at Test.main(Test.java:5)
(以下省略)

何が起きている?

調べてみたら、NoClassDefFoundError の発生個所は printStackTrace(PrintStreamOrWriter s) メソッドの一行目でした。

Set<Throwable> dejaVu =
    Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());

ここで、java.util.IdentityHashMap が初期化できないという例外*3が出ていました。


もちろん、普通に使えば IdentityHashMap はちゃんと初期化できます。
不思議なことに、IdentityHashMap を事前に使っていればスタックトレースが出ました…。

    public static void main(String[] args) {
        new IdentityHashMap<>();    // これでちゃんと動くようになります。
        try {
            main(args);
        } catch (Error e) {
            e.printStackTrace();
        }
    }

(この場合のスタックトレースは、Java6と同じ)


また、StackOverflowError 以外だとちゃんと動きます。

    public static void main(String[] args) {
        try {
            new ArrayList<String>(Integer.MAX_VALUE);
        } catch (Error e) {
            e.printStackTrace();
        }
    }
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
	at java.util.ArrayList.<init>(ArrayList.java:132)
	at Test.main(Test.java:8)

まとめ

バグでしょうか・・・?


以下、わかったことを追記(2012/06/11)

Throwable でキャッチすると、また挙動が変わる

「 catch (Error e) 」ではなく、「 catch (Throwable e) 」と書くと、挙動が変わります。普通に実行すると、StackOverflowError になります。
ただ、eclipse からデバッグモードで動かすと、JavaVMが死にます。

64bit 版の Java だと挙動が違う

64bit 版の Java だと、StackOverflowError になります。
上記の現象(NoClassDefFoundError)が発生するのは、32bit 版の Java だけのようです。
(OSには依存しません。64bit 版の OS に、32bit 版の Java をインストールした場合は、NoClassDefFoundError でした。)


Java の 32bit/64bit バージョンの確認方法ですが、「java -version」とコマンドを打って、64bit という表記があれば 64bit 版、特にビットについてについて記載がなければ 32bit です。

(32bit 版の例)
java version "1.7.0_04"
Java(TM) SE Runtime Environment (build 1.7.0_04-b22)
Java HotSpot(TM) Client VM (build 23.0-b21, mixed mode, sharing)

(64bit 版の例)
java version "1.7.0_04"
Java(TM) SE Runtime Environment (build 1.7.0_04-b20)
Java HotSpot(TM) 64-Bit Server VM (build 23.0-b21, mixed mode)

id:nowokay さんや、[twitter:@mitsu]さんから、普通にスタックトレースが出たということを教えていただきました*4
おそらく、この違いによるものなのかなーと思います。

挙動について

ここでの挙動は下図のようになります。
1. main メソッドの再帰呼び出し時に、スタックに積めなくなるので StackOverflowError が発生

2. catch がその例外を受け止め、printStackTrace メソッドの呼び出しに進む

よく考えてみると少し不思議で、普通のメソッドはスタックに積めないのに、printStackTrace メソッドはスタックに積める、ということになります*5
可能性としては、まだスタックに多少の余裕がある状態で StackOverflowError を発生させている、もしくは、例外処理のスタックは別扱いしていると考えられますが…、よくわからないです。


2012/06/13 追記
コメントいただいた点を、新しい記事にまとめました。
Java6 と Java7 の挙動の違いは、バグではありませんでした。 - 地平線に行く

*1:Windows XP - Java1.7.0 Update4、 Fedora16 - OpenJDK 1.7.0_b147 で確認

*2:でも、スタックトレースの場所がおかしい気がします。

*3:java.lang.NoClassDefFoundError: Could not initialize class java.util.IdentityHashMap

*4:ありがとうございました!

*5:しかも、PrintStream のメソッド呼び出しなどでさらにスタックが積みあがっていきます。