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の話かなぁ‥