[Learning Notes-Java Collection-6] WeakHashMap Source Code Analysis

Keywords: Java Attribute jvm less

introduce

WeakHashMap is a kind of weak reference map. The internal keys are stored as weak references. When jvm gc does not exist strong references, these keys will be recycled by gc. The next time we operate a map, the corresponding Entry will be deleted. Based on this feature, WeakHashMap is especially suitable for cache processing.

Inheritance System


WeakHashMap does not implement Clone and Serilizable interfaces, so it does not have cloning and serialization features.

storage structure

WeakHashMap is doomed to have fewer elements because it recycles key s that are not strongly referenced when it is gc, so it doesn't need to be converted into a red-black tree when there are more elements like HashMap.

Therefore, the storage structure of WeakHashMap is only (array + linked list).

Source code parsing

attribute


/**
 * The default initial capacity is 16
 */
private static final int DEFAULT_INITIAL_CAPACITY = 16;

/**
 * The 30 th power of maximum capacity 2
 */
private static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * Default Load Factor
 */
private static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * bucket
 */
Entry<K,V>[] table;

/**
 * Number of elements
 */
private int size;

/**
 * Expansion threshold, equal to capacity * loadFactor
 */
private int threshold;

/**
 * Loading factor
 */
private final float loadFactor;

/**
 * Reference queue, which adds Entry to the queue when the weak key fails
 */
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
  1. capacity

    Capacity is the length of the array, that is, the number of barrels, default to 16, the maximum is 20 times, when the capacity reaches 64 can be tree.

  2. Loading factor

    Loading factor is used to calculate the capacity of expansion, the default loading factor is 0.75.

  3. Reference queue

    Entry is added to the queue when the weak key fails, and the invalid Entry is removed when the map is visited next time.

Entry inner class

The storage node inside WeakHashMap has no key attribute.


private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    // You can find that there is no key because the key is stored in the Referen class as a weak reference.
    V value;
    final int hash;
    Entry<K,V> next;

    Entry(Object key, V value,
          ReferenceQueue<Object> queue,
          int hash, Entry<K,V> next) {
        // Initialize key and reference queue by calling WeakReference's constructor
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }
}

public class WeakReference<T> extends Reference<T> {
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        // Initialize key and reference queue by calling the constructor of Reference
        super(referent, q);
    }
}

public abstract class Reference<T> {
    // Where key s are actually stored
    private T referent;         /* Treated specially by GC */
    // Reference queue
    volatile ReferenceQueue<? super T> queue;

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
}

From Entry's method of construction, we know that key and queue will eventually be passed to Reference's method of construction, where key is the referent attribute of Reference, and it will be treated specially by gc, that is, when no strong reference exists, it will be cleared when the next gc.

Construction method

public WeakHashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Initial Capacity: "+
                initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;

    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal Load factor: "+
                loadFactor);
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;
    table = newTable(capacity);
    this.loadFactor = loadFactor;
    threshold = (int)(capacity * loadFactor);
}

public WeakHashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public WeakHashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

public WeakHashMap(Map<? extends K, ? extends V> m) {
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
            DEFAULT_INITIAL_CAPACITY),
            DEFAULT_LOAD_FACTOR);
    putAll(m);
}

The construction method is similar to HashMap in that the initial capacity is greater than or equal to the nearest 2 n-th power of the incoming capacity, and the threshold of expansion is equal to capacity * loadFactor.

put(K key, V value) method

Method of Adding Elements


public V put(K key, V value) {
    // If the key is empty, replace it with an empty object
    Object k = maskNull(key);
    // Calculate the hash value of key
    int h = hash(k);
    // Acquisition barrel
    Entry<K,V>[] tab = getTable();
    // Which bucket is the calculation element in, H & (length-1)
    int i = indexFor(h, tab.length);

    // Traversing the linked list corresponding to the bucket
    for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
        if (h == e.hash && eq(k, e.get())) {
            // If an element is found, replace the old value with the new value and return the old value.
            V oldValue = e.value;
            if (value != oldValue)
                e.value = value;
            return oldValue;
        }
    }

    modCount++;
    // If not, insert the new value into the head of the list
    Entry<K,V> e = tab[i];
    tab[i] = new Entry<>(k, value, queue, h, e);
    // If the number of barrels reached the threshold of expansion after inserting elements, the number of barrels would be expanded to twice the size.
    if (++size >= threshold)
        resize(tab.length * 2);
    return null;
}
  1. hash is calculated.
    This is different from HashMap. If the key in HashMap is empty and returns directly to 0, it is calculated with empty objects.
    HashMap only used once XOR, here used four times, HashMap gives an explanation that is enough once, and even if the conflict is converted into red and black trees, it has no effect on efficiency.
  2. Which bucket is calculated?
  3. Traverse the linked list corresponding to the bucket;
  4. If an element is found, the old value is replaced with a new value and the old value is returned.
  5. If not, insert a new element at the head of the list.

    HashMap is inserted at the end of the list.

  6. If the number of elements reaches the expansion threshold, the capacity will be increased to 2 times.

    HashMap is larger than threshold before expansion, which is equivalent to threshold expansion.

Resize (int new capacity) method

Expansion method.


void resize(int newCapacity) {
    // Getting the old bucket, getTable() eliminates invalid Entry
    Entry<K,V>[] oldTable = getTable();
    // Old capacity
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    // New barrel
    Entry<K,V>[] newTable = newTable(newCapacity);
    // Transfer elements from old barrels to new ones
    transfer(oldTable, newTable);
    // Appoint the new bucket to the bucket variable
    table = newTable;

    /*
     * If ignoring null elements and processing ref queue caused massive
     * shrinkage, then restore old table.  This should be rare, but avoids
     * unbounded expansion of garbage-filled tables.
     */
    // If the number of elements is more than half of the expansion threshold, the new barrel and capacity are used and the new expansion threshold is calculated.
    if (size >= threshold / 2) {
        threshold = (int)(newCapacity * loadFactor);
    } else {
        // Otherwise, the element is transferred back to the old bucket, or the old bucket is used.
        // Because the invalid Entry is cleared at transfer, the number of elements may not be as large as that, and there is no need to expand it.
        expungeStaleEntries();
        transfer(newTable, oldTable);
        table = oldTable;
    }
}

private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) {
    // Traveling through old barrels
    for (int j = 0; j < src.length; ++j) {
        Entry<K,V> e = src[j];
        src[j] = null;
        while (e != null) {
            Entry<K,V> next = e.next;
            Object key = e.get();
            // If the key equals null, the key is cleared, indicating that the key is cleared by gc, then the entire Entry is cleared.
            if (key == null) {
                e.next = null;  // Help GC
                e.value = null; //  "   "
                size--;
            } else {
                // Otherwise, calculate the position in the new bucket and place the element at the head of the corresponding list in the new bucket.
                int i = indexFor(e.hash, dest.length);
                e.next = dest[i];
                dest[i] = e;
            }
            e = next;
        }
    }
}
  1. Determine whether the old capacity reaches the maximum capacity.
  2. New barrels are built and all elements are transferred to new barrels.
  3. If the number of elements after transfer is less than half of the expansion threshold, then the elements are transferred back to the old barrel and continue to use the old barrel, indicating that no expansion is needed.
  4. Otherwise, the new barrel is used and the new expansion threshold is calculated.
  5. In the process of transferring elements, the element whose key is null will be removed, so the size will be smaller.

get(Object key) method

Get the element.


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

  1. hash value was calculated.
  2. Traverse the linked list corresponding to the bucket.
  3. If found, the value of the element is returned.
  4. If not found, return to empty.

remove(Object key) method

Removing Elements


public V remove(Object key) {
    Object k = maskNull(key);
    // Computing hash
    int h = hash(k);
    Entry<K,V>[] tab = getTable();
    int i = indexFor(h, tab.length);
    // The first element of the bucket in which the element is located
    Entry<K,V> prev = tab[i];
    Entry<K,V> e = prev;

    // Traversal list
    while (e != null) {
        Entry<K,V> next = e.next;
        if (h == e.hash && eq(k, e.get())) {
            // If found, delete the element
            modCount++;
            size--;

            if (prev == e)
                // If it is a header node, point the header node to the next node
                tab[i] = next;
            else
                // If it is not the header node, delete the node
                prev.next = next;
            return e.value;
        }
        prev = e;
        e = next;
    }

    return null;
}
  1. hash is calculated.
  2. Find the bucket where you are.
  3. Traverse the linked list corresponding to the bucket;
  4. If found, delete the node and return the value of the node.
  5. If not, return null.

expungeStaleEntries() method

Eliminate invalid Entry.


private void expungeStaleEntries() {
    // Traversing the reference queue
    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);
            // Find the bucket.
            Entry<K,V> prev = table[i];
            Entry<K,V> p = prev;
            // Traversal list
            while (p != null) {
                Entry<K,V> next = p.next;
                // Find the element
                if (p == e) {
                    // Delete this element
                    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;
            }
        }
    }
}
  1. When the key fails, gc automatically adds the corresponding Entry to the reference queue.
  2. All operations on map are directly or indirectly invoked to this method to remove invalid Entry first, such as getTable(), size(), resize().
  3. The purpose of this method is to traverse the reference queue and remove the saved Entry from the map. See the class annotations for the specific process.
  4. From here you can see that while removing Entry, the value is also placed side by side as null to help gc clean up elements and defensive programming.

Use examples

package com.coolcoding.code;

import java.util.Map;
import java.util.WeakHashMap;

public class WeakHashMapTest {

public static void main(String[] args) {
    Map<String, Integer> map = new WeakHashMap<>(3);

    // Put in three strings declared by new String()
    map.put(new String("1"), 1);
    map.put(new String("2"), 2);
    map.put(new String("3"), 3);

    // Put in strings that are not declared by new String()
    map.put("6", 6);

    // Use key to strongly refer to the string "3"
    String key = null;
    for (String s : map.keySet()) {
        // This "3" and new String("3") are not a reference
        if (s.equals("3")) {
            key = s;
        }
    }

    // Output {6 = 6, 1 = 1, 2 = 2, 3 = 3}, all key s can be printed without gc
    System.out.println(map);

    // gc
    System.gc();

    // Place a new String() declaration string
    map.put(new String("4"), 4);

    // Output {4 = 4, 6 = 6, 3 = 3}, the values put in after gc and the strongly referenced key s can be printed out
    System.out.println(map);

    // Reference Fracture of key and "3"
    key = null;

    // gc
    System.gc();

    // Output {6 = 6} and the strongly referenced key after gc can be printed out
    System.out.println(map);
}
}

Variables declared by new String() are weak references here, and using "6" will always exist in the constant pool and will not be cleaned up, so "6" will always be in the map, and other elements will be cleaned up with gc.

summary

  1. WeakHashMap uses (array + linked list) storage structure;
  2. The key in WeakHashMap is a weak reference, which is cleared when gc is used.
  3. The Entry corresponding to the invalid key is eliminated in each operation of map.
  4. When using String as key, you must declare key in the way of new String(), which will invalidate. Other basic types of packaging are the same.
  5. WeakHashMap is often used as a cache.

Summary of Strong, Soft, Weak and False Citation

  1. Strong citation

    Use the most common reference. If an object has a strong reference, it will never be recycled by gc. If memory space is insufficient, GC would rather throw OutOfMemoryError than recycle objects with strong references.

  2. Soft citation

    If an object has only soft references, it will not be reclaimed when there is enough memory space, but it will be reclaimed when there is insufficient memory space. As long as this object with soft references is not recycled, the program can be used normally.

  3. Weak citation

    If an object has only weak references, it will be reclaimed when the gc scans it, regardless of the insufficient memory space.

  4. Virtual reference

    If an object has only virtual references, it can be reclaimed by gc at any time, just as it does not have any references.

Soft (weak, weak) references must be used with a Reference Queue. When gc reclaims the soft (weak, weak) referenced object, it places the soft (weak, weak) references in the reference queue.

For example, the Entry mentioned above is a weak reference, which refers to key, and when the key is recycled, Entry is put into queue.

Posted by Bit343 on Wed, 14 Aug 2019 00:44:05 -0700