在系統開發中,我們經常會使用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中的元素少量並簡單。