JavaでOutOfMemoryErrorを出す方法

お題:

結合テスト中のシステムで、OutOfMemoryErrorが発生しました。UT後ソースコードの変更はしていません。ヒープメモリは足りているようです。原因として何が考えられますか?(筆記解答)

8つの質問で、Java SI業界の現状を知る - レベルエンター山本大のブログ

OSのswapが足りないとOutOfMemoryError後に死んだりするみたいですね。

Out of Memory Errors a list of all of them」の15ページ目致命的なOOMEの例が幾つも載っているけど、それぞれどういう時に出るかを面接で聞かれても答えられないかも……。

今回は折角なので致命的じゃない方の、普通にヒープメモリが足りない場合について実際に発生させてみたい。

java.lang.OutOfMemoryError: Java heap space

普通にヒープメモリが足りなくなるヤツ。

        // クラッシュするコード
        List<byte[]> buffers = new ArrayList<byte[]>();
        while (true) {
            buffers.add(new byte[1024 * 1024 * 1024]);
        }
        // クラッシュするコード
        StringBuilder sb = new StringBuilder();
        while (true) {
            sb.append("1234567890");
        }

どんどんヒープを消費して行けばそのうちなくなります。参照が維持されているのでGCされない。

java.lang.OutOfMemoryError: Requested array size exceeds VM limit

超でっかい配列を作ろうとして怒られるパターン。

        // クラッシュするコード
        byte[] buffer = new byte[Integer.MAX_VALUE];

java.lang.OutOfMemoryError: unable to create new native thread

スレッド作り過ぎパターン。10秒の間に全力でスレッド作るので多分足りなくなる。Cヒープが枯渇してもOOMEになる。

        // クラッシュするコード
        while (true) {
            new Thread() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
        }

java.lang.OutOfMemoryError: GC overhead limit exceeded

ヒープサイズを大きく取りすぎたりして、GCに時間がかかり過ぎ&あんまり回収出来ない場合に出るそうですが、上手く出す方法見つからなかった。
いろいろ試してたらマシンが2〜3回死んだ。

java.lang.OutOfMemoryError: PermGen space (Javassist版)

Javaでヒープ領域を余らせたままOutOfMemoryErrorを出す方法 - 西尾泰和のはてなダイアリー」でやられているように、クラスを沢山作ってクラスローダを開放しないと、JavaヒープのPermanent領域が枯渇して出る。
バイトコード書くの大変なので、Javassistとか使っちゃう。

package sample.oom;
// w/ javassist-3.4.ga.jar
import javassist.ClassPool; 

// クラッシュするコード
public class OOMSample {
    public static void main(String[] args) {
        new Exception().printStackTrace();
        try {
            ClassPool pool = ClassPool.getDefault();
            for (int i = 0; i < Integer.MAX_VALUE; i++) {
                String className = String.format("MyClass%d", i);
                pool.makeClass(className).toClass();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

結果:

daphne:JavaSample terazzo$ java -cp bin:lib/javassist-3.4.ga.jar sample.oom.OOMSample
java.lang.Exception
	at sample.oom.OOMSample.main(OOMSample.java:7)
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
	at javassist.ClassPool.toClass(ClassPool.java:904)
	at javassist.ClassPool.toClass(ClassPool.java:847)
	at javassist.ClassPool.toClass(ClassPool.java:805)
	at javassist.CtClass.toClass(CtClass.java:1037)
	at sample.oom.OOMSample.main(OOMSample.java:12)

「new Exception().printStackTrace();」は入れておかないとスタックトレース出力中に再度OOMEがおこるので入れてます。
起動オプションに「-XX:+TraceClassLoading」「-XX:+TraceClassUnloading」を付けると、クラスのロード/アンロードの様子がログで出力されて分かりやすい。

daphne:JavaSample terazzo$ java -XX:+TraceClassLoading -XX:+TraceClassUnloading -cp bin:lib/javassist-3.4.ga.jar sample.oom.OOMSample
[Opened /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar]
...
[Loaded sun.reflect.NativeMethodAccessorImpl from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.reflect.DelegatingMethodAccessorImpl from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded MyClass0 from __JVM_DefineClass__]
[Loaded MyClass1 from __JVM_DefineClass__]
[Loaded MyClass2 from __JVM_DefineClass__]
[Loaded MyClass3 from __JVM_DefineClass__]
[Loaded MyClass4 from __JVM_DefineClass__]
[Loaded MyClass5 from __JVM_DefineClass__]
...
[Loaded MyClass78596 from __JVM_DefineClass__]
[Loaded MyClass78597 from __JVM_DefineClass__]
[Loaded MyClass78598 from __JVM_DefineClass__]
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
        at javassist.ClassPool.toClass(ClassPool.java:904)
        at javassist.ClassPool.toClass(ClassPool.java:847)
        at javassist.ClassPool.toClass(ClassPool.java:805)
        at javassist.CtClass.toClass(CtClass.java:1037)
        at sample.oom.OOMSample.main(OOMSample.java:12)

java.lang.OutOfMemoryError: PermGen space (Rhino版)

Rhinoは内部でJavaScriptJavaのクラスに変換しているので、やはり作りすぎて開放しないとPermGen spaceを食いつぶしてしまう。

package sample.oom;

import java.util.ArrayList;
import java.util.List;

// w/ rhino1_7R2.zip
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Script;

// クラッシュするコード
public class RhinoOOMSample {
    public static void main(String[] args) {
        Context context = Context.enter();
        try {
            List<Script> scripts = new ArrayList<Script>();
            while (true) {
                // JavaScriptコード「""」をコンパイルし、意図的にリークさせる
                scripts.add(context.compileString("", null, 0, null));
            }
        } finally {
            Context.exit();
        }
    }
}

結果:

daphne:JavaSample terazzo$ java -cp bin:./lib/js.jar sample.oom.RhinoOOMSample
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
	at java.lang.ClassLoader.defineClass(ClassLoader.java:791)
	at org.mozilla.javascript.DefiningClassLoader.defineClass(DefiningClassLoader.java:62)
	at org.mozilla.javascript.optimizer.Codegen.defineClass(Codegen.java:146)
	at org.mozilla.javascript.optimizer.Codegen.createScriptObject(Codegen.java:101)
	at org.mozilla.javascript.Context.compileImpl(Context.java:2409)
	at org.mozilla.javascript.Context.compileString(Context.java:1359)
	at org.mozilla.javascript.Context.compileString(Context.java:1348)
	at sample.oom.RhinoOOMSample.main(RhinoOOMSample.java:16)

ログあり版

daphne:JavaSample terazzo$ java -XX:+TraceClassLoading -XX:+TraceClassUnloading -cp bin:./lib/js.jar sample.oom.RhinoOOMSample
[Opened /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar]
...
[Loaded org.mozilla.javascript.DefiningClassLoader from file:/tmp/JavaSample/lib/js.jar]
[Loaded org.mozilla.javascript.SecurityUtilities from file:/tmp/JavaSample/lib/js.jar]
[Loaded org.mozilla.javascript.SecurityUtilities$2 from file:/tmp/JavaSample/lib/js.jar]
[Loaded org.mozilla.javascript.gen.c1 from file:/tmp/JavaSample/lib/js.jar]
[Loaded org.mozilla.javascript.gen.c2 from file:/tmp/JavaSample/lib/js.jar]
[Loaded org.mozilla.javascript.gen.c3 from file:/tmp/JavaSample/lib/js.jar]
...
java.lang.OutOfMemoryError: PermGen space
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:791)
        at org.mozilla.javascript.DefiningClassLoader.defineClass(DefiningClassLoader.java:62)
        at org.mozilla.javascript.optimizer.Codegen.defineClass(Codegen.java:146)
        at org.mozilla.javascript.optimizer.Codegen.createScriptObject(Codegen.java:101)
        at org.mozilla.javascript.Context.compileImpl(Context.java:2409)
        at org.mozilla.javascript.Context.compileString(Context.java:1359)
        at org.mozilla.javascript.Context.compileString(Context.java:1348)
        at sample.oom.RhinoOOMSample.main(RhinoOOMSample.java:16)
[Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar]

リフレクションが使われているので、使い方によってはスクリプト本体に加えてsun.reflect.Generated〜みたいなリフレクションの高速化の為のクラスも沢山作られるようだ。