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<>();
- 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.
- Loading factor
Loading factor is used to calculate the capacity of expansion, the default loading factor is 0.75.
- 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; }
- 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. - Which bucket is calculated?
- Traverse the linked list corresponding to the bucket;
- If an element is found, the old value is replaced with a new value and the old value is returned.
- If not, insert a new element at the head of the list.
HashMap is inserted at the end of the list.
- 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; } } }
- Determine whether the old capacity reaches the maximum capacity.
- New barrels are built and all elements are transferred to new barrels.
- 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.
- Otherwise, the new barrel is used and the new expansion threshold is calculated.
- 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; }
- hash value was calculated.
- Traverse the linked list corresponding to the bucket.
- If found, the value of the element is returned.
- 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; }
- hash is calculated.
- Find the bucket where you are.
- Traverse the linked list corresponding to the bucket;
- If found, delete the node and return the value of the node.
- 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; } } } }
- When the key fails, gc automatically adds the corresponding Entry to the reference queue.
- All operations on map are directly or indirectly invoked to this method to remove invalid Entry first, such as getTable(), size(), resize().
- 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.
- 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
- WeakHashMap uses (array + linked list) storage structure;
- The key in WeakHashMap is a weak reference, which is cleared when gc is used.
- The Entry corresponding to the invalid key is eliminated in each operation of map.
- 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.
- WeakHashMap is often used as a cache.
Summary of Strong, Soft, Weak and False Citation
- 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.
- 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.
- Weak citation
If an object has only weak references, it will be reclaimed when the gc scans it, regardless of the insufficient memory space.
- 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.