讀古今文學網 > 編寫高質量代碼:改善Java程序的151個建議 > 建議78:減少HashMap中元素的數量 >

建議78:減少HashMap中元素的數量

在系統開發中,我們經常會使用HashMap作為數據集容器,或者是用緩衝池來處理,一般很穩定,但偶爾也會出現內存溢出的問題(如OutOfMemory錯誤),而且這經常是與HashMap有關的,比如我們使用緩衝池操作數據時,大批量的增刪查改操作就可能會讓內存溢出,下面建立一段模擬程序,重現該問題,代碼如下:


public static void main(Stringargs){

Map<String, String>map=new HashMap<String, String>();

final Runtime rt=Runtime.getRuntime();

//JVM終止前記錄內存信息

rt.addShutdownHook(new Thread(){

@Override

public void run(){

StringBuffer sb=new StringBuffer();

long heapMaxSize=rt.maxMemory()>>20;

sb.append(\"最大可用內存:\"+heapMaxSize+\"Mn\");

long total=rt.totalMemory()>>20;

sb.append(\"對內存大小:\"+total+\"Mn\");

long free=rt.freeMemory()>>20;

sb.append(\"空閒內存:\"+free+\"M\");

System.out.println(sb);

}

});

//放入近40萬鍵值對

for(int i=0;i<393217;i++){

map.put(\"key\"+i,\"vlaue\"+i);

}

}


該程序只是向Map中放入了近40萬個鍵值對元素(不是整40萬個,而是393217個,為什麼呢?請繼續往後看),只是增加,沒有任何其他操作。想想看,會出現什麼問題?內存溢出?運行結果如下所示:


Exception in thread\"main\"最大可用內存:63M

java.lang.OutOfMemoryError:Java heap space

at java.util.HashMap.resize(HashMap.java:462)

at java.util.HashMap.addEntry(HashMap.java:755)

at java.util.HashMap.put(HashMap.java:385)

at Client.main(Client.java:24)

對內存大小:63M

空閒內存:7M


內存溢出了!可能會有讀者說,這很好解決,在運行時增加\"-Xmx\"參數設置內存大小即可。這確實可以,不過浮於表面了,沒有真正從溢出的最根本原因上來解決問題。

難道是String字符串太多了?不對呀,字符串對像加起來撐死也就10MB,而且這裡還空閒了7MB內存,不應該報內存溢出呀?

或者是put方法有缺陷,產生了內存洩露?不可能,這裡還有7MB內存可用,應該要用盡了才會出現內存洩露啊。

為了更加清晰地理解該問題,我們與ArrayList做一個對比,把相同數據插入到ArrayList中看看會怎麼樣,代碼如下:


public static void main(Stringargs){

List<String>list=new ArrayList<String>();

/*Runtime增加的鉤子函數相同,不再贅述*/

//放入40萬同樣字符串

for(int i=0;i<400000;i++){

list.add(\"key\"+i);

list.add(\"vlaue\"+i);

}

}


同樣的程序,只是把HashMap修改成了List,增加的字符串元素也相同(只是HashMap將其拆分成了兩個字符串,一個是key,一個是value,此處則是把兩個字符串放到list中),我們來看運行結果:


最大可用內存:63M

對內存大小:63M

空閒內存:11M


ArrayList運行很正常,沒有出現內存溢出情況。兩個容器,容納的元素相同,數量相同,ArrayList沒有溢出,但HashMap卻溢出了。很明顯,這與HashMap內部的處理機制有極大的關係。

HashMap在底層也是以數組方式保存元素的,其中每一個鍵值對就是一個元素,也就是說HashMap把鍵值對封裝成了一個Entry對象,然後再把Entry放到了數組中,我們簡單看一下Entry類:


static class Entry<K, V>implements Map.Entry<K, V>{

//鍵

final K key;

//值

V value;

//相同哈希碼的下一個元素

Entry<K, V>next;

final int hash;

/*key、value的getter/setter方法,以及重寫的equals、hashCode、toString方法*/

}

}


HashMap底層的數組變量名叫table,它是Entry類型的數組,保存的是一個一個的鍵值對(在我們的例子中Entry是由兩個String類型組成的)。再回過頭來想想,對我們的例子來說,HashMap比ArrayList多了一次封裝,把String類型的鍵值對轉換成Entry對像後再放入數組,這就多了40萬個對象,這應該是問題產生的第一個原因。

我們知道HashMap的長度也是可以動態增加的,它的擴容機制與ArrayList稍有不同,其代碼如下:


if(size++>=threshold)

resize(2*table.length);


在插入鍵值對時,會做長度校驗,如果大於或等於閥值(threshold變量),則數組長度增大一倍。不過,默認的閥值是多大的呢?默認是當前長度與加載因子的乘積。


threshold=(int)(newCapacity*loadFactor);


默認的加載因子(loadFactor變量)是0.75,也就是說只要HashMap的size大於數組長度的0.75倍時,就開始擴容,經過計算得知(怎麼計算的?查找2的N次冪大於40萬的最小值即為數組的最大長度,再乘以0.75就是最後一次擴容點,計算的結果是N=19),在Map的size為393216時,符合了擴容條件,於是393216個元素準備開始大搬家,要擴容嘛,那首先要申請一個長度為1048576(當前長度的兩倍嘛,2的19次方再乘以2,即2的20次方)的數組,但問題是此時剩餘的內存只有7MB了,不足以支撐此運算,於是就報內存溢出了!這是第二個原因,也是最根本的原因。

這也就解釋了為什麼還剩餘著7MB內存就報內存溢出了。我們再來思考一下ArrayList的擴容策略,它是在小於數組長度的時候才會擴容1.5倍,經過計算得知,ArrayList的size在超過80萬後(一次加兩個元素,40萬的兩倍),最近的一次擴容會是在size為1005308時,也就是說,如果程序設置了增加元素的上限為502655,同樣會報內存溢出,因為它也要申請一個1507963長度的數組,如果沒這麼大的地方,就會報錯了。

綜合來說,HashMap比ArrayList多了一個層Entry的底層對像封裝,多佔用了內存,並且它的擴容策略是2倍長度的遞增,同時還會依據閥值判斷規則進行判斷,因此相對於ArrayList來說,它就會先出現內存溢出。

可能會有讀者在想,是不是可以在聲明時指定HashMap的默認長度和加載因子來減少此問題的發生。是可以緩解此問題,可以不再頻繁地進行數組擴容,但仍然避免不了內存溢出問題,因為鍵值對的封裝對像Entry還是少不了的,內存依然增長較快。

注意 盡量讓HashMap中的元素少量並簡單。