在系統開發中我們經常會使用HashMap作為數據集容器,或者是用緩沖池來處理,一般很穩定,但偶爾也會出現內存溢出的問題(OutOfMemory錯誤),而且這經常是與HashMap有關的.而且這經常是與HashMap有關的.比如我們使用緩沖池操作數據時,大批量的增刪改產操作就可能會讓內存溢出,下面建立一段模擬程序,重現該問題,看代碼:
1 import java.util.HashMap; 2 import java.util.Map; 3 4 public class Client { 5 public static void main(String[] args) { 6 Map<String, String> map = new HashMap<String, String>(); 7 final Runtime rt = Runtime.getRuntime(); 8 // JVM終止前記錄內存信息 9 rt.addShutdownHook(new Thread() { 10 @Override 11 public void run() { 12 StringBuffer sb = new StringBuffer(); 13 long heapMaxSize = rt.maxMemory() >> 20; 14 sb.append("最大可用內存:" + heapMaxSize + "M\n"); 15 long total = rt.totalMemory() >> 20; 16 sb.append("對內存大小:" + total + "M\n"); 17 long free = rt.freeMemory() >> 20; 18 sb.append("空閑內存:" + free + "M"); 19 System.out.println(sb); 20 } 21 }); 22 // 放入40萬鍵值對 23 for (int i = 0; i < 40*10000; i++) { 24 map.put("key" + i, "vlaue" + i); 25 } 26 } 27 }
運行結果:
Exception in thread "main" 最大可用內存:247M
對內存大小:247M
空閑內存:8M
java.lang.OutOfMemoryError: Java heap space
at java.util.HashMap.resize(Unknown Source)
at java.util.HashMap.addEntry(Unknown Source)
at java.util.HashMap.put(Unknown Source)
at cn.summerchill.test.Client.main(Client.java:26)
內存溢出了....可能認為在運行時增加"-Xmx"參數設置內存大小就可以了,這確實可以,不過浮於表面,沒有真正的從溢出的最根本原因上來解決問題.
難道的是String字符串太多了?字符串對象對象加起來撐死最多10MB,而且這里還空閑了7MB內存,不應該報內存溢出.....
或者是put方法有缺陷,產生了內存泄漏?不可能...這里還有7MB的內存可用,應該要用盡了才會出現內存泄漏.....
用ArrayList做一個對比,把相同數據插入到ArrayList中看看會怎么樣,看代碼:
1 import java.util.ArrayList; 2 import java.util.List; 3 4 public class Client { 5 public static void main(String[] args) { 6 List<String> list = new ArrayList<String>(); 7 final Runtime rt = Runtime.getRuntime(); 8 // JVM終止前記錄內存信息 9 rt.addShutdownHook(new Thread() { 10 @Override 11 public void run() { 12 StringBuffer sb = new StringBuffer(); 13 long heapMaxSize = rt.maxMemory() >> 20; 14 sb.append("最大可用內存:" + heapMaxSize + "M\n"); 15 long total = rt.totalMemory() >> 20; 16 sb.append("對內存大小:" + total + "M\n"); 17 long free = rt.freeMemory() >> 20; 18 sb.append("空閑內存:" + free + "M"); 19 System.out.println(sb); 20 } 21 }); 22 // 放入40萬同樣字符串 23 for (int i = 0; i < 502654; i++) { 24 list.add("key" + i); 25 list.add("vlaue" + i); 26 } 27 } 28 }
運行輸出:
最大可用內存:247M
對內存大小:95M
空閑內存:27M
ArrayList運行很正常,沒有出現內存溢出的情況,兩個容器,容納的元素相同,數量相同,ArrayList沒有溢出,但HashMap卻溢出了,很明顯,這與HashMap內部的處理機制有很大的關系.
HashMap在底層也是以數組方式保存元素的,其中每一個鍵值對就是一個元素 ,也就是說HashMap把鍵值對封裝成了一個Entry對象,然后再把Entry放到了數組中,我們簡單看一下Entry類:
java.util.HashMap.Entry<K, V>
1 static class Entry<K,V> implements Map.Entry<K,V> { 2 //鍵 3 final K key; 4 //值 5 V value; 6 //相同哈希碼的下一個元素 7 Entry<K,V> next; 8 9 final int hash; 10 //key,value的getter和setter方法,以及重寫的equals,hashCode,toString方法 11 }
HashMap底層的數組變量名叫table,它是Entry類型的數組,保存的是一個個的鍵值對(在我們的例子中Entry是由兩個String類型組成的).對我們的例子來說,HashMap比ArrayList多了一次封裝,把String類型的鍵值對轉換成Entry對象后再放入數組,這就多了40萬個對象,這應該是問題產生的一個原因.
我們知道HashMap的長度也是可以動態增加的,它的擴容機制與ArrayList稍有不同,其代碼如下:
if (size++ >= threshold)
resize(2 * table.length);
在插入鍵值對時,會做長度校驗,如果大於或等於閥值(threshold變量),則數組長度增大一倍,不過,默認的閥值是多大呢?默認是當前長度與加載因子的乘積.
threshold = (int) (newCapacity * loadFactory);
默認的加載因子(loadFactor變量)是0.75,也就是說只要HashMap的size大於數組長度的0.75倍時,就開始擴容,經過計算得知(怎么計算,查找2的N次方大於40萬的最小值即為數組的最大長度,再乘以0.75就是最后一次擴容點,計算的結果是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中的元素少量並簡單.
//============================================
模擬List和Map的長度增長.....
1 public class Client { 2 public static void main(String[] args) { 3 //Map的最后一次擴容 4 int mapSize =16; 5 for(int i=0;i<100;i++){ 6 mapSize = mapSize * 2; 7 if(mapSize > 40*10000){ 8 System.out.println(i); 9 System.out.println("map的最后一次擴容:" + (mapSize *3/4)); 10 return; 11 } 12 } 13 14 int listSize = 10; 15 for (int i = 1; i < 1000; i++) { 16 listSize = (listSize * 3) / 2 + 1; 17 if (listSize > 40 * 10000 * 2) { 18 System.out.println("list的最后一次擴容:"+listSize); 19 return; 20 } 21 } 22 23 } 24 25 }