1. 程式人生 > >Java集合框架:WeakHashMap

Java集合框架:WeakHashMap

WeakHashMap定義

package java.util;
import java.lang.ref.WeakReference;
import java.lang.ref.ReferenceQueue;

public class WeakHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V> {
}

  WeakHashMap實現了Map介面,是HashMap的一種實現,它比HashMap多了一個引用佇列:

private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

  博主認真比對過WeakHashMap和JDK7 HashMap的原始碼,發現WeakHashMap中方法的實現方式基本和JDK7 HashMap的一樣,注意“基本”兩個字,除了沒有實現Cloneable和Serializable這兩個標記介面,最大的區別在於在於expungeStaleEntries()這個方法,這個是整個WeakHashMap的精髓,我們稍後進行闡述。
  它使用弱引用作為內部資料的儲存方案。WeakHashMap是弱引用的一種典型應用,它可以為簡單的快取表解決方案。

WeakHashMap使用

  我們舉一個簡單的例子來說明一下WeakHashMap的使用:

		Map<String,Integer> map = new WeakHashMap<>();
        map.put("s1", 1);
        map.put("s2", 2);
        map.put("s3", 3);
        map.put("s4", 4);
        map.put("s5", 5);
        map.put(null, 9);
        map.put("s6", 6);
        map.put("s7", 7);
        map.put("s8", 8);
        map.put(null, 11);
        for(Map.Entry<String,Integer> entry:map.entrySet())
        {
            System.out.println(entry.getKey()+":"+entry.getValue());
        }
        System.out.println(map);

  執行結果:

s4:4
s3:3
s6:6
null:11
s5:5
s8:8
s7:7
s1:1
s2:2
{s4=4, s3=3, s6=6, null=11, s5=5, s8=8, s7=7, s1=1, s2=2}

  WeakHashMap和HashMap一樣key和value的值都可以為null,並且也是無序的。但是HashMap的null是存在table[0]中的,這是固定的,並且null的hash為0,而在WeakHashMap中的null卻沒有存入table[0]中。
  這是因為WeakHashMap對null值進行了包裝:

 private static final Object NULL_KEY = new Object();
    private static Object maskNull(Object key) {
        return (key == null) ? NULL_KEY : key;
    }
    static Object unmaskNull(Object key) {
        return (key == NULL_KEY) ? null : key;
    }

  當對map進行put和get操作的時候,將null值標記為NULL_KEY,然後對NULL_KEY即對new Object()進行與其他物件一視同仁的hash,這樣就使得null和其他非null的值毫無區別。

JDK關鍵原始碼分析

  首先看一下Entry<K,V>這個靜態內部類:

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        int hash;
        Entry<K,V> next;

        /**
         * Creates new entry.
         */
        Entry(Object key, V value,
              ReferenceQueue<Object> queue,
              int hash, Entry<K,V> next) {
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
    //其餘程式碼略
}

  可以看到Entry繼承擴充套件了WeakReference類(有關Java的引用型別可以參考《Java引用型別》)。並在其建構函式中,構造了key的弱引用。
  此外,在WeakHashMap的各項操作中,比如get()、put()、size()都間接或者直接呼叫了expungeStaleEntries()方法,以清理持有弱引用的key的表象。
  expungeStaleEntries()方法的實現如下:

private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                    Entry<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);

                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                while (p != null) {
                    Entry<K,V> next = p.next;
                    if (p == e) {
                        if (prev == e)
                            table[i] = next;
                        else
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }

  可以看到每呼叫一次expungeStaleEntries()方法,就會在引用佇列中尋找是否有被清楚的key物件,如果有則在table中找到其值,並將value設定為null,next指標也設定為null,讓GC去回收這些資源。

案例應用

  如果在一個普通的HashMap中儲存一些比較大的值如下:

        Map<Integer,Object> map = new HashMap<>();
        for(int i=0;i<10000;i++)
        {
            Integer ii = new Integer(i);
            map.put(ii, new byte[i]);
        }

  執行引數:-Xmx5M
  執行結果:

     Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at collections.WeakHashMapTest.main(WeakHashMapTest.java:39)

  同樣我們將HashMap換成WeakHashMap其餘都不變:

         Map<Integer,Object> map = new WeakHashMap<>();
        for(int i=0;i<10000;i++)
        {
            Integer ii = new Integer(i);
            map.put(ii, new byte[i]);
        }

  執行結果:(無任何報錯)
  這兩段程式碼比較可以看到WeakHashMap的功效,如果在系統中需要一張很大的Map表,Map中的表項作為快取只用,這也意味著即使沒能從該Map中取得相應的資料,系統也可以通過候選方案獲取這些資料。雖然這樣會消耗更多的時間,但是不影響系統的正常執行。
  在這種場景下,使用WeakHashMap是最合適的。因為WeakHashMap會在系統記憶體範圍內,儲存所有表項,而一旦記憶體不夠,在GC時,沒有被引用的表項又會很快被清除掉,從而避免系統記憶體溢位。
  我們這裡稍微改變一下上面的程式碼(加了一個List):

Map<Integer,Object> map = new WeakHashMap<>();
        List<Integer> list = new ArrayList<>();
        for(int i=0;i<10000;i++)
        {
            Integer ii = new Integer(i);
            list.add(ii);
            map.put(ii, new byte[i]);
        }   

  執行結果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at collections.WeakHashMapTest.main(WeakHashMapTest.java:43)

  如果存放在WeakHashMap中的key都存在強引用,那麼WeakHashMap就會退化成HashMap。如果在系統中希望通過WeakHashMap自動清楚資料,請儘量不要在系統的其他地方強引用WeakHashMap的key,否則,這些key就不會被回收,WeakHashMap也就無法正常釋放它們所佔用的表項

博主如是說:要想WeakHashMap能夠釋放掉key被GC的value的物件,儘可能的多呼叫下put/size/get等操作,因為這些方法會呼叫expungeStaleEntries方法,expungeStaleEntries方法是關鍵,而如果不操作WeakHashMap,以企圖WeakHashMap“自動”釋放記憶體是不可取的,這裡的“自動”是指譬如map.put(obj,new byte[10M]);之後obj=null了,之後再也沒掉用過map的任何方法,那麼new出來的10M空間是不會釋放的。

疑問

  樓主對於WeakHashMap一直有一個疑問,是這樣的:
  我們知道WeakHashMap的key可以為null,那麼當put一個key為null,value為一個很大物件的時候,這個很大的物件怎麼採用WeakHashMap的自帶的功能自動釋放呢?
  程式碼如下:

Map<Object,Object> map = new WeakHashMap<>();
        map.put(null,new byte[5*1024*928]);
        int i = 1;
        while(true)
        {
            System.out.println();
            TimeUnit.SECONDS.sleep(2);
            System.out.println(map.size());
            System.gc();
            System.out.println("==================第"+i+++"次GC結束====================");
        }

  執行引數:-Xmx5M -XX:+PrintGCDetails
  執行結果:

1
[GC [PSYoungGen: 680K->504K(2560K)] 5320K->5240K(7680K), 0.0035741 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 504K->403K(2560K)] [ParOldGen: 4736K->4719K(5120K)] 5240K->5123K(7680K) [PSPermGen: 2518K->2517K(21504K)], 0.0254473 secs] [Times: user=0.06 sys=0.00, real=0.03 secs] 
==================第1次GC結束====================

1
[Full GC [PSYoungGen: 526K->0K(2560K)] [ParOldGen: 4719K->5112K(5120K)] 5246K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0172785 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
==================第2次GC結束====================

1
[Full GC [PSYoungGen: 41K->0K(2560K)] [ParOldGen: 5112K->5112K(5120K)] 5153K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0178421 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
==================第3次GC結束====================

1
[Full GC [PSYoungGen: 41K->0K(2560K)] [ParOldGen: 5112K->5112K(5120K)] 5153K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0164874 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
==================第4次GC結束====================

1
[Full GC [PSYoungGen: 41K->0K(2560K)] [ParOldGen: 5112K->5112K(5120K)] 5153K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0191096 secs] [Times: user=0.05 sys=0.00, real=0.02 secs] 
==================第5次GC結束====================
(一直迴圈下去)

  可以看到在map.put(null,new byte[51024928]);之後,相應的記憶體一直沒有得到釋放
  通過顯式的呼叫map.remove(null)可以將記憶體釋放掉(如下程式碼所示)

    Map<Integer,Object> map = new WeakHashMap<>();
        System.gc();
        System.out.println("===========gc:1=============");
        map.put(null,new byte[4*1024*1024]);
        TimeUnit.SECONDS.sleep(5);
        System.gc();
        System.out.println("===========gc:2=============");
        TimeUnit.SECONDS.sleep(5);
        System.gc();
        System.out.println("===========gc:3=============");
        map.remove(null);
        TimeUnit.SECONDS.sleep(5);
        System.gc();
        System.out.println("===========gc:4=============");

  執行引數:-Xmx5M -XX:+PrintGCDetails
  執行結果:

[GC [PSYoungGen: 720K->504K(2560K)] 720K->544K(6144K), 0.0023652 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 40K->480K(3584K)] 544K->480K(6144K) [PSPermGen: 2486K->2485K(21504K)], 0.0198023 secs] [Times: user=0.11 sys=0.00, real=0.02 secs] 
===========gc:1=============
[GC [PSYoungGen: 123K->32K(2560K)] 4699K->4608K(7680K), 0.0026722 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 4576K->4578K(5120K)] 4608K->4578K(7680K) [PSPermGen: 2519K->2519K(21504K)], 0.0145734 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
===========gc:2=============
[GC [PSYoungGen: 40K->32K(2560K)] 4619K->4610K(7680K), 0.0013068 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 4578K->4568K(5120K)] 4610K->4568K(7680K) [PSPermGen: 2519K->2519K(21504K)], 0.0189642 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] 
===========gc:3=============
[GC [PSYoungGen: 40K->32K(2560K)] 4609K->4600K(7680K), 0.0011742 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 4568K->472K(5120K)] 4600K->472K(7680K) [PSPermGen: 2519K->2519K(21504K)], 0.0175907 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
===========gc:4=============
Heap
 PSYoungGen      total 2560K, used 82K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 4% used [0x00000000ffd00000,0x00000000ffd14820,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 5120K, used 472K [0x00000000ff800000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 5120K, 9% used [0x00000000ff800000,0x00000000ff876128,0x00000000ffd00000)
 PSPermGen       total 21504K, used 2526K [0x00000000fa600000, 0x00000000fbb00000, 0x00000000ff800000)
  object space 21504K, 11% used [0x00000000fa600000,0x00000000fa8778f8,0x00000000fbb00000)

  如果真是隻有通過remove的方式去刪除null的鍵所指向的value的話,博主建議在使用WeakHashMap的時候儘量避免使用null作為鍵。如果有大神可以解答一下這個問題,請在下方留言。

參考資料:

  1. 《Java程式效能優化——讓你的Java程式更快、更穩定》葛一鳴 等編著。

歡迎支援《RabbitMQ實戰指南》以及關注微信公眾號:朱小廝的部落格。