package org.ovirt.engine.core.utils; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; /** * Models a strongly-referenced primary hash map, coupled with a reapable secondary map based on soft-referenced values * (rather than keys, as is the case with the java.util.WeakHashMap). * * Use this like a normal hash map, but when values need no longer be retained they should be marked as reapable so that * they are made eligible for garbage collection. Entries are finally evicted if not already GC'd before the expiry of * the reapAfter timeout (calculated either from the point at which it was marked reapable or the last time of access). * Freeing of the entry prior to this timeout expiring is at the discretion of the garbage collector and depends on * whether the JVM is experiencing memory pressure, the type of JVM selected (the client JVM uses much more aggressive * GC policies that the server variant) and also the JVM options controlling the heap size. * * REVISIT: inherited entrySet() etc. don't take account of secondary * * @param <K> * key type * @param <V> * value type */ public class ReapedMap<K, V> extends HashMap<K, V> { static final long serialVersionUID = 12345678987654321L; private static Long DEFAULT_REAP_AFTER = 10 * 60 * 1000L; // 10 minutes private long reapAfter; private boolean accessBasedAging; private ReferenceQueue<V> queue; // Secondary Map, note: // - keys are strongly referenced, as GC of corresponding values // will trigger their release // - reap requires a predictable iteration order (based on insertion order) // hence the use of LinkedHasMap // LinkedHashMap<K, IdAwareReference<K, V>> reapableMap; public ReapedMap() { this(DEFAULT_REAP_AFTER); } /** * @param reapAfter * entries become eligible for reaping after this duration (ms) */ public ReapedMap(long reapAfter) { this(reapAfter, false); } /** * @param reapAfter * entries become eligible for reaping after this duration (ms) * @param accessBasedAging * reset reapAfter timeout on each access */ public ReapedMap(long reapAfter, boolean accessBasedAging) { this(reapAfter, accessBasedAging, new ReferenceQueue<>()); } /** * Package-protected constructor intended for test use. * * @param reapAfter * entries become eligible for reaping after this duration (ms) * @param accessBasedAging * reset reapAfter timeout on each access * @param queue * reference queue to avoid leaked mappings in case where aggressive GC eats referent before it is reaped */ ReapedMap(long reapAfter, boolean accessBasedAging, ReferenceQueue<V> queue) { this.reapAfter = reapAfter; this.accessBasedAging = accessBasedAging; this.queue = queue; reapableMap = new LinkedHashMap<>(); } @Override public synchronized V get(Object key) { V ret = super.get(key); if (ret == null) { IdAwareReference<K, V> ref = accessBasedAging ? reapableMap.remove(key) : reapableMap.get(key); if (ref != null) { if (ref.isEnqueued()) { ref.clear(); reapableMap.remove(key); } else { ret = ref.get(); if (ret == null) { reapableMap.remove(key); } else if (accessBasedAging) { // re-insert on timestamp reset so // as to maintain insertion order reapableMap.put(ref.key, ref.reset()); } } } } reap(); return ret; } @Override public synchronized V put(K k, V v) { reap(); return super.put(k, v); } @Override public synchronized V remove(Object key) { V ret = super.remove(key); if (ret == null) { IdAwareReference<K, V> ref = reapableMap.remove(key); if (ref != null) { if (ref.isEnqueued()) { ref.clear(); } else { ret = ref.get(); } } } reap(); return ret; } @Override public synchronized void clear() { super.clear(); reapableMap.clear(); while (queue.poll() != null) { // do nothing } } /** * Mark a key as being reapable, caching corresponding soft reference to corresponding value in the secondary map. */ public synchronized void reapable(K k) { V v = super.remove(k); if (v != null) { reapableMap.put(k, new IdAwareReference<>(k, v, queue)); } reap(); } /** * @return the size of the secondary map */ public synchronized long reapableSize() { return reapableMap.size(); } /** * Reap <i>before</i> additive operations, <i>after</i> for neutral and destructive ones. */ private synchronized void reap() { // reap entries older than age permitted // long now = System.currentTimeMillis(); Iterator<Map.Entry<K, IdAwareReference<K, V>>> entries = reapableMap.entrySet().iterator(); while (entries.hasNext()) { Map.Entry<K, IdAwareReference<K, V>> entry = entries.next(); IdAwareReference<K, V> v = entry.getValue(); if (now - v.timestamp > reapAfter) { entries.remove(); entry.getValue().clear(); entry.setValue(null); } else { // guaranteed iteration on insertion order => no older entries // break; } } // poll reference queue for GC-pending references to trigger // reaping of referent // Object ref = null; while ((ref = queue.poll()) != null) { @SuppressWarnings("unchecked") IdAwareReference<K, V> value = (IdAwareReference<K, V>) ref; reapableMap.remove(value.getKey()); } } /** * Encapsulate key and timestamp (the latter is used for eager reaping). The reference queue provides access to * finalizable instances of the reference type, not the class wrapping it. Hence we must extend SoftReference as * opposed to encapsulating it. */ static class IdAwareReference<T, S> extends SoftReference<S> { long timestamp; T key; IdAwareReference(T key, S value, ReferenceQueue<S> queue) { super(value, queue); this.key = key; timestamp = System.currentTimeMillis(); } public T getKey() { return key; } public boolean equals(Object other) { boolean ret = false; S one = null; ret = other == this || (other instanceof SoftReference<?> && (one = get()) != null && one.equals(((SoftReference<?>) other).get())); return ret; } public int hashCode() { S one = get(); return one != null ? one.hashCode() : 0; } private IdAwareReference<T, S> reset() { timestamp = System.currentTimeMillis(); return this; } } }