Java: PermGen はヒープの外

今頃知った。PermGen も普通にヒープ内だと思ってました。


上記の新公式サイトで JDK 6u21 のリリースを知ってさっそく入れてみたところ、6u20 では普通に動いていた Eclipse 3.4 が、起動して少し操作しているとメモリ不足を訴えるようになった。一瞬「今日もー いつーもの ヒープ ぶそく」と思ってしまったけど、ログファイルを見ると、よく見るのとはちょっとだけメッセージが違っていた。くっ、ハズしたか……!

OutOfMemoryError: PermGen space

なに? PermGen 限定なの? あぁなんかそんなコマンドラインオプションがあったような。でも覚えてないな。覚えてない故の過ち。訳すと OutOfMemoryError というわけか。なるほどなるほどなるほど。
PermGen 領域のサイズを変更するには、java コマンドのオプションで HotSpot VM に対して指定する。HotSpot VM 関連のオプションは Java HotSpot VM Options のページにまとめられている。今回の場合は、eclipse.ini に以下の行を追加すればいい。-vmargs より後ろでないといけないことに注意。デフォルトは 64MB で、64 ビット版 VM では 30% 増しらしいので、まぁなんか少し大きめの値を指定してやればよろしかろう。

 -XX:MaxPermSize=128m

JVM ヒープについてのおさらい

Sun JDK の HotSpot JVM ではヒープが以下のような領域に分かれている。

  • 最近作られた若いオブジェクトが入る Eden 領域
  • それより少し長生きしてる中年層が入る Survivor 0 および Survivor 1 領域
  • がっつり長生きしてるオブジェクトが入る Old (Tenured) 領域

このようにオブジェクトの世代に応じて GC を行うやり方は Generational GC と呼ばれる。JDK 付属の Visual VM に Visual GC というプラグインを入れると、各領域がどのように変動するかをリアルタイムに見ることができる (Mac OS X に入っている Apple JDK には Visual VM が含まれていないが、Visual VM プロジェクトのサイトにあるダウンロードのページから入手できる)。

同じく Visual VM に VisualVM-MBeans プラグインを入れるか、やはり JDK 付属の JConsole を使用すると、領域によって GC の適用範囲が異なることも確認できる。まず、ツリーから java.lang を展開すると、GarbageCollector とか MemoryPool というのがあるはず。GarbageCollector はその JVM 上で使われる GC の種類、MemoryPool はメモリ領域を表している。

例えば、GarbageCollector の下にあるどれかの GC を選んで、Attributes タブにある MemoryPoolNames という行を探す。その右側 (Value 列) をダブルクリックすると、値がリスト表示される。下の図は Sun JDK 6u21 じゃなくて AppleMac OS X 版 6u20 での例なんだけど、GC の種類が 2 つあって、そのうち ParNew と表示されている方は Eden と Survivor しか対象としていない (=Tenured 領域の GC はしない) ということがわかる。

で、PermGen 領域って何なのか

僕は今まで、てっきり「PermGen 領域は、Old よりさらに古くて『もういっそ GC しないでよさそう』と判断されたオブジェクトが入る領域」なのだと思っていた。VisualGC では Old の次に表示されてるし。しかしよく考えるとこれはおかしい。古いオブジェクトの GC を諦めていったら、いつかはヒープを食いつぶしてしまいかねない。それじゃぁただのメモリリークだ。
今回調べてみたところ、「そもそも PermGen 領域は JVM ヒープの中にはない」ということを知った。JVM 用語でヒープと言ったら、作ったオブジェクト (インスタンス) が格納される領域のことだけど、PermGen 領域にはクラスやメソッドのデータ、およびそれらに関するメタデータが格納されるらしい。
したがって、Eclipseプラグインをたくさん使っていたりすると、PermGen 領域が一杯になり OutOfMemoryError が起きる可能性がある。そういえば今回僕のとこで起きたのも、Maven の POM ファイルを開こうとしたとき、つまり m2eclipse プラグインの POM エディタが読み込まれたタイミングだった。また、ネットで検索してみると、「Web アプリを Tomcat 上で動かしてたら OutOfMemoryError: PermGen space って出た」という報告が多く見られる。これも、ライブラリをたくさん使っていたり、JSPHibernate、Spring 等がたくさんのクラスを動的生成したために PermGen 領域があふれたってことなんだろう。
今回リリースされた JDK 6u21 では、HotSpot も最新版の HotSpot 17.0 にバージョンアップされている。以前起きなかった PermGen 枯渇が 6u21 で起きたということは、以前に比べてクラスのメタ情報を多く使うようになったのかもしれない。Java でアプリやライブラリを作ってる人は、動作環境の要件を見直す必要がある、のかな?
ちなみに、「PermGen 領域はヒープの外」という言い方と矛盾するような話になるけど、クラスも不要になれば GC の対象となって解放される。HotSpot の場合は、Old 領域を GC するときに、ついでに PermGen 領域に対しても GC が行われる。したがって、Tomcat でアプリを配備し直したなんて場合には、きちんとクラスが GC されていれば PermGen 領域が枯渇することはないはずである (インスタンスに対する参照が残ってしまっているとそのクラスも GC されなくなってしまうので、Tomcat では、配備解除するアプリ内の static フィールドを強制的にクリアして参照をゼロにするというちょっとゴリ押し的な対策を行っている。おかげで、アプリ側が気にしなくても PermGen の枯渇が回避できるわけだけど、この対策が引き金になって配備解除時に NullPointerException が発生することもあるので、まったく意識しなくていいということはない)。

PermGen 領域が枯渇した場合の対策方法

通常のヒープが不足した場合は、コマンドラインオプションの -Xmx を使って

java -Xmx1024m example.Main

のようにヒープの最大サイズを大きめにすることになるわけだけど、PermGen 領域は HotSpot 的にはヒープと違うため、-Xmx でどんなに大きな値を指定しても、PermGen 領域枯渇の対策にはならない。そこで登場するのが前述の -XX:MaxPermSize オプション。

java -XX:MaxPermSize=128m example.Main

今後

Oracle は Sun 買収で手に入れた HotSpot の他に、以前に BEA 買収で手に入れた JRockit という JVM も持っている。JRockit は複数の GC 方式を用意していて、実行中に様子を見ながらその時々で最もよさそうなものを使うという仕組みになっていて、場合によっては HotSpot よりもかなりよい性能を発揮するようだ。Oracle からは「今後は JRockit と HotSpot をマージして最強の JVM 作っちゃうぜ」というアナウンスがされているので、そのうちまたこの辺の知識は勉強し直す必要があるかもしれない。