One article is enough to fix the working principle of WeakHashMap!!!

1, What is WeakHashMap?

From the name, we can know that it is mainly related to Map, but there is also a break, so we can naturally think that there is also a Weak reference structure involved. Therefore, to fully understand, we need to know four kinds of references.

Strong reference:
If an object has a strong reference, it will not be collected by the garbage collector. Even if the current memory space is insufficient, the JVM will not recycle it, but will throw OutOfMemoryError error to cause the program to terminate abnormally.
For example, String str = "hello" in this case, str is a strong reference.

Soft reference:
When there is enough memory, the soft reference object will not be recycled. Only when there is insufficient memory, the system will recycle the soft reference object. If there is still not enough memory after recycling the soft reference object, a memory overflow exception will be thrown.

Weak reference:
If an object has a weak reference, once a weak reference object is found during garbage collection, the weak reference will be recycled regardless of whether the current memory space is sufficient.

Virtual reference:
If an object has a virtual reference, it is equivalent to no reference and may be recycled at any time.
The purpose of using virtual reference is to know when the object is GC. Therefore, virtual reference can be used for some operations before destruction, such as resource release.

WeakHashMap is based on weak reference, that is, as soon as the garbage collection mechanism is turned on, it will start sweeping directly and clear it when it is seen

2, Why do you need WeakHashMap?

Because WeakHashMap uses weak references, its objects may be recycled at any time.

More intuitively, when using WeakHashMap, even if no element is deleted, its size and get method may be different.

For example:

(1) Call the size() method twice to return different values; The first time is 10 and the second time is 8.

(2) Call isEmpty() method twice, return false for the first time and true for the second time;

(3) Call containsKey() method twice, return true for the first time and false for the second time;

(4) Call the get() method twice, returning a value for the first time and null for the second time;

Do you feel a little sick? This kind of erratic thing seems useless. Imagine that you are going to use WeakHashMap to save some data. What else do you want to save.

However, there is a scene that likes to delete things that are erratic and inconsistent.

That is caching. In the caching scenario, due to the limited memory, all objects cannot be cached. Therefore, a certain deletion mechanism is required to eliminate some objects

Now we know that WeakHashMap is based on weak reference, and its objects may be recycled at any time, which is suitable for cached scenes.

3, Example of WeakHashMap

public class TestWeakHashMap {

    public static void main(String[] args) {
        WeakHashMap<String, String> weakHashMap = new WeakHashMap<>(10);

        String key0 = new String("str1");
        String key1 = new String("str2");
        String key2 = new String("str3");

        // Store elements
        weakHashMap.put(key0, "data1");
        weakHashMap.put(key1, "data2");
        weakHashMap.put(key2, "data3");
        System.out.printf("weakHashMap: %s\n", weakHashMap);

        // Include a key
        System.out.printf("contains key str1 : %s\n", weakHashMap.containsKey(key0));
        System.out.printf("contains key str2 : %s\n", weakHashMap.containsKey(key1));

        // Remove key
        weakHashMap.remove(key0);
        System.out.printf("weakHashMap after remove: %s", weakHashMap);

        // This means that the "weak key" key1 is no longer referenced by other objects, and the key value pair corresponding to key1 in the WeakHashMap will be recycled when calling gc
        key1 = null;
        // Memory recycling, where the key value pair corresponding to "key0" in the WeakHashMap will be recycled
        System.gc();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Traverse WeakHashMap
        for (Map.Entry<String, String> m : weakHashMap.entrySet()) {
            System.out.printf("next : %s >>> %s\n", m.getKey(), m.getValue());
        }
        // Print the actual size of the WeakHashMap
        System.out.printf("after gc WeakHashMap size: %s\n", weakHashMap.size());
    }
}


The above example shows the addition, deletion, modification and query of WeakHashMap and the recycling of weak keys. You can see that the Key reference is set to null,
After gc, the key value pair is recycled.

4, Usage scenario of WeakHashMap

It is generally used for caching. For example, in Tomcat's source code, WeakHashMap is used to implement caching,
In the caching system, using WeakHashMap can avoid memory leakage. However, when using WeakHashMap for caching, it should be noted that if only its key is used by the WeakHashMap itself, and there is no strong reference to the key outside the WeakHashMap, the entry corresponding to the key will be recycled during GC.

Therefore, the WeakHashMap cannot be used as the primary cache. The appropriate usage should be to use it as the secondary memory cache, that is, expired cache data or low-frequency cache data

public final class ConcurrentCache<K,V> {
 
    private final int size;
 
    private final Map<K,V> eden;
 
    private final Map<K,V> longterm;
 
    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap<>(size);
        this.longterm = new WeakHashMap<>(size);
    }
 
    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            synchronized (longterm) {
                v = this.longterm.get(k);
            }
            if (v != null) {
                this.eden.put(k, v);
            }
        }
        return v;
    }
 
    public void put(K k, V v) {
        if (this.eden.size() >= size) {
            synchronized (longterm) {
                this.longterm.putAll(this.eden);
            }
            this.eden.clear();
        }
        this.eden.put(k, v);
    }
}

There are two maps of eden and longterm in the source code. If you know something about the jvm heap, you can guess that tomcat uses ConcurrentHashMap and WeakHashMap for generation caching.

In the put method, when inserting a k-v, first check whether the capacity of eden cache is exceeded.

If there is no super, it will be directly put into the eden cache. If it is, lock the longterm and put all k-v in eden into the longterm.

Then empty eden and insert k-v.

In the get method, it is also a priority to find the corresponding v from eden. If not, it will enter the longterm cache. After finding it, it will be added to the eden cache and returned.

Through this design, the relatively common objects can be found in the eden cache, and the less common objects (objects that may be destroyed) will enter the longterm cache.

When there is no other reference to the actual object of the key of the longterm, the gc will automatically recycle the actual object pointed to by the weak reference in the heap, and the weak reference will enter the reference queue.

longterm calls the expungeStaleEntries() method to traverse the weak references in the reference queue and clear the corresponding entries without wasting memory space.

5, Data structure of WeakHashMap

1. Definition of class

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

2. Constants and variables

    private static final int DEFAULT_INITIAL_CAPACITY = 16;
 
    private static final int MAXIMUM_CAPACITY = 1 << 30;
 
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
 
    Entry<K,V>[] table;
 
    private int size;
 
    private int threshold;
 
    private final float loadFactor;
 
    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
 
    int modCount;
  • DEFAULT_ INITIAL_ Capability: initial capacity
  • MAXIMUM_ Capability: maximum capacity
  • DEFAULT_LOAD_FACTOR: default load factor
  • Table: entry array
  • size: the number of data actually stored
  • Threshold: capacity expansion threshold
  • loadFactor: load factor
  • Queue: reference queue
  • modCount: modification times

3. Entry class

   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;
        }
 
        @SuppressWarnings("unchecked")
        public K getKey() {
            return (K) WeakHashMap.unmaskNull(get());
        }
 
        public V getValue() {
            return value;
        }
 
        public V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        ...
    }

You can see that the entry class implements the Map.Entry interface, inherits weak references, and the attributes have key, value and next references.

A reference queue needs to be passed in the constructor. The method mainly depends on getKey():

return (K) WeakHashMap.unmaskNull(get());

Let's look at the static method unmaskNull() of WeakHashMap:

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

Determine whether the key is equal to NULL_KEY to select whether to return null.

4. Class diagram

6, Weak key recovery of WeakHashMap

After reading the data structure of WeakHashMap, how does WeakHashMap realize weak key recycling?

In fact, as you can guess from the previous articles, use Reference and ReferenceQueue.

1. Input data

    public V put(K key, V value) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int i = indexFor(h, tab.length);
 
        for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
            if (h == e.hash && eq(k, e.get())) {
                V oldValue = e.value;
                if (value != oldValue)
                    e.value = value;
                return oldValue;
            }
        }
 
        modCount++;
        Entry<K,V> e = tab[i];
        tab[i] = new Entry<>(k, value, queue, h, e);
        if (++size >= threshold)
            resize(tab.length * 2);
        return null;
    }
 
   private static Object maskNull(Object key) {
        return (key == null) ? NULL_KEY : key;
    }

Judge whether the key is null. If it is null, assign the key to NULL_KEY.
Calculate the hash value of the key, and then find the position to be inserted according to the hash value.
Traverse the Entry array to see if the key already exists. If so, replace the old value and return the old value.
If it does not exist, the Entry object is built and stored in the array.

This process is similar to HashMap, HashTable, etc.

2. get data

    public V get(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int index = indexFor(h, tab.length);
        Entry<K,V> e = tab[index];
        while (e != null) {
            if (e.hash == h && eq(k, e.get()))
                return e.value;
            e = e.next;
        }
        return null;
    }

In a word, first find the index position according to the hash value of the key, then get the Entry object, then judge whether there is a next reference to the Entry (i.e. hash collision), traverse the single linked list and compare the value value.

3. How to recycle weak bonds?

According to the above analysis, it is easy to conclude that the internal data storage of WeakHashMap uses the Entry [] array, that is, the key value pair array.

The Entry class inherits from WeakReference (weak reference),

The feature of weak reference is reiterated again: when the JVM performs garbage collection, the objects associated with weak references will be recycled regardless of whether the memory is sufficient or not.

When constructing an Entry object, a ReferenceQueue will be passed in. The key is the object wrapped by weak reference:

    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

Let's take a look at how WeakHashMap cleans up the recycled keys. The recycled keys will be stored in the reference queue:

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;
                }
            }
        }
    }

Traverse the reference queue, and then delete the recycled key value pairs (remove them from the array, change the single linked list node reference, and assign value to null). This method will be called where the WeakHashMap is added, deleted, modified, queried, and expanded.

After a key value is stored in the WeakHashMap, the key will be stored in a weak reference package. If the key has no strong reference outside the WeakHashMap, it will be recycled during GC, and then the WeakHashMap will clean up the recycled key according to the reference queue.

reference resources:

[1]https://baijiahao.baidu.com/s?id=1666368292461068600&wfr=spider&for=pc
[2]https://blog.csdn.net/u014294681/article/details/86522487

Tags: Java

Posted on Thu, 14 Oct 2021 02:58:24 -0400 by Nikos7