All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jruby.javasupport.util.ObjectProxyCache Maven / Gradle / Ivy

There is a newer version: 9.4.9.0
Show newest version
package org.jruby.javasupport.util;

import org.jruby.util.log.Logger;
import org.jruby.util.log.LoggerFactory;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;

import java.util.concurrent.locks.ReentrantLock;


/**
 * Maps Java objects to their proxies.  Combines elements of WeakHashMap and
 * ConcurrentHashMap to permit unsynchronized reads.  May be configured to
 * use either Weak (the default) or Soft references.

* * Note that both Java objects and their proxies are held by weak/soft * references; because proxies (currently) keep strong references to their * Java objects, if we kept strong references to them the Java objects would * never be gc'ed. This presents a problem in the case where a user passes * a Rubified Java object out to Java but keeps no reference in Ruby to the * proxy; if the object is returned to Ruby after its proxy has been gc'ed, * a new (and possibly very wrong, in the case of JRuby-defined subclasses) * proxy will be created. Use of soft references may help reduce the * likelihood of this occurring; users may be advised to keep Ruby-side * references to prevent it occurring altogether. * * @author Bill Dortch * */ public abstract class ObjectProxyCache { private static final Logger LOG = LoggerFactory.getLogger("ObjectProxyCache"); public static enum ReferenceType { WEAK, SOFT } private static final int DEFAULT_SEGMENTS = 16; // must be power of 2 private static final int DEFAULT_SEGMENT_SIZE = 8; // must be power of 2 private static final float DEFAULT_LOAD_FACTOR = 0.75f; private static final int MAX_CAPACITY = 1 << 30; private static final int MAX_SEGMENTS = 1 << 16; private static final int VULTURE_RUN_FREQ_SECONDS = 5; private static int _nextId = 0; private static synchronized int nextId() { return ++_nextId; } private final ReferenceType referenceType; private final Segment[] segments; private final int segmentShift; private final int segmentMask; private Thread vulture; private final int id; public ObjectProxyCache() { this(DEFAULT_SEGMENTS, DEFAULT_SEGMENT_SIZE, ReferenceType.WEAK); } public ObjectProxyCache(ReferenceType refType) { this(DEFAULT_SEGMENTS, DEFAULT_SEGMENT_SIZE, refType); } public ObjectProxyCache(int numSegments, int initialSegCapacity, ReferenceType refType) { if (numSegments <= 0 || initialSegCapacity <= 0 || refType == null) { throw new IllegalArgumentException(); } this.id = nextId(); this.referenceType = refType; if (numSegments > MAX_SEGMENTS) numSegments = MAX_SEGMENTS; // Find power-of-two sizes best matching arguments int sshift = 0; int ssize = 1; while (ssize < numSegments) { ++sshift; ssize <<= 1; } // note segmentShift differs from ConcurrentHashMap's calculation due to // issues with System.identityHashCode (upper n bits always 0, at least // under Java 1.6 / WinXP) this.segmentShift = 24 - sshift; this.segmentMask = ssize - 1; this.segments = Segment.newArray(ssize); if (initialSegCapacity > MAX_CAPACITY) { initialSegCapacity = MAX_CAPACITY; } int cap = 1; while (cap < initialSegCapacity) cap <<= 1; for (int i = ssize; --i >= 0; ) { segments[i] = new Segment(cap, this); } // vulture thread will periodically expunge dead // entries. entries are also expunged during 'put' // operations; this is designed to cover the case where // many objects are created initially, followed by limited // put activity. // // FIXME: DISABLED (below) pending resolution of finalization issue // try { this.vulture = new Thread("ObjectProxyCache "+id+" vulture") { public void run() { for ( ;; ) { try { sleep(VULTURE_RUN_FREQ_SECONDS * 1000); } catch (InterruptedException e) {} boolean dump = size() > 200; if (dump) { LOG.debug("***Vulture {} waking, stats:", id); LOG.debug(stats()); } for (int i = segments.length; --i >= 0; ) { Segment seg = segments[i]; seg.lock(); try { seg.expunge(); } finally { seg.unlock(); } yield(); } if (dump) { LOG.debug("***Vulture {} sleeping, stats:", id); LOG.debug(stats()); } } } }; vulture.setDaemon(true); } catch (SecurityException e) { this.vulture = null; } // FIXME: vulture daemon thread prevents finalization, // find alternative approach. // vulture.start(); // System.err.println("***ObjectProxyCache " + id + " started at "+ new java.util.Date()); } // protected void finalize() throws Throwable { // System.err.println("***ObjectProxyCache " + id + " finalized at "+ new java.util.Date()); // } public abstract T allocateProxy(Object javaObject, A allocator); public T get(Object javaObject) { if (javaObject == null) return null; int hash = hash(javaObject); return segmentFor(hash).get(javaObject, hash); } public T getOrCreate(Object javaObject, A allocator) { if (javaObject == null || allocator == null) return null; int hash = hash(javaObject); return segmentFor(hash).getOrCreate(javaObject, hash, allocator); } public void put(Object javaObject, T proxy) { if (javaObject == null || proxy == null) return; int hash = hash(javaObject); segmentFor(hash).put(javaObject, hash, proxy); } private static int hash(Object javaObject) { int h = System.identityHashCode(javaObject); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } private Segment segmentFor(int hash) { return segments[(hash >>> segmentShift) & segmentMask]; } /** * Returns the approximate size (elements in use) of the cache. The * sizes of the segments are summed. No effort is made to synchronize * across segments, so the value returned may differ from the actual * size at any point in time. * * @return */ public int size() { int size = 0; for (Segment seg : segments) { size += seg.tableSize; } return size; } public String stats() { StringBuilder b = new StringBuilder(); int n = 0; int size = 0; int alloc = 0; b.append("Segments: ").append(segments.length).append("\n"); for (Segment seg : segments) { int ssize = 0; int salloc = 0; seg.lock(); try { ssize = seg.count(); salloc = seg.entryTable.length; } finally { seg.unlock(); } size += ssize; alloc += salloc; b.append("seg[").append(n++).append("]: size: ").append(ssize) .append(" alloc: ").append(salloc).append("\n"); } b.append("Total: size: ").append(size) .append(" alloc: ").append(alloc).append("\n"); return b.toString(); } // EntryRefs include hash with key to facilitate lookup by Segment#expunge // after ref is removed from ReferenceQueue private static interface EntryRef { T get(); int hash(); } private static final class WeakEntryRef extends WeakReference implements EntryRef { final int hash; WeakEntryRef(int hash, T rawObject, ReferenceQueue queue) { super(rawObject, queue); this.hash = hash; } public int hash() { return hash; } } private static final class SoftEntryRef extends SoftReference implements EntryRef { final int hash; SoftEntryRef(int hash, T rawObject, ReferenceQueue queue) { super(rawObject, queue); this.hash = hash; } public int hash() { return hash; } } // Unlike WeakHashMap, our Entry does not subclass WeakReference, but rather // makes it a final field. The theory is that doing so should force a happens-before // relationship WRT the WeakReference constructor, guaranteeing that the key will be // visibile to other threads (unless it's been GC'ed). See JLS 17.5 (final fields) and // 17.4.5 (Happens-before order) to confirm or refute my reasoning here. static class Entry { final EntryRef objectRef; final int hash; final EntryRef proxyRef; final Entry next; Entry(Object object, int hash, T proxy, ReferenceType type, Entry next, ReferenceQueue queue) { this.hash = hash; this.next = next; // references to the Java object and its proxy will either both be // weak or both be soft, since the proxy contains a strong reference // to the object, so it wouldn't make sense for the reference types // to differ. if (type == ReferenceType.WEAK) { this.objectRef = new WeakEntryRef(hash, object, queue); this.proxyRef = new WeakEntryRef(hash, proxy, queue); } else { this.objectRef = new SoftEntryRef(hash, object, queue); this.proxyRef = new SoftEntryRef(hash, proxy, queue); } } // ctor used by remove/rehash Entry(EntryRef objectRef, int hash, EntryRef proxyRef, Entry next) { this.objectRef = objectRef; this.hash = hash; this.proxyRef = proxyRef; this.next = next; } @SuppressWarnings("unchecked") static final Entry[] newArray(int size) { return new Entry[size]; } } // lame generics issues: making Segment class static and manually // inserting cache reference to work around various problems generically // referencing methods/vars across classes. static class Segment extends ReentrantLock { final ObjectProxyCache cache; final ReferenceQueue referenceQueue = new ReferenceQueue(); volatile Entry[] entryTable; int tableSize; int threshold; Segment(int capacity, ObjectProxyCache cache) { threshold = (int)(capacity * DEFAULT_LOAD_FACTOR); entryTable = Entry.newArray(capacity); this.cache = cache; } // must be called under lock private void expunge() { Entry[] table = entryTable; ReferenceQueue queue = referenceQueue; EntryRef ref; // note that we'll potentially see the refs for both the java object and // proxy -- whichever we see first will cause the entry to be removed; // the other will not match an entry and will be ignored. while ((ref = (EntryRef)queue.poll()) != null) { int hash; for (Entry e = table[(hash = ref.hash()) & (table.length - 1)]; e != null; e = e.next) { if (hash == e.hash && (ref == e.objectRef || ref == e.proxyRef)) { remove(table, hash, e); break; } } } } // must be called under lock private void remove(Entry[] table, int hash, Entry e) { int index = hash & (table.length - 1); Entry first = table[index]; for (Entry n = first; n != null; n = n.next) { if (n == e) { Entry newFirst = n.next; for (Entry p = first; p != n; p = p.next) { newFirst = new Entry(p.objectRef, p.hash, p.proxyRef, newFirst); } table[index] = newFirst; tableSize--; entryTable = table; // write-volatile return; } } } // temp method to verify tableSize value // must be called under lock private int count() { int count = 0; for (Entry e : entryTable) { while (e != null) { count++; e = e.next; } } return count; } // must be called under lock private Entry[] rehash() { assert tableSize == count() : "tableSize "+tableSize+" != count() "+count(); Entry[] oldTable = entryTable; // read-volatile int oldCapacity; if ((oldCapacity = oldTable.length) >= MAX_CAPACITY) { return oldTable; } int newCapacity = oldCapacity << 1; int sizeMask = newCapacity - 1; threshold = (int)(newCapacity * DEFAULT_LOAD_FACTOR); Entry[] newTable = Entry.newArray(newCapacity); Entry e; for (int i = oldCapacity; --i >= 0; ) { if ((e = oldTable[i]) != null) { int idx = e.hash & sizeMask; Entry next; if ((next = e.next) == null) { // Single node in list newTable[idx] = e; } else { // Reuse trailing consecutive sequence at same slot int lastIdx = idx; Entry lastRun = e; for (Entry last = next; last != null; last = last.next) { int k; if ((k = last.hash & sizeMask) != lastIdx) { lastIdx = k; lastRun = last; } } newTable[lastIdx] = lastRun; // Clone all remaining nodes for (Entry p = e; p != lastRun; p = p.next) { int k = p.hash & sizeMask; Entry m = new Entry(p.objectRef, p.hash, p.proxyRef, newTable[k]); newTable[k] = m; } } } } entryTable = newTable; // write-volatile return newTable; } void put(Object object, int hash, T proxy) { lock(); try { expunge(); Entry[] table; int potentialNewSize; if ((potentialNewSize = tableSize + 1) > threshold) { table = rehash(); // indirect read-/write- volatile } else { table = entryTable; // read-volatile } int index; Entry e; for (e = table[index = hash & (table.length - 1)]; e != null; e = e.next) { if (hash == e.hash && object == e.objectRef.get()) { if (proxy == e.proxyRef.get()) return; // entry exists, proxy doesn't match. replace. // this could happen if old proxy was gc'ed // TODO: raise exception if stored proxy is non-null? (not gc'ed) remove(table, hash, e); potentialNewSize--; break; } } e = new Entry(object, hash, proxy, cache.referenceType, table[index], referenceQueue); table[index] = e; tableSize = potentialNewSize; entryTable = table; // write-volatile } finally { unlock(); } } T getOrCreate(Object object, int hash, A allocator) { Entry[] table; T proxy; for (Entry e = (table = entryTable)[hash & table.length - 1]; e != null; e = e.next) { if (hash == e.hash && object == e.objectRef.get()) { if ((proxy = e.proxyRef.get()) != null) return proxy; break; } } lock(); try { expunge(); int potentialNewSize; if ((potentialNewSize = tableSize + 1) > threshold) { table = rehash(); // indirect read-/write- volatile } else { table = entryTable; // read-volatile } int index; Entry e; for (e = table[index = hash & (table.length - 1)]; e != null; e = e.next) { if (hash == e.hash && object == e.objectRef.get()) { if ((proxy = e.proxyRef.get()) != null) return proxy; // entry exists, proxy has been gc'ed. replace entry. remove(table, hash, e); potentialNewSize--; break; } } proxy = cache.allocateProxy(object, allocator); e = new Entry(object, hash, proxy, cache.referenceType, table[index], referenceQueue); table[index] = e; tableSize = potentialNewSize; entryTable = table; // write-volatile return proxy; } finally { unlock(); } } T get(Object object, int hash) { Entry[] table; for (Entry e = (table = entryTable)[hash & table.length - 1]; e != null; e = e.next) { if (hash == e.hash && object == e.objectRef.get()) { return e.proxyRef.get(); } } return null; } @SuppressWarnings("unchecked") static final Segment[] newArray(int size) { return new Segment[size]; } } }