java.util.HashMap のインスタンスを使いまわしてはいけない
java.util.HashMap クラスは、言わずと知れた、keyとvalueの組を保持するMap実装である。
HashMap (Java 2 Platform SE 5.0)
java.io.ByteArrayOutputStream のエントリを書いている最中に言われて(謎)気付いた。
このクラスでも、内部で配列を使用しており、この配列がハッシュテーブルを表現している。java.io.ByteArrayOutputStreamクラスと同様に、一旦長さを拡張した配列の長さが短くなることはない。
Mapなどを使いまわすなと言われても、サーバープログラムなんかでは何かをキャッシュしたりするのに使ったりするので厳しい面もあるが、要はこういう問題があるのでちゃんと考えなきゃね、ということで。
で、ちょっと検証。
HashMap内部の配列(tableフィールド)はpackage privateなので、リフレクションAPIを使って無理矢理取得するためのメソッドを追加したクラスを使用する。
import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; public class HashMapX<K,V> extends HashMap<K,V> { /** * <code>serialVersionUID</code> のコメント */ private static final long serialVersionUID = -259216577698633608L; public HashMapX() { } public HashMapX(int initialCapacity) { super(initialCapacity); } public HashMapX(Map<K,V> m) { super(m); } public HashMapX(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); } public Object[] getRawBuffer() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { // HashMapクラスのフィールド'table'を無理矢理取得 Object[] res; Field field = this.getClass().getSuperclass().getDeclaredField("table"); boolean oldAccessible = field.isAccessible(); field.setAccessible(true); try { res = (Object[]) field.get(this); } finally { field.setAccessible(oldAccessible); } return res; } }
検証のためのコード。
public class Main { /** * @param args * @throws IllegalAccessException * @throws NoSuchFieldException * @throws IllegalArgumentException * @throws SecurityException */ public static void main(String[] args) throws SecurityException, IllegalArgumentException, NoSuchFieldException, IllegalAccessException { System.out.println("java.version = " + System.getProperty("java.version")); System.out.println("java.vendor = " + System.getProperty("java.vendor")); System.out.println("os.name = " + System.getProperty("os.name")); dump("initial", null); HashMapX<Integer, String> map = new HashMapX<Integer, String>(1024); dump("gen map of 1024", map); for (int i = 0; i < 1000000; ++i) { map.put(new Integer(i), "hello world"); } dump("put 1M entry", map); map.clear(); dump("clear", map); map.put(new Integer(0), String.valueOf(0)); dump("put 1 entry", map); map = null; dump("set to null", map); } private static void dump(String label, HashMapX<Integer, String> bout) throws SecurityException, IllegalArgumentException, NoSuchFieldException, IllegalAccessException { System.out.println("=== " + label + " ==="); int size = -1; int len = -1; if (bout != null) { size = bout.size(); len = bout.getRawBuffer().length; } System.gc(); System.gc(); Runtime rt = Runtime.getRuntime(); long max = rt.maxMemory(); long total = rt.totalMemory(); long free = rt.freeMemory(); long used = total - free; System.out.printf("(%10d, %10d) (%10.6f, %10.6f, %10.6f, %10.6f)\n", size, len, (max / 1024.0 / 1024), (total / 1024.0 / 1024), (free / 1024.0 / 1024), (used / 1024.0 / 1024)); } }
そして結果。
java.version = 1.5.0_12 java.vendor = Sun Microsystems Inc. os.name = Windows XP === initial === ( -1, -1) ( 63.562500, 1.937500, 1.791595, 0.145905) === gen map of 1024 === ( 0, 1024) ( 63.562500, 1.937500, 1.734108, 0.203392) === put 1M entry === ( 1000000, 2097152) ( 63.562500, 63.562500, 17.233841, 46.328659) === clear === ( 0, 2097152) ( 63.562500, 60.191406, 52.009750, 8.181656) === put 1 entry === ( 1, 2097152) ( 63.562500, 29.398438, 21.216705, 8.181732) === set to null === ( -1, -1) ( 63.562500, 1.941406, 1.759804, 0.181602)
clear()メソッドを呼ぶと、ヒープメモリ使用量は減るが、これは単に保持していたオブジェクトがGCで回収されただけ。オブジェクトをputする前と比べると8MB増加している。
HashMapクラスのソースコードを見てみると、
- clear()メソッドは、配列tableの要素をnullにする
だけであるから、やはり配列の長さが縮むはずはない。