From 2f2b9d43eb27913dd5c079f3a4ea2a6e6b8d2013 Mon Sep 17 00:00:00 2001 From: Robert Burrell Donkin Date: Sun, 11 Dec 2005 21:21:10 +0000 Subject: [PATCH] Added WeakHashtable to standard distribution. git-svn-id: https://svn.apache.org/repos/asf/jakarta/commons/proper/logging/trunk@356024 13f79535-47bb-0310-9956-ffa450edef68 --- .../commons/logging/impl/WeakHashtable.java | 488 ++++++++++++++++++ .../logging/impl/WeakHashtableTest.java | 249 +++++++++ 2 files changed, 737 insertions(+) create mode 100644 src/java/org/apache/commons/logging/impl/WeakHashtable.java create mode 100644 src/test/org/apache/commons/logging/impl/WeakHashtableTest.java diff --git a/src/java/org/apache/commons/logging/impl/WeakHashtable.java b/src/java/org/apache/commons/logging/impl/WeakHashtable.java new file mode 100644 index 0000000..232b365 --- /dev/null +++ b/src/java/org/apache/commons/logging/impl/WeakHashtable.java @@ -0,0 +1,488 @@ +/* + * Copyright 2004 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.commons.logging.impl; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.*; + +/** + *

Implementation of Hashtable that uses WeakReference's + * to hold its keys thus allowing them to be reclaimed by the garbage collector. + * The associated values are retained using strong references.

+ * + *

This class follows the symantics of Hashtable as closely as + * possible. It therefore does not accept null values or keys.

+ * + *

Note: + * This is not intended to be a general purpose hash table replacement. + * This implementation is also tuned towards a particular purpose: for use as a replacement + * for Hashtable in LogFactory. This application requires + * good liveliness for get and put. Various tradeoffs + * have been made with this in mind. + *

+ *

+ * Usage: typical use case is as a drop-in replacement + * for the Hashtable used in LogFactory for J2EE enviroments + * running 1.3+ JVMs. Use of this class in most cases (see below) will + * allow classloaders to be collected by the garbage collector without the need + * to call {@link org.apache.commons.logging.LogFactory#release(ClassLoader) LogFactory.release(ClassLoader)}. + *

+ * + *

org.apache.commons.logging.LogFactory looks to see whether this + * class is present in the classpath, and if so then uses it to store + * references to the LogFactory implementationd it loads + * (rather than using a standard Hashtable instance). + * Having this class used instead of Hashtable solves + * certain issues related to dynamic reloading of applications in J2EE-style + * environments. However this class requires java 1.3 or later (due to its use + * of java.lang.ref.WeakReference and associates) and therefore cannot be + * included in the main logging distribution which supports JVMs prior to 1.3. + * And by the way, this extends Hashtable rather than HashMap + * for backwards compatibility reasons. See the documentation + * for method LogFactory.createFactoryStore for more details.

+ * + *

The reason all this is necessary is due to a issue which + * arises during hot deploy in a J2EE-like containers. + * Each component running in the container owns one or more classloaders; when + * the component loads a LogFactory instance via the component classloader + * a reference to it gets stored in the static LogFactory.factories member, + * keyed by the component's classloader so different components don't + * stomp on each other. When the component is later unloaded, the container + * sets the component's classloader to null with the intent that all the + * component's classes get garbage-collected. However there's still a + * reference to the component's classloader from the "global" LogFactory's + * factories member! If LogFactory.release() is called whenever component + * is unloaded (as happens in some famous containers), the classloaders will be correctly + * garbage collected. + * However, holding the classloader references weakly ensures that the classloader + * will be garbage collected without programmatic intervention. + * Unfortunately, weak references are + * only available in java 1.3+, so this code only uses WeakHashtable if the + * class has explicitly been made available on the classpath.

+ * + *

+ * Because the presence of this class in the classpath ensures proper + * unload of components without the need to call method + * {@link org.apache.commons.logging.LogFactory#release(ClassLoader) LogFactory.release ClassLoader)}, + * it is recommended that this class be deployed along with the standard + * commons-logging.jar file when using commons-logging in J2EE + * environments (which will presumably be running on Java 1.3 or later). + * There are no know ill effects from using this class.

+ * + *

+ * Limitations: + * There is still one (unusual) scenario in which a component will not + * be correctly unloaded without an explicit release. Though weak references + * are used for its keys, it is necessary to use + * strong references for its values.

+ * + *

If the abstract class LogFactory is + * loaded by the container classloader but a subclass of + * LogFactory [LogFactory1] is loaded by the component's + * classloader and an instance stored in the static map associated with the + * base LogFactory class, then there is a strong reference from the LogFactory + * class to the LogFactory1 instance (as normal) and a strong reference from + * the LogFactory1 instance to the component classloader via + * getClass().getClassLoader(). This chain of references will prevent + * collection of the child classloader.

+ * + *

+ * Such a situation occurs when the commons-logging.jar is + * loaded by a parent classloader (e.g. a server level classloader in a + * servlet container) and a custom LogFactory implementation is + * loaded by a child classloader (e.g. a web app classloader).

+ * + *

To avoid this scenario, ensure + * that any custom LogFactory subclass is loaded by the same classloader as + * the base LogFactory. Creating custom LogFactory subclasses is, + * however, rare. The standard LogFactoryImpl class should be sufficient + * for most or all users.

+ * + * + * @author Brian Stansberry + */ +public final class WeakHashtable extends Hashtable { + + /** + * The maximum number of times put() or remove() can be called before + * the map will be purged of all cleared entries. + */ + private static final int MAX_CHANGES_BEFORE_PURGE = 100; + + /** + * The maximum number of times put() or remove() can be called before + * the map will be purged of one cleared entry. + */ + private static final int PARTIAL_PURGE_COUNT = 10; + + /* ReferenceQueue we check for gc'd keys */ + private ReferenceQueue queue = new ReferenceQueue(); + /* Counter used to control how often we purge gc'd entries */ + private int changeCount = 0; + + /** + * Constructs a WeakHashtable with the Hashtable default + * capacity and load factor. + */ + public WeakHashtable() {} + + + /** + *@see Hashtable + */ + public boolean containsKey(Object key) { + // purge should not be required + Referenced referenced = new Referenced(key); + return super.containsKey(referenced); + } + + /** + *@see Hashtable + */ + public Enumeration elements() { + purge(); + return super.elements(); + } + + /** + *@see Hashtable + */ + public Set entrySet() { + purge(); + Set referencedEntries = super.entrySet(); + Set unreferencedEntries = new HashSet(); + for (Iterator it=referencedEntries.iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + Referenced referencedKey = (Referenced) entry.getKey(); + Object key = referencedKey.getValue(); + Object value = entry.getValue(); + if (key != null) { + Entry dereferencedEntry = new Entry(key, value); + unreferencedEntries.add(dereferencedEntry); + } + } + return unreferencedEntries; + } + + /** + *@see Hashtable + */ + public Object get(Object key) { + // for performance reasons, no purge + Referenced referenceKey = new Referenced(key); + return super.get(referenceKey); + } + + /** + *@see Hashtable + */ + public Enumeration keys() { + purge(); + final Enumeration enumer = super.keys(); + return new Enumeration() { + public boolean hasMoreElements() { + return enumer.hasMoreElements(); + } + public Object nextElement() { + Referenced nextReference = (Referenced) enumer.nextElement(); + return nextReference.getValue(); + } + }; + } + + + /** + *@see Hashtable + */ + public Set keySet() { + purge(); + Set referencedKeys = super.keySet(); + Set unreferencedKeys = new HashSet(); + for (Iterator it=referencedKeys.iterator(); it.hasNext();) { + Referenced referenceKey = (Referenced) it.next(); + Object keyValue = referenceKey.getValue(); + if (keyValue != null) { + unreferencedKeys.add(keyValue); + } + } + return unreferencedKeys; + } + + /** + *@see Hashtable + */ + public Object put(Object key, Object value) { + // check for nulls, ensuring symantics match superclass + if (key == null) { + throw new NullPointerException("Null keys are not allowed"); + } + if (value == null) { + throw new NullPointerException("Null values are not allowed"); + } + + // for performance reasons, only purge every + // MAX_CHANGES_BEFORE_PURGE times + if (changeCount++ > MAX_CHANGES_BEFORE_PURGE) { + purge(); + changeCount = 0; + } + // do a partial purge more often + else if ((changeCount % PARTIAL_PURGE_COUNT) == 0) { + purgeOne(); + } + + Object result = null; + Referenced keyRef = new Referenced(key, queue); + return super.put(keyRef, value); + } + + /** + *@see Hashtable + */ + public void putAll(Map t) { + if (t != null) { + Set entrySet = t.entrySet(); + for (Iterator it=entrySet.iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + put(entry.getKey(), entry.getValue()); + } + } + } + + /** + *@see Hashtable + */ + public Collection values() { + purge(); + return super.values(); + } + + /** + *@see Hashtable + */ + public Object remove(Object key) { + // for performance reasons, only purge every + // MAX_CHANGES_BEFORE_PURGE times + if (changeCount++ > MAX_CHANGES_BEFORE_PURGE) { + purge(); + changeCount = 0; + } + // do a partial purge more often + else if ((changeCount % PARTIAL_PURGE_COUNT) == 0) { + purgeOne(); + } + return super.remove(new Referenced(key)); + } + + /** + *@see Hashtable + */ + public boolean isEmpty() { + purge(); + return super.isEmpty(); + } + + /** + *@see Hashtable + */ + public int size() { + purge(); + return super.size(); + } + + /** + *@see Hashtable + */ + public String toString() { + purge(); + return super.toString(); + } + + /** + * @see Hashtable + */ + protected void rehash() { + // purge here to save the effort of rehashing dead entries + purge(); + super.rehash(); + } + + /** + * Purges all entries whose wrapped keys + * have been garbage collected. + */ + private void purge() { + synchronized (queue) { + WeakKey key; + while ((key = (WeakKey) queue.poll()) != null) { + super.remove(key.getReferenced()); + } + } + } + + /** + * Purges one entry whose wrapped key + * has been garbage collected. + */ + private void purgeOne() { + + synchronized (queue) { + WeakKey key = (WeakKey) queue.poll(); + if (key != null) { + super.remove(key.getReferenced()); + } + } + } + + /** Entry implementation */ + private final static class Entry implements Map.Entry { + + private final Object key; + private final Object value; + + private Entry(Object key, Object value) { + this.key = key; + this.value = value; + } + + public boolean equals(Object o) { + boolean result = false; + if (o != null && o instanceof Map.Entry) { + Map.Entry entry = (Map.Entry) o; + result = (getKey()==null ? + entry.getKey() == null : + getKey().equals(entry.getKey())) + && + (getValue()==null ? + entry.getValue() == null : + getValue().equals(entry.getValue())); + } + return result; + } + + public int hashCode() { + + return (getKey()==null ? 0 : getKey().hashCode()) ^ + (getValue()==null ? 0 : getValue().hashCode()); + } + + public Object setValue(Object value) { + throw new UnsupportedOperationException("Entry.setValue is not supported."); + } + + public Object getValue() { + return value; + } + + public Object getKey() { + return key; + } + } + + + /** Wrapper giving correct symantics for equals and hashcode */ + private final static class Referenced { + + private final WeakReference reference; + private final int hashCode; + + /** + * + * @throws NullPointerException if referant is null + */ + private Referenced(Object referant) { + reference = new WeakReference(referant); + // Calc a permanent hashCode so calls to Hashtable.remove() + // work if the WeakReference has been cleared + hashCode = referant.hashCode(); + } + + /** + * + * @throws NullPointerException if key is null + */ + private Referenced(Object key, ReferenceQueue queue) { + reference = new WeakKey(key, queue, this); + // Calc a permanent hashCode so calls to Hashtable.remove() + // work if the WeakReference has been cleared + hashCode = key.hashCode(); + + } + + public int hashCode() { + return hashCode; + } + + private Object getValue() { + return reference.get(); + } + + public boolean equals(Object o) { + boolean result = false; + if (o instanceof Referenced) { + Referenced otherKey = (Referenced) o; + Object thisKeyValue = getValue(); + Object otherKeyValue = otherKey.getValue(); + if (thisKeyValue == null) { + result = (otherKeyValue == null); + + // Since our hashcode was calculated from the original + // non-null referant, the above check breaks the + // hashcode/equals contract, as two cleared Referenced + // objects could test equal but have different hashcodes. + // We can reduce (not eliminate) the chance of this + // happening by comparing hashcodes. + if (result == true) { + result = (this.hashCode() == otherKey.hashCode()); + } + // In any case, as our c'tor does not allow null referants + // and Hashtable does not do equality checks between + // existing keys, normal hashtable operations should never + // result in an equals comparison between null referants + } + else + { + result = thisKeyValue.equals(otherKeyValue); + } + } + return result; + } + } + + /** + * WeakReference subclass that holds a hard reference to an + * associated value and also makes accessible + * the Referenced object holding it. + */ + private final static class WeakKey extends WeakReference { + + private final Referenced referenced; + + private WeakKey(Object key, + ReferenceQueue queue, + Referenced referenced) { + super(key, queue); + this.referenced = referenced; + } + + private Referenced getReferenced() { + return referenced; + } + } +} diff --git a/src/test/org/apache/commons/logging/impl/WeakHashtableTest.java b/src/test/org/apache/commons/logging/impl/WeakHashtableTest.java new file mode 100644 index 0000000..7798f74 --- /dev/null +++ b/src/test/org/apache/commons/logging/impl/WeakHashtableTest.java @@ -0,0 +1,249 @@ +/* + * Copyright 2004 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.commons.logging.impl; + +import java.lang.ref.*; +import junit.framework.*; +import java.util.*; + +public class WeakHashtableTest extends TestCase { + + + /** Maximum number of iterations before our test fails */ + private static final int MAX_GC_ITERATIONS = 50; + + private WeakHashtable weakHashtable; + private Long keyOne; + private Long keyTwo; + private Long keyThree; + private Long valueOne; + private Long valueTwo; + private Long valueThree; + + public WeakHashtableTest(String testName) { + super(testName); + } + + + protected void setUp() throws Exception { + super.setUp(); + weakHashtable = new WeakHashtable(); + + keyOne = new Long(1); + keyTwo = new Long(2); + keyThree = new Long(3); + valueOne = new Long(100); + valueTwo = new Long(200); + valueThree = new Long(300); + + weakHashtable.put(keyOne, valueOne); + weakHashtable.put(keyTwo, valueTwo); + weakHashtable.put(keyThree, valueThree); + } + + /** Tests public boolean contains(ObjectÊvalue) */ + public void testContains() throws Exception { + assertFalse(weakHashtable.contains(new Long(1))); + assertFalse(weakHashtable.contains(new Long(2))); + assertFalse(weakHashtable.contains(new Long(3))); + assertTrue(weakHashtable.contains(new Long(100))); + assertTrue(weakHashtable.contains(new Long(200))); + assertTrue(weakHashtable.contains(new Long(300))); + assertFalse(weakHashtable.contains(new Long(400))); + } + + /** Tests public boolean containsKey(ObjectÊkey) */ + public void testContainsKey() throws Exception { + assertTrue(weakHashtable.containsKey(new Long(1))); + assertTrue(weakHashtable.containsKey(new Long(2))); + assertTrue(weakHashtable.containsKey(new Long(3))); + assertFalse(weakHashtable.containsKey(new Long(100))); + assertFalse(weakHashtable.containsKey(new Long(200))); + assertFalse(weakHashtable.containsKey(new Long(300))); + assertFalse(weakHashtable.containsKey(new Long(400))); + } + + /** Tests public boolean containsValue(ObjectÊvalue) */ + public void testContainsValue() throws Exception { + assertFalse(weakHashtable.containsValue(new Long(1))); + assertFalse(weakHashtable.containsValue(new Long(2))); + assertFalse(weakHashtable.containsValue(new Long(3))); + assertTrue(weakHashtable.containsValue(new Long(100))); + assertTrue(weakHashtable.containsValue(new Long(200))); + assertTrue(weakHashtable.containsValue(new Long(300))); + assertFalse(weakHashtable.containsValue(new Long(400))); + } + + /** Tests public Enumeration elements() */ + public void testElements() throws Exception { + ArrayList elements = new ArrayList(); + for (Enumeration e = weakHashtable.elements(); e.hasMoreElements();) { + elements.add(e.nextElement()); + } + assertEquals(3, elements.size()); + assertTrue(elements.contains(valueOne)); + assertTrue(elements.contains(valueTwo)); + assertTrue(elements.contains(valueThree)); + } + + /** Tests public Set entrySet() */ + public void testEntrySet() throws Exception { + Set entrySet = weakHashtable.entrySet(); + for (Iterator it = entrySet.iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + Object key = entry.getKey(); + if (keyOne.equals(key)) { + assertEquals(valueOne, entry.getValue()); + } else if (keyTwo.equals(key)) { + assertEquals(valueTwo, entry.getValue()); + } else if (keyThree.equals(key)) { + assertEquals(valueThree, entry.getValue()); + } else { + fail("Unexpected key"); + } + } + } + + /** Tests public Object get(ObjectÊkey) */ + public void testGet() throws Exception { + assertEquals(valueOne, weakHashtable.get(keyOne)); + assertEquals(valueTwo, weakHashtable.get(keyTwo)); + assertEquals(valueThree, weakHashtable.get(keyThree)); + assertNull(weakHashtable.get(new Long(50))); + } + + /** Tests public Enumeration keys() */ + public void testKeys() throws Exception { + ArrayList keys = new ArrayList(); + for (Enumeration e = weakHashtable.keys(); e.hasMoreElements();) { + keys.add(e.nextElement()); + } + assertEquals(3, keys.size()); + assertTrue(keys.contains(keyOne)); + assertTrue(keys.contains(keyTwo)); + assertTrue(keys.contains(keyThree)); + } + + /** Tests public Set keySet() */ + public void testKeySet() throws Exception { + Set keySet = weakHashtable.keySet(); + assertEquals(3, keySet.size()); + assertTrue(keySet.contains(keyOne)); + assertTrue(keySet.contains(keyTwo)); + assertTrue(keySet.contains(keyThree)); + } + + /** Tests public Object put(ObjectÊkey, ObjectÊvalue) */ + public void testPut() throws Exception { + Long anotherKey = new Long(2004); + weakHashtable.put(anotherKey, new Long(1066)); + + assertEquals(new Long(1066), weakHashtable.get(anotherKey)); + + // Test compliance with the hashtable API re nulls + Exception caught = null; + try { + weakHashtable.put(null, new Object()); + } + catch (Exception e) { + caught = e; + } + assertNotNull("did not throw an exception adding a null key", caught); + caught = null; + try { + weakHashtable.put(new Object(), null); + } + catch (Exception e) { + caught = e; + } + assertNotNull("did not throw an exception adding a null value", caught); + } + + /** Tests public void putAll(MapÊt) */ + public void testPutAll() throws Exception { + Map newValues = new HashMap(); + Long newKey = new Long(1066); + Long newValue = new Long(1415); + newValues.put(newKey, newValue); + Long anotherNewKey = new Long(1645); + Long anotherNewValue = new Long(1815); + newValues.put(anotherNewKey, anotherNewValue); + weakHashtable.putAll(newValues); + + assertEquals(5, weakHashtable.size()); + assertEquals(newValue, weakHashtable.get(newKey)); + assertEquals(anotherNewValue, weakHashtable.get(anotherNewKey)); + } + + /** Tests public Object remove(ObjectÊkey) */ + public void testRemove() throws Exception { + weakHashtable.remove(keyOne); + assertEquals(2, weakHashtable.size()); + assertNull(weakHashtable.get(keyOne)); + } + + /** Tests public Collection values() */ + public void testValues() throws Exception { + Collection values = weakHashtable.values(); + assertEquals(3, values.size()); + assertTrue(values.contains(valueOne)); + assertTrue(values.contains(valueTwo)); + assertTrue(values.contains(valueThree)); + } + + public void testRelease() throws Exception { + assertNotNull(weakHashtable.get(new Long(1))); + ReferenceQueue testQueue = new ReferenceQueue(); + WeakReference weakKeyOne = new WeakReference(keyOne, testQueue); + + // lose our references + keyOne = null; + keyTwo = null; + keyThree = null; + valueOne = null; + valueTwo = null; + valueThree = null; + + int iterations = 0; + int bytz = 2; + while(true) { + System.gc(); + if(iterations++ > MAX_GC_ITERATIONS){ + fail("Max iterations reached before resource released."); + } + + if(weakHashtable.get(new Long(1)) == null) { + break; + + } else { + // create garbage: + byte[] b = new byte[bytz]; + bytz = bytz * 2; + } + } + + // some JVMs seem to take a little time to put references on + // the reference queue once the reference has been collected + // need to think about whether this is enough to justify + // stepping through the collection each time... + while(testQueue.poll() == null) {} + + // Test that the released objects are not taking space in the table + assertEquals("underlying table not emptied", 0, weakHashtable.size()); + } +}