[改善Java代碼]減少HashMap中元素的數量


在系統開發中我們經常會使用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 }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM