You are viewing a plain text version of this content. The canonical link for it is here.
Posted to scm@geronimo.apache.org by gd...@apache.org on 2005/10/24 23:29:13 UTC

svn commit: r328153 - in /geronimo/trunk/modules/kernel/src: java/org/apache/geronimo/kernel/basic/ test/org/apache/geronimo/kernel/basic/

Author: gdamour
Date: Mon Oct 24 14:29:04 2005
New Revision: 328153

URL: http://svn.apache.org/viewcvs?rev=328153&view=rev
Log:
GERONIMO-983 Memory used by proxies isn't released by dereferencing the 
proxy (I speculate)

Patch submitted by Kevan Miller. Geir has confirmed that the borrowed code
from commons-collection is properly identified as such.

Added:
    geronimo/trunk/modules/kernel/src/java/org/apache/geronimo/kernel/basic/BasicProxyMap.java
    geronimo/trunk/modules/kernel/src/test/org/apache/geronimo/kernel/basic/
    geronimo/trunk/modules/kernel/src/test/org/apache/geronimo/kernel/basic/BasicProxyMapTest.java
Modified:
    geronimo/trunk/modules/kernel/src/java/org/apache/geronimo/kernel/basic/BasicProxyManager.java

Modified: geronimo/trunk/modules/kernel/src/java/org/apache/geronimo/kernel/basic/BasicProxyManager.java
URL: http://svn.apache.org/viewcvs/geronimo/trunk/modules/kernel/src/java/org/apache/geronimo/kernel/basic/BasicProxyManager.java?rev=328153&r1=328152&r2=328153&view=diff
==============================================================================
--- geronimo/trunk/modules/kernel/src/java/org/apache/geronimo/kernel/basic/BasicProxyManager.java (original)
+++ geronimo/trunk/modules/kernel/src/java/org/apache/geronimo/kernel/basic/BasicProxyManager.java Mon Oct 24 14:29:04 2005
@@ -45,8 +45,7 @@
     private final static Log log = LogFactory.getLog(BasicProxyManager.class);
     private final Kernel kernel;
 
-    // todo use weak keys for this
-    private final IdentityHashMap interceptors = new IdentityHashMap();
+    private final BasicProxyMap interceptors = new BasicProxyMap();
 
     public BasicProxyManager(Kernel kernel) {
         this.kernel = kernel;

Added: geronimo/trunk/modules/kernel/src/java/org/apache/geronimo/kernel/basic/BasicProxyMap.java
URL: http://svn.apache.org/viewcvs/geronimo/trunk/modules/kernel/src/java/org/apache/geronimo/kernel/basic/BasicProxyMap.java?rev=328153&view=auto
==============================================================================
--- geronimo/trunk/modules/kernel/src/java/org/apache/geronimo/kernel/basic/BasicProxyMap.java (added)
+++ geronimo/trunk/modules/kernel/src/java/org/apache/geronimo/kernel/basic/BasicProxyMap.java Mon Oct 24 14:29:04 2005
@@ -0,0 +1,485 @@
+/**
+ *
+ * Copyright 2002-2005 The Apache Software Foundation
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.geronimo.kernel.basic;
+
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Streamlined version of a WeakIdentityHashMap. Provides Identity semantics with 
+ * Weak References to keys. This allows proxies to be GC'ed when no longer referenced
+ * by clients. <code>BasicProxymanager.destroyProxy()</code> need not be invoked when a
+ * proxy is no longer needed. Note that this is not a full Map implementation. 
+ * The iteration and collection capabilities of Map have been discarded to keep the 
+ * implementation lightweight.
+ * <p>
+ * Much of this code was cribbed from the Commons Collection 3.1 implementation of 
+ * <code>ReferenceIdentityMap</code> and <code>AbstractReferenceMap</code>.
+ */
+public class BasicProxyMap implements Map {
+
+    /** The default capacity to use. Always use a power of 2!!! */
+    private static final int DEFAULT_CAPACITY = 16;
+    /** The default load factor to use */
+    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
+    /** The maximum capacity allowed */
+    private static final int MAXIMUM_CAPACITY = 1 << 30;
+    
+    /** Load factor, normally 0.75 */
+    private float loadFactor;
+    /** The size of the map */
+    private transient int size;
+    /** Map entries */
+    private transient ReferenceEntry[] data;
+    /** Size at which to rehash */
+    private transient int threshold;
+
+    /**
+     * ReferenceQueue used to eliminate GC'ed entries.
+     */
+    private ReferenceQueue purgeQueue;
+
+    public BasicProxyMap() {
+        this.loadFactor = DEFAULT_LOAD_FACTOR;
+        this.data = new ReferenceEntry[DEFAULT_CAPACITY];
+        this.threshold = calculateThreshold(DEFAULT_CAPACITY, loadFactor);
+        this.purgeQueue = new ReferenceQueue();
+    }
+    
+    /**
+     * Gets the size of the map.
+     * 
+     * @return the size
+     */
+    public int size() {
+        purge();
+        return size;
+    }
+
+    /**
+     * Checks whether the map is currently empty.
+     * 
+     * @return true if the map is currently size zero
+     */
+    public boolean isEmpty() {
+        purge();
+        return (size == 0);
+    }
+
+    /**
+     * Checks whether the map contains the specified key.
+     * 
+     * @param key  the key to search for
+     * @return true if the map contains the key
+     */
+    public boolean containsKey(Object key) {
+        purge();
+        ReferenceEntry entry = getEntry(key);
+        if (entry == null) {
+            return false;
+        }
+        return (entry.getValue() != null);
+    }
+
+    /**
+     * Checks whether the map contains the specified value.
+     * 
+     * @param value  the value to search for
+     * @return true if the map contains the value
+     */
+    public boolean containsValue(Object value) {
+        purge();
+        if (value == null || size == 0) {
+            return false;
+        }
+        ReferenceEntry [] table = data;
+        for (int i = 0; i < table.length; i++) {
+            ReferenceEntry entry = table[i];
+            while (entry != null) {
+                if (value.equals(entry.getValue())) {
+                    return true;
+                }
+                entry = entry.next;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Gets the value mapped to the key specified.
+     * 
+     * @param key  the key
+     * @return the mapped value, null if no match
+     */
+    public Object get(Object key) {
+        purge();
+        ReferenceEntry entry = getEntry(key);
+        if (entry == null) {
+            return null;
+        }
+        return entry.getValue();
+    }
+
+
+    /**
+     * Puts a key-value entry into this map.
+     * Neither the key nor the value may be null.
+     * 
+     * @param key  the key to add, must not be null
+     * @param value  the value to add, must not be null
+     * @return the value previously mapped to this key, null if none
+     */
+    public Object put(Object key, Object value) {
+        assert key != null: "key is null";
+        assert value != null: "value is null";
+
+        purge();
+
+        int hashCode = hash(key);
+        int index = hashIndex(hashCode, data.length);
+        ReferenceEntry entry = data[index];
+        while (entry != null) {
+            if (entry.hashCode == hashCode && key == entry.getKey()) {
+                return entry.setValue(value);
+            }
+            entry = entry.next;
+        }   
+            
+        createEntry(index, hashCode, key, value);
+        return null;
+    }
+    
+    /**
+     * Removes the specified mapping from this map.
+     * 
+     * @param key  the mapping to remove
+     * @return the value mapped to the removed key, null if key not in map
+     */
+    public Object remove(Object key) {
+        if (key == null) {
+            return null;
+        }
+        purge();
+        int hashCode = hash(key);
+        int index = hashIndex(hashCode, data.length);
+        ReferenceEntry entry = data[index];
+        ReferenceEntry previous = null;
+        while (entry != null) {
+            if (entry.hashCode == hashCode && (key == entry.getKey())) {
+                Object oldValue = entry.getValue();
+                removeEntry(entry, index, previous);
+                return oldValue;
+            }
+            previous = entry;
+            entry = entry.next;
+        }
+        return null;
+    }
+
+    /**
+     * Clears the map, resetting the size to zero and nullifying references
+     * to avoid garbage collection issues.
+     */
+    public void clear() {
+        ReferenceEntry[] data = this.data;
+        for (int i = data.length - 1; i >= 0; i--) {
+            data[i] = null;
+        }
+        size = 0;
+        while (purgeQueue.poll() != null) {} // drain the queue
+    }
+
+    public Collection values() {
+        throw new UnsupportedOperationException();
+    }
+
+    public void putAll(Map t) {
+        throw new UnsupportedOperationException();
+    }
+
+    public Set entrySet() {
+        throw new UnsupportedOperationException();
+    }
+
+    public Set keySet() {
+        throw new UnsupportedOperationException();
+    }
+
+    // end of public methods
+    
+    /**
+     * Gets the entry mapped to the key specified.
+     * @param key  the key
+     * @return the entry, null if no match
+     */
+    private ReferenceEntry getEntry(Object key) {
+        if (key == null) {
+            return null;
+        }
+        int hashCode = hash(key);
+        ReferenceEntry entry = data[hashIndex(hashCode, data.length)];
+        while (entry != null) {
+            if (entry.hashCode == hashCode && (key == entry.getKey())) {
+                return entry;
+            }
+            entry = entry.next;
+        }
+        return null;
+    }
+
+    /**
+     * Creates a new ReferenceEntry.
+     * 
+     * @param index the index into the data map
+     * @param hashCode  the hash code for the new entry
+     * @param key  the key to store
+     * @param value  the value to store
+     * @return the newly created entry
+     */
+    private ReferenceEntry createEntry(int index, int hashCode, Object key, Object value) {
+        ReferenceEntry newEntry = new ReferenceEntry(this, data[index], hashCode, key, value);
+        data[index] = newEntry;
+        size++;
+        checkCapacity();
+        return newEntry;
+    }
+
+    /**
+     * Removes an entry from the chain stored in a particular index.
+     * <p>
+     * This implementation removes the entry from the data storage table.
+     * The size is not updated.
+     * 
+     * @param entry  the entry to remove
+     * @param hashIndex  the index into the data structure
+     * @param previous  the previous entry in the chain
+     */
+    private void removeEntry(ReferenceEntry entry, int hashIndex, ReferenceEntry previous) {
+        if (previous == null) {
+            data[hashIndex] = entry.next;
+        } else {
+            previous.next = entry.next;
+        }
+        size--;
+        entry.next = null;
+        entry.clear();
+        entry.value = null;
+    }
+    
+    /**
+     * Checks the capacity of the map and enlarges it if necessary.
+     * <p>
+     * This implementation uses the threshold to check if the map needs enlarging
+     */
+    private void checkCapacity() {
+        if (size >= threshold) {
+            int newCapacity = data.length * 2;
+            if (newCapacity <= MAXIMUM_CAPACITY) {
+                ensureCapacity(newCapacity);
+            }
+        }
+    }
+    
+    /**
+     * Changes the size of the data structure to the capacity proposed.
+     * 
+     * @param newCapacity  the new capacity of the array (a power of two, less or equal to max)
+     */
+    private void ensureCapacity(int newCapacity) {
+        int oldCapacity = data.length;
+        if (newCapacity <= oldCapacity) {
+            return;
+        }
+
+        ReferenceEntry oldEntries[] = data;
+        ReferenceEntry newEntries[] = new ReferenceEntry[newCapacity];
+
+        for (int i = oldCapacity - 1; i >= 0; i--) {
+            ReferenceEntry entry = oldEntries[i];
+            if (entry != null) {
+                oldEntries[i] = null;  // gc
+                do {
+                    ReferenceEntry next = entry.next;
+                    int index = hashIndex(entry.hashCode, newCapacity);  
+                    entry.next = newEntries[index];
+                    newEntries[index] = entry;
+                    entry = next;
+                } while (entry != null);
+            }
+        }
+        threshold = calculateThreshold(newCapacity, loadFactor);
+        data = newEntries;
+    }
+
+    /**
+     * Calculates the new threshold of the map, where it will be resized.
+     * This implementation uses the load factor.
+     * 
+     * @param newCapacity  the new capacity
+     * @param factor  the load factor
+     * @return the new resize threshold
+     */
+    private int calculateThreshold(int newCapacity, float factor) {
+        return (int) (newCapacity * factor);
+    }
+
+    /**
+     * Gets the hash code for the key specified.
+     * <p>
+     * This implementation uses the identity hash code.
+     * 
+     * @param key  the key to get a hash code for
+     * @return the hash code
+     */
+    private int hash(Object key) {
+        return System.identityHashCode(key);
+    }
+
+    /**
+     * Gets the index into the data storage for the hashCode specified.
+     * This implementation uses the least significant bits of the hashCode.
+     * 
+     * @param hashCode  the hash code to use
+     * @param dataSize  the size of the data to pick a bucket from
+     * @return the bucket index
+     */
+    private int hashIndex(int hashCode, int dataSize) {
+        return hashCode & (dataSize - 1);
+    }
+
+    // Code that handles WeakReference cleanup... Invoked prior to 
+    // any operation accessing the ReferenceEntry array...
+    
+    /**
+     * Purges stale mappings from this map.
+     * <p>
+     * Note that this method is not synchronized!  Special
+     * care must be taken if, for instance, you want stale
+     * mappings to be removed on a periodic basis by some
+     * background thread.
+     */
+    private void purge() {
+        Reference entryRef = purgeQueue.poll();
+        while (entryRef != null) {
+            purge(entryRef);
+            entryRef = purgeQueue.poll();
+        }
+    }
+
+    /**
+     * Purges the specified reference.
+     * 
+     * @param ref  the reference to purge
+     */
+    private void purge(Reference purgedEntry) {
+        int hash = ((ReferenceEntry)purgedEntry).hashCode;
+        int index = hashIndex(hash, data.length);
+        ReferenceEntry previous = null;
+        ReferenceEntry currentEntry = data[index];
+        while (currentEntry != null) {
+            if (currentEntry == purgedEntry) {
+                currentEntry.purged();
+                if (previous == null) {
+                    data[index] = currentEntry.next;
+                } else {
+                    previous.next = currentEntry.next;
+                }
+                this.size--;
+                return;
+            }
+            previous = currentEntry;
+            currentEntry = currentEntry.next;
+        }
+    }
+
+    /**
+     * Each entry in the Map is represented with a ReferenceEntry.
+     * <p>
+     * If getKey() or getValue() returns null, it means
+     * the mapping is stale and should be removed.
+     * 
+     * @since Commons Collections 3.1
+     */
+    private static class ReferenceEntry extends WeakReference {
+        /** The next entry in the hash chain */
+        private ReferenceEntry next;
+        /** The hash code of the key */
+        private int hashCode;
+        /** The value */
+        private Object value;
+
+        /**
+         * Creates a new entry object for the ReferenceMap.
+         * 
+         * @param parent  the parent map
+         * @param next  the next entry in the hash bucket
+         * @param hashCode  the hash code of the key
+         * @param key  the key
+         * @param value  the value
+         */
+        private ReferenceEntry(BasicProxyMap parent, ReferenceEntry next, int hashCode, Object key, Object value) {
+            super(key, parent.purgeQueue);
+            this.next = next;
+            this.hashCode = hashCode;
+            this.value = value;
+        }
+
+        /**
+         * Gets the key from the entry.
+         * This method dereferences weak and soft keys and thus may return null.
+         * 
+         * @return the key, which may be null if it was garbage collected
+         */
+        private Object getKey() {
+            return this.get();
+        }
+
+        /**
+         * Gets the value from the entry.
+         * This method dereferences weak and soft value and thus may return null.
+         * 
+         * @return the value, which may be null if it was garbage collected
+         */
+        private Object getValue() {
+            return value;
+        }
+
+        /**
+         * Sets the value of the entry.
+         * 
+         * @param obj  the object to store
+         * @return the previous value
+         */
+        private Object setValue(Object obj) {
+            Object old = getValue();
+            value = obj;
+            return old;
+        }
+
+        /**
+         * Purges this entry.
+         */
+        private void purged() {
+            this.clear();
+            value = null;
+        }
+    }
+}

Added: geronimo/trunk/modules/kernel/src/test/org/apache/geronimo/kernel/basic/BasicProxyMapTest.java
URL: http://svn.apache.org/viewcvs/geronimo/trunk/modules/kernel/src/test/org/apache/geronimo/kernel/basic/BasicProxyMapTest.java?rev=328153&view=auto
==============================================================================
--- geronimo/trunk/modules/kernel/src/test/org/apache/geronimo/kernel/basic/BasicProxyMapTest.java (added)
+++ geronimo/trunk/modules/kernel/src/test/org/apache/geronimo/kernel/basic/BasicProxyMapTest.java Mon Oct 24 14:29:04 2005
@@ -0,0 +1,157 @@
+package org.apache.geronimo.kernel.basic;
+
+import junit.framework.TestCase;
+
+public class BasicProxyMapTest extends TestCase {
+    private BasicProxyMap map;
+    
+    public void testPut() {
+        Object key1 = new Object();
+        Object value1 = new Object();
+        Object key2 = new Object();
+        Object value2 = new Object();
+
+        // test expected start conditions
+        assertEquals(0,map.size());
+        assertTrue(map.isEmpty());
+        assertNull(map.get(key1));
+        assertFalse(map.containsKey(key1));
+        assertFalse(map.containsValue(value1));
+        
+        // test put
+        assertNull(map.put(key1, value1));
+        assertEquals(1,map.size());
+        assertFalse(map.isEmpty());
+        assertSame(value1, map.put(key1,value2));
+        assertSame(value2, map.put(key1,value1));
+        assertNull(map.put(key2, value2));
+        assertEquals(2,map.size());
+    }
+    
+    public void testContains() {
+        Object key1 = new Object();
+        Object value1 = new Object();
+        Object key2 = new Object();
+        Object value2 = new Object();
+
+        map.put(key1,value1);
+        map.put(key2, value2);
+        
+        assertTrue(map.containsKey(key1));
+        assertTrue(map.containsValue(value1));
+        assertTrue(map.containsKey(key2));
+        assertTrue(map.containsValue(value2));
+        assertFalse(map.containsKey(new Object()));
+        assertFalse(map.containsValue(new Object()));
+    }
+    
+    public void testGet() {
+        Object key1 = new Object();
+        Object value1 = new Object();
+        Object key2 = new Object();
+        Object value2 = new Object();
+
+        map.put(key1,value1);
+        map.put(key2, value2);
+        
+        assertSame(value1,map.get(key1));
+        assertSame(value2,map.get(key2));
+        assertNull(map.get(new Object()));
+    }
+    
+    public void testRemove() {
+        Object key1 = new Object();
+        Object value1 = new Object();
+        Object key2 = new Object();
+        Object value2 = new Object();
+
+        map.put(key1,value1);
+        map.put(key2, value2);
+
+        assertSame(value1,map.remove(key1));
+        assertEquals(1,map.size());
+        assertFalse(map.isEmpty());
+        assertFalse(map.containsKey(key1));
+        assertFalse(map.containsValue(value1));
+        assertNull(map.get(key1));
+        assertNull(map.put(key1, value1));
+    }
+    
+    public void testClear() {
+        Object key1 = new Object();
+        Object value1 = new Object();
+        Object key2 = new Object();
+        Object value2 = new Object();
+
+        map.put(key1,value1);
+        map.put(key2, value2);
+        map.clear();
+        
+        assertEquals(0,map.size());
+        assertTrue(map.isEmpty());
+        assertNull(map.get(key1));
+        assertFalse(map.containsKey(key1));
+        assertFalse(map.containsValue(value1));
+    }
+    
+    public void testIdentitySemantic() {
+        Object value1 = new Object();
+        Object value2 = new Object();
+
+        class IdentityTest {
+            public int hashCode() { return 1; }
+            public boolean equals(Object o) { return true; }
+        }
+        class IdentityTest2 {
+            public int hashCode() { return 1; }
+            public boolean equals(Object o) { return false; }
+        }
+        
+        Object key1 = new IdentityTest();
+        Object key2 = new IdentityTest();
+        
+        assertNull(map.put(key1, value1));
+        assertNull(map.get(key2));
+        assertNull(map.put(key2, value2));
+        assertSame(value1, map.get(key1));
+        assertSame(value2, map.get(key2));
+        map.clear();
+
+        key1 = new IdentityTest2();
+        key2 = new IdentityTest2();
+        
+        assertNull(map.put(key1, value1));
+        assertNull(map.get(key2));
+        assertNull(map.put(key2, value2));
+        assertSame(value1, map.get(key1));
+        assertSame(value2, map.get(key2));
+    }
+    
+    public void testWeakSemantic() {
+        int MAX = 256;
+        Object [] keys = new Object[MAX];
+        Object [] values = new Object[MAX];
+        for (int i=0;i<MAX;i++) {
+            keys[i] = new Object(); values[i] = new Object();
+            assertNull(map.put(keys[i],values[i]));
+        }
+        for (int i=0;i<MAX;i++) {
+            assertEquals(values[i], map.get(keys[i]));
+        }
+        
+        // now add a number of entries that will be available for GCing
+        for (int i=0;i<MAX;i++) {
+            assertNull(map.put(new Object(), new Object()));
+        }
+        System.gc();
+        try {Thread.sleep(1000);} catch (InterruptedException ie) {}
+        assertEquals(MAX,map.size());
+        for (int i=0;i<MAX;i++) {
+            assertSame(values[i], map.get(keys[i]));
+        }
+    }
+
+    protected void setUp() throws Exception {
+        map = new BasicProxyMap();
+    }    
+}