WeakHashMap, Source Code Interpretation

Keywords: Java JDK Tomcat Apache

Summary

WeakHashMap is also an implementation class of the Map interface. It is similar to HashMap and is also a hash table that stores key-value pairs and is non-thread-safe. However, WeakHashMap does not introduce a red-black tree to avoid the impact of hash conflicts. The internal implementation is just an array + a single linked list. In addition, the biggest difference between WeakHashMap and HashMap is that the key of WeakHashMap is "weak keys". That is, when a key is no longer in normal use, the key-value pair corresponding to the key will automatically be deleted from WeakHashMap. In this case, even if the key-value pair exists, the key will still be deleted by GC. Recycle, so that its corresponding key-value pair is effectively removed from the map.

Four References to Java

Before entering the WeakHashMap source code formally, we need to have a basic understanding of "weak reference". For this reason, here are four references that JDK 1.2 started to introduce:

  • Strong Reference refers to references such as Objective obj = new Object(), which are ubiquitous in program code. As long as strong references exist, garbage collectors will never reclaim referenced objects.
  • Soft Reference is used to describe some useful but not necessary objects. For objects associated with soft reference, these objects will be included in the recovery scope for the second recovery before the system will have memory overflow exception. If there is not enough memory for this reclamation, a memory overflow exception will be thrown. After JDK 1.2, the SoftReference class is provided to implement soft reference.
  • Weak Reference is also used to describe non-essential objects, but its strength is weaker than that of soft references, and objects associated with weak references can only survive until the next garbage collection occurs. When the garbage collector is working, regardless of whether the current memory is sufficient or not, objects associated with only weak references are reclaimed. After JDK 1.2, WeakReference classes are provided to implement weak references.
  • Phantom Reference, also known as ghost or phantom reference, is the weakest reference relationship. Whether an object has a virtual reference will not affect its lifetime at all, nor can it obtain an object instance through a virtual reference. The only purpose of setting virtual reference associations for an object is to receive a system notification when the object is reclaimed by the collector. After JDK 1.2, the PhantomReference class is provided to implement virtual references.

We say WeakHashMap's key is weak-keys, that is, the key values of the Map implementation class are weak references.

Low-level implementation

Let's first look at the definition of WeakHashMap:

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

 

Like HashMap, it inherits from the AbstractMap class and specifically labels the Map interface. In addition, WeakHashMap does not implement Cloneable interface and Serilizable interface.

Let's look again at the important static inner class Entry of WeakHashMap:

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

   /** * Creates new entry. */
    Entry(Object key, V value,
          ReferenceQueue<Object> queue,
          int hash, Entry<K,V> next) {
        //hold key Pass on to the parent class WeakReference Constructor, description Entry Of key Is a weak reference
        //meanwhile key Values enter the reference queue queue Waiting to be processed
        super(key, queue);
        //Display Definition value,Explanation Entry Of value Is a strong quotation
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }

    @SuppressWarnings("unchecked")
    public K getKey() {//In acquisition key Timely needs unmaskNull,Because for null Of key,Is used WeakHashMap Represented by internal member attributes
        return (K) WeakHashMap.unmaskNull(get());
    }

    public V getValue() {
        return value;
    }

    public V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;
        K k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            V v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }

    public int hashCode() {
        K k = getKey();
        V v = getValue();
        return Objects.hashCode(k) ^ Objects.hashCode(v);
    }

    public String toString() {
        return getKey() + "=" + getValue();
    }
}

 

The unmaskNull() method is used here to represent null worth key with an empty Object object. Its source code is as follows:

private static final Object NULL_KEY = new Object();
/** * When key is null, use NULL_KEY to represent key */
private static Object maskNull(Object key) {
    return (key == null) ? NULL_KEY : key;
}

 

You can see that Entry < K, V > inherits from WeakReference class. For Entry class, fields that need to be defined as weak references can be passed directly into the constructor of the parent class, such as the key seen in the code, which also implements the "weak key" we said earlier. Value values give strong references, but that doesn't matter. We'll introduce a WeakHashMap.expungeStaleEntries method later, which assigns the key-value corresponding to the weak key as null to help GC recycle it.

Let's look again at the important fields of WeakHashMap:

 /** * An array that stores key-value pairs, usually a power of 2 */
    Entry<K,V>[] table;
   /** * The actual number of key-value pairs */
    private int size;
   /** * The critical value of expansion can be calculated by capacity * load factor. HashMap will be expanded beyond this value* @serial */
    private int threshold;
   /** * Load factor */ 
    private final float loadFactor;
   /** * Reference queues to save weak keys that will be reclaimed by GC */
    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
   /** * Record the number of times HashMap has been modified. * Modifications include changing the number of key-value pairs or modifying internal structures, such as rehash * in the fail-fast mechanism used as an iterator for HashMap (see Concurrent ModificationException) */ 
    int modCount;

 

Compared with HashMap, WeakHashMap has fewer entrySet fields and more reference queue s.

WeakHashMap defines static global variables as follows:

 private static final int DEFAULT_INITIAL_CAPACITY = 16;

    /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    /** * The load factor used when none specified in constructor. */
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;

 

Compared with HashMap, three static global variables, TREEIFY_THRESHOLD, UNTREEIFY_THRESHOLD, MIN_TREEIFY_CAPACITY, are missing. They are used to transform single-linked list and red-black tree. Red-black tree is not implemented in WeakHashMap, so these three variables are not needed.

Next, let's look at expungeStaleEntries, an important method related to weak key implementations:

/** * Delete recycled references from hash tables */
private void expungeStaleEntries() {
    for (Object x; (x = queue.poll()) != null; ) {
        synchronized (queue) {
            @SuppressWarnings("unchecked")
                Entry<K,V> e = (Entry<K,V>) x;//e For cleaning up Entry
            int i = indexFor(e.hash, table.length);
            Entry<K,V> prev = table[i];
            Entry<K,V> p = prev;
            //Ergodic collision list
            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; // hold value Assignment null,Help GC Recovering strongly cited value
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

 

From the source code of WeakHashMap.Entry, we can see that the value of weak keys will be saved in the reference queue. This method means that the Entry corresponding to the weak keys saved in the reference queue will be deleted from the single linked list, that is, the key value pairs recovered by GC in the hash table will be deleted. This method is called in addition, deletion, alteration and search methods defined by WeakHashMap.

Application scenarios

cache

Caching is a common source of memory leaks. Once you put an object reference into the cache, it is easy to forget, so that it will remain in the cache for a long time after it is no longer useful.

There are several possible solutions to this problem. If you happen to implement a cache that makes sense as long as there is a key reference to an item outside the cache, you can use WeakHashMap to represent the cache. When items in the cache expire, they are automatically deleted (note that WeakHashMap is useful only if the lifetime of the cached item is determined by the external reference of the key rather than by the value).

Tomcat implements its concurrent cache Concurrent Cache with WeakHashMap. The source code is as follows:

package org.apache.tomcat.util.collections;

import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

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

 

Monitors and other callbacks

Another common source of memory leaks is listeners and other callbacks. If you are implementing client registered callbacks without explicitly canceling registered API s, they will accumulate unless you take some action. The best way to ensure that callbacks are immediately considered garbage collection is to save only their weak reference s, which can be saved as keys in WeakHashMap.

 

Recommended Reading

JVM's latest interview questions in 2019 must be collected

The most comprehensive Ali multi-threaded interview questions, how many can you answer?

Java Interview Question: Collections in Java and Their Inheritance Relations

It took nearly a decade to sort out the most comprehensive Java interview questions in history.

Posted by MytHunter on Sat, 07 Sep 2019 00:49:46 -0700