HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

Stringオブジェクトに関する罠に対する罠

Stringオブジェクトに関する罠 - HHeLiBeXの日記 正道編
書くのがだいぶ遅くなったけど‥
同じ文字列を表す異なるStringオブジェクトに対してintern()メソッドを呼び出すと、同じStringオブジェクトが返される。のだが、ここに罠が潜んでいる。まぁ、よっぽどのことをしない限りは問題にはならないんだけど。
まず、検証するためのコード。

import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.LinkedList;
import java.util.List;

public class Main {

    // 長さ 60 の文字列を生成するためのformatter
    private static NumberFormat nf
        = new DecimalFormat("000000000000000000000000000000000000000000000000000000000000");

    public static void main(String[] args) {
        boolean makeIntern = (args.length == 1 && args[0].equals("intern"));

        System.err.println(System.getProperty("java.version"));
        List list = new LinkedList();
        long n = 0;
        dumpMemoryUsage();
        try {
            for (;true; ++n) {
//                if (list.size() >= 300000) {
//                    list.remove(0);
//                }
                if (n % 50000 == 0) {
                    System.err.println("... " + n);
                    dumpMemoryUsage();
                }
                String s;
                if (makeIntern) {
                    s = nf.format(n).intern();
                } else {
                    s = nf.format(n);
                }
                list.add(s);
            }
        } finally {
            System.err.println(n);
            dumpMemoryUsage();
        }
    }

    private static void dumpMemoryUsage() {
        Runtime rt = Runtime.getRuntime();
        long max = rt.maxMemory();
        long total = rt.totalMemory();
        long free = rt.freeMemory();
        long used = (total - free);
        System.err.println((max / 1024.0 / 1024)
                + ", " + (total / 1024.0 / 1024)
                + ", " + (free / 1024.0 / 1024)
                + ", " + (used / 1024.0 / 1024));
    }
}

何をやっているかと言うと、長さが60の異なる文字列を次々にLinkedListに追加していく。追加するStringオブジェクトがintern()メソッドが返したものかそうでないかを決定するのは、実行時のパラメータに"intern"を渡すかどうか。
また、メモリの使用状況も分かるように、時々メモリの使用状況を出力する。何らかのエラーが起こったら(また起こらなくても)最後にいくつのStringオブジェクトを追加できたかを出力する。可能ならば最終的なメモリの使用状況も出力する。(本当にメモリがいっぱいいっぱいならば、メッセージ出力どころではないので、出力されない場合もある。)
で、これを、SunのJ2SDK 1.4.2_18で実行してみると、結果は次のようになる。

> java -Xmx64m Main asis
1.4.2_18
63.5625, 1.9375, 1.615875244140625, 0.321624755859375
... 0
63.5625, 1.9375, 1.6134567260742188, 0.32404327392578125
... 50000
63.5625, 14.890625, 5.0833892822265625, 9.807235717773438
... 100000
63.5625, 25.58984375, 6.163841247558594, 19.426002502441406
... 150000
63.5625, 43.58984375, 13.950286865234375, 29.639556884765625
... 200000
63.5625, 43.58984375, 4.451957702636719, 39.13788604736328
... 250000
63.5625, 63.5625, 14.648185729980469, 48.91431427001953
... 300000
63.5625, 63.5625, 4.7720947265625, 58.7904052734375
332606
java.lang.OutOfMemoryError
Exception in thread "main" 

次がヒープメモリの最大サイズを変えた場合の実行結果のすべて。(ただし、途中経過と最後のエラーメッセージは省略する。)

> java -Xmx64m Main asis
... 300000
63.5625, 63.5625, 4.7720947265625, 58.7904052734375
332606
> java -Xmx128m Main asis
... 650000
127.0625, 127.0625, 2.6384201049804688, 124.42407989501953
665529
> java -Xmx256m Main asis
... 1300000
254.0625, 254.0625, 4.87640380859375, 249.18609619140625
1331374
> java -Xmx512m Main asis
... 2650000
508.0625, 508.0625, 0.5318679809570312, 507.53063201904297
2663067
> java -Xmx1024m Main asis
... 5300000
1016.125, 1016.125, 1.187835693359375, 1014.9371643066406
5326777

> java -Xmx64m Main intern
... 400000
63.5625, 15.921875, 5.821815490722656, 10.100059509277344
408694
63.5625, 17.046875, 7.5695953369140625, 9.477279663085938
> java -Xmx128m Main intern
... 400000
127.0625, 15.921875, 5.821815490722656, 10.100059509277344
408694
127.0625, 17.046875, 7.5695953369140625, 9.477279663085938
> java -Xmx256m Main intern
... 400000
254.0625, 15.921875, 5.821815490722656, 10.100059509277344
408694
254.0625, 17.046875, 7.5695953369140625, 9.477279663085938
> java -Xmx512m Main intern
... 400000
508.0625, 15.921875, 5.821815490722656, 10.100059509277344
408694
508.0625, 17.046875, 7.5695953369140625, 9.477279663085938
> java -Xmx1024m Main intern
... 400000
1016.125, 15.921875, 5.821815490722656, 10.100059509277344
408694
1016.125, 17.046875, 7.5695953369140625, 9.477279663085938

前半がintern()メソッドを呼ばないケース、後半がintern()メソッドを呼ぶケース。
見てもらうと分かるが、ヒープメモリの最大サイズに関わらず、intern()メソッドを呼ぶケースでは、長さ60のStringオブジェクトを40万個あまり生成したところで頭打ちとなっている。
これは、JDK 5.0やJDK 6で実行してみると違いがよく分かる。
JDK 5.0 Update 11 で実行した結果を示す。

> java -Xmx64m Main asis
... 350000
63.5625, 63.5625, 1.7101058959960938, 61.852394104003906
361199
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

> java -Xmx64m Main intern
... 400000
63.5625, 16.390625, 6.9474029541015625, 9.443222045898438
416764
63.5625, 17.453125, 7.710029602050781, 9.743095397949219
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

つまり、intern()メソッドを呼び出すと、Permanent領域が、「ある文字列に対応するStringオブジェクトのテーブル」を管理するために消費されるということ。(もしかして、そのStringオブジェクト自体もPermanent領域行きなのかな‥)

要はそれだけの数の“intern化された”Stringオブジェクトを保持するから問題になるのだが、それを確認できるのが、検証コードのコメントアウトした部分。このコメントをはずして実行すると、OutOfMemoryErrorは基本的に発生しなくなる。

どうしてもそれだけの数の“intern化された”Stringオブジェクトを保持したいならば、Permanent領域を広げるしかない。

> java -Xmx256m -XX:MaxPermSize=128m Main intern
... 800000
254.0625, 32.046875, 13.464866638183594, 18.582008361816406
828124
254.0625, 34.234375, 15.1571044921875, 19.0772705078125
java.lang.OutOfMemoryError
Exception in thread "main" 

Permanent領域の最大サイズのデフォルト値は64MBらしい(Javaパフォーマンスチューニング(6):HotSpot VMの特性を知る (2/2) - @IT)。これって、SunのVMの話かなぁ‥