Javaでヒープ領域を余らせたままOutOfMemoryErrorを出す方法

先日、こんな問題を見かけたのだけども、JavaGCにはあまり詳しくないので答えがわからなかった。

OutOfMemoryErrorが発生しました。(中略)ヒープメモリは足りているようです。原因として何が考えられますか?

http://d.hatena.ne.jp/iad_otomamay/20130318/1363596244

なんでだろうなぁと思っていたところid:moriyoshiが「Permanent領域があふれたんじゃないの」と一言。「Permanent領域」で検索してみると、なるほど、そういうことなのかー。

というわけで早速それを再現させるコードを書いてみた。ヒープの大部分ががら空きなのにPermanent領域だけ99%になっているのがわかるかと思う。

Exception in thread "main" [Full GC [Tenured: 515K->515K(56896K), 0.0106600 secs] 972K->515K(82560K), [Perm : 4095K->4026K(4096K)], 0.0107050 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
java.lang.OutOfMemoryError: PermGen space
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:791)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:634)
	at Test.loadNext(Test.java:135)
	at Test.main(Test.java:142)
Heap
 def new generation   total 25664K, used 914K [0x00000007bfc00000, 0x00000007c17d0000, 0x00000007d5150000)
  eden space 22848K,   4% used [0x00000007bfc00000, 0x00000007bfce4878, 0x00000007c1250000)
  from space 2816K,   0% used [0x00000007c1250000, 0x00000007c1250000, 0x00000007c1510000)
  to   space 2816K,   0% used [0x00000007c1510000, 0x00000007c1510000, 0x00000007c17d0000)
 tenured generation   total 56896K, used 515K [0x00000007d5150000, 0x00000007d88e0000, 0x00000007ffc00000)
   the space 56896K,   0% used [0x00000007d5150000, 0x00000007d51d0e30, 0x00000007d51d1000, 0x00000007d88e0000)
 compacting perm gen  total 4096K, used 4058K [0x00000007ffc00000, 0x0000000800000000, 0x0000000800000000)
   the space 4096K,  99% used [0x00000007ffc00000, 0x00000007ffff6bd8, 0x00000007ffff6c00, 0x0000000800000000)
No shared spaces configured.

コードは重要なところだけ抜粋するとこんな感じ。

public class Test extends ClassLoader{
    static int count = 0;
    Class loadNext() throws ClassNotFoundException {
        (中略)
        return defineClass(name, b, 0, b.length);
    }

    public static void main(String args[]) throws ClassNotFoundException{
        Test me = new Test();
        while(true){
            me.loadNext();
            count++;
        }
    }
}

つまりクラスを動的に生成しつづけるコードだ。先ほどのコンソール出力の例では手早く落とすために「-XX:PermSize=4m -XX:MaxPermSize=4m」を指定してPermanent領域を小さくしたので800個ちょいのクラスを定義した所で落ちる。その設定なしだと僕の環境では8万個のクラスを定義したあたりで落ちる。

ソースコードはこちら。なお、バイトコードを理解しないままコピペするのが嫌で、理解できるまで調査した結果大量のコメントが入っているけども、そこは今回のストーリー上ではあんまり重要じゃないので無視してもよいだろう。
https://github.com/nishio/learn_language/blob/master/oome/Test.java