一文搞懂WeakHashMap工作原理(java後端面試高薪必備知識點)

這個問題是一個高頻面試題,本篇文章將從概念、原理、實際使用的角度來分析。希望對你有幫助:

一、什麼是WeakHashMap?

從名字可以得知主要和Map有關,不過還有一個Weak,我們就更能自然而然的想到這裏面還牽扯到一種弱引用結構,因此想要徹底搞懂,我們還需要知道四種引用。如果你已經知道了,可以跳過。

1、四種引用

在jvm中,一個對象如果不再被使用就會被當做垃圾給回收掉,判斷一個對象是否是垃圾,通常有兩種方法:引用計數法和可達性分析法。不管是哪一種方法判斷一個對象是否是垃圾的條件總是一個對象的引用是都沒有了。

JDK.1.2 之後,Java 對引用的概念進行了擴充,將引用分爲了:強引用、軟引用、弱引用、虛引用4 種。而我們的WeakHashMap就是基於弱引用。

(1)強引用

如果一個對象具有強引用,它就不會被垃圾回收器回收。即使當前內存空間不足,JVM也不會回收它,而是拋出 OutOfMemoryError 錯誤,使程序異常終止。比如String str = "hello"這時候str就是一個強引用。

(2)軟引用

內存足夠的時候,軟引用對象不會被回收,只有在內存不足時,系統則會回收軟引用對象,如果回收了軟引用對象之後仍然沒有足夠的內存,纔會拋出內存溢出異常。

(3)弱引用

如果一個對象具有弱引用,在垃圾回收時候,一旦發現弱引用對象,無論當前內存空間是否充足,都會將弱引用回收。

(4)虛引用

如果一個對象具有虛引用,就相當於沒有引用,在任何時候都有可能被回收。使用虛引用的目的就是爲了得知對象被GC的時機,所以可以利用虛引用來進行銷燬前的一些操作,比如說資源釋放等。

我們的WeakHashMap是基於弱引用的,也就是說只要垃圾回收機制一開啓,就直接開始了掃蕩,看見了就清除。

2、爲什麼需要WeakHashMap

WeakHashMap正是由於使用的是弱引用,因此它的對象可能被隨時回收。更直觀的說,當使用 WeakHashMap 時,即使沒有刪除任何元素,它的尺寸、get方法也可能不一樣。比如:

(1)調用兩次size()方法返回不同的值;第一次爲10,第二次就爲8了。

(2)兩次調用isEmpty()方法,第一次返回false,第二次返回true;

(3)兩次調用containsKey()方法,第一次返回true,第二次返回false;

(4)兩次調用get()方法,第一次返回一個value,第二次返回null;

是不是覺得有點噁心,這種飄忽不定的東西好像沒什麼用,試想一下,你準備使用WeakHashMap保存一些數據,寫着寫着都沒了,那還保存個啥呀。

不過有一種場景,最喜歡這種飄忽不定、一言不合就刪除的東西。那就是緩存。在緩存場景下,由於內存是有限的,不能緩存所有對象,因此就需要一定的刪除機制,淘汰掉一些對象。

現在我們已經知道了WeakHashMap是基於弱引用,其對象可能隨時被回收,適用於緩存的場景。下面我們就來看看,WeakHashMap是如何實現這些功能。

二、WeakHashMap工作原理

1、WeakHashMap爲什麼具有弱引用的特點:隨時被回收對象

這個問題就比較簡單了,我們的目的主要是驗證。WeakHashMap是基於弱引用的,肯定就具有了弱引用的性質。我們去他的源碼中看一下:

   private static class Entry<K,V> 
   extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> next;
        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,也就是弱引用,所以就具有了弱引用的特點。不過還要注意一點,那就是ReferenceQueue,他的作用是GC會**清理掉對象之後,**引用對象會被放到ReferenceQueue中。

2、WeakHashMap中的Entry被GC後,WeakHashMap是如何將其移除的?

意思是某一個Entry突然被垃圾回收了,這之後WeakHashMap肯定就不能保留這個Entry了,那他是如何將其移除的呢?

WeakHashMap內部有一個expungeStaleEntries函數,在這個函數內部實現移除其內部不用的entry從而達到的自動釋放內存的目的。因此我們每次訪問WeakHashMap的時候,都會調用這個expungeStaleEntries函數清理一遍。這也就是爲什麼前兩次調用WeakHashMap的size()方法有可能不一樣的原因。我們可以看看是如何實現的:

    private void expungeStaleEntries() {
        //(1)for循環遍歷每一個被清除地對象Entry。
        for (Object x; (x = queue.poll()) != null; ) {
            //(2)synchronized (queue)加鎖刪除
            synchronized (queue) {
                //(3)indexFor計算此entry在桶的位置,並找到Entry
                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;
                //(4)while循環開始移動指針進行刪除
                while (p != null) {
                    Entry<K,V> next = p.next;
                    if (p == e) {
                        if (prev == e) table[i] = next;
                        else prev.next = next;
                        e.value = null; // Help GC,加速回收
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }

首先GC每次清理掉一個對象之後,引用對象會被放到ReferenceQueue中。然後遍歷這個queue進行刪除即可。

當然。WeakHashMap的增刪改查操作都會直接或者間接的調用expungeStaleEntries()方法,達到及時清除過期entry的目的。

三、WeakHashMap的使用

1、緩存中使用

在文章一開始提到WeakHashMap可以在緩存中使用。下面我們看一下如何使用的:

WeakHashMap<UniqueImageName, BigImage> map = new WeakHashMap<>();
BigImage bigImage = new BigImage("image_id");
UniqueImageName imageName = new UniqueImageName("name_of_big_image");
 
map.put(imageName, bigImage);
assertTrue(map.containsKey(imageName));
 
imageName = null;
System.gc();
 
await().atMost(10, TimeUnit.SECONDS).until(map::isEmpty);

本案例來源於:https://www.baeldung.com/java-weakhashmap。一個外國兄弟寫的,很棒。

2、不要使用基礎類型作爲WeakHashMap的key

緩存的使用案例太多了,這裏舉一個WeakHashMap使用不規範的例子。

public static void main(String[] args) {
	WeakHashMap<Object, Object> objectMap 
        =  new WeakHashMap<Object, Object>();
	for (int i = 0; i < 1000; i++) {
		objectMap.put(i, new Object());
	    System.gc();
		System.out.println("Map size :" + objectMap.size());
    }
}

objectMap.put方法執行的時候i會被封裝爲Integer類型的,Integer保留了-128到127的緩存。但是對於int來說範圍大很多,因此哪些Key <= 127的Entry將不會進行自動回收,但是那些大於127的將會被回收,因此最後的尺寸總是會穩定在128左右。
在這裏插入圖片描述

OK。今天的文章先說到這。如有問題還請批評指正。目前公衆號已由“java的架構師技術棧”改名爲“愚公要移山”。感謝各位的支持。

一個人炫耀什麼,說明內心缺少什麼,我是愚公,要移山。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章