blob: cf66955c41ecd7ceebc19e6dcfce025a26604127 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Implementation of {@code Hashtable} that uses {@code WeakReference}'s
* to hold its keys thus allowing them to be reclaimed by the garbage collector.
* The associated values are retained using strong references.
* <p>
* This class follows the semantics of {@code Hashtable} as closely as
* possible. It therefore does not accept null values or keys.
* <p>
* <strong>Note:</strong>
* This is <em>not</em> intended to be a general purpose hash table replacement.
* This implementation is also tuned towards a particular purpose: for use as a replacement
* for {@code Hashtable} in {@code LogFactory}. This application requires
* good liveliness for {@code get} and {@code put}. Various tradeoffs
* have been made with this in mind.
* <p>
* <strong>Usage:</strong> typical use case is as a drop-in replacement
* for the {@code Hashtable} used in {@code LogFactory} for J2EE environments
* running 1.3+ JVMs. Use of this class <i>in most cases</i> (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)}.
* <p>
* {@code org.apache.commons.logging.LogFactory} checks whether this class
* can be supported by the current JVM, and if so then uses it to store
* references to the {@code LogFactory} implementation it loads
* (rather than using a standard Hashtable instance).
* Having this class used instead of {@code 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 {@code java.lang.ref.WeakReference} and associates).
* And by the way, this extends {@code Hashtable} rather than {@code HashMap}
* for backwards compatibility reasons. See the documentation
* for method {@code LogFactory.createFactoryStore} for more details.
* <p>
* 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 a key in the "global"
* {@code LogFactory}'s factories member! If {@code LogFactory.release()}
* is called whenever component is unloaded, the classloaders will be correctly
* garbage collected; this <i>should</i> be done by any container that
* bundles commons-logging by default. However, holding the classloader
* references weakly ensures that the classloader will be garbage collected
* without the container performing this step.
* <p>
* <strong>Limitations:</strong>
* 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.
* <p>
* If the abstract class {@code LogFactory} is
* loaded by the container classloader but a subclass of
* {@code 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
* {@code getClass().getClassLoader()}. This chain of references will prevent
* collection of the child classloader.
* <p>
* 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 {@code LogFactory} implementation is
* loaded by a child classloader (e.g. a web app classloader).
* <p>
* To avoid this scenario, ensure
* that any custom LogFactory subclass is loaded by the same classloader as
* the base {@code LogFactory}. Creating custom LogFactory subclasses is,
* however, rare. The standard LogFactoryImpl class should be sufficient
* for most or all users.
*
* @since 1.1
*/
public final class WeakHashtable extends Hashtable {
/** Serializable version identifier. */
private static final long serialVersionUID = -1546036869799732453L;
/**
* 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 final ReferenceQueue queue = new ReferenceQueue();
/* Counter used to control how often we purge gc'd entries */
private int changeCount;
/**
* Constructs a WeakHashtable with the Hashtable default
* capacity and load factor.
*/
public WeakHashtable() {}
/**
*@see Hashtable
*/
@Override
public boolean containsKey(final Object key) {
// purge should not be required
final Referenced referenced = new Referenced(key);
return super.containsKey(referenced);
}
/**
*@see Hashtable
*/
@Override
public Enumeration elements() {
purge();
return super.elements();
}
/**
*@see Hashtable
*/
@Override
public Set entrySet() {
purge();
final Set referencedEntries = super.entrySet();
final Set unreferencedEntries = new HashSet();
for (final Object referencedEntry : referencedEntries) {
final Map.Entry entry = (Map.Entry) referencedEntry;
final Referenced referencedKey = (Referenced) entry.getKey();
final Object key = referencedKey.getValue();
final Object value = entry.getValue();
if (key != null) {
final Entry dereferencedEntry = new Entry(key, value);
unreferencedEntries.add(dereferencedEntry);
}
}
return unreferencedEntries;
}
/**
*@see Hashtable
*/
@Override
public Object get(final Object key) {
// for performance reasons, no purge
final Referenced referenceKey = new Referenced(key);
return super.get(referenceKey);
}
/**
*@see Hashtable
*/
@Override
public Enumeration keys() {
purge();
final Enumeration enumer = super.keys();
return new Enumeration() {
@Override
public boolean hasMoreElements() {
return enumer.hasMoreElements();
}
@Override
public Object nextElement() {
final Referenced nextReference = (Referenced) enumer.nextElement();
return nextReference.getValue();
}
};
}
/**
*@see Hashtable
*/
@Override
public Set keySet() {
purge();
final Set referencedKeys = super.keySet();
final Set unreferencedKeys = new HashSet();
for (final Object referencedKey : referencedKeys) {
final Referenced referenceKey = (Referenced) referencedKey;
final Object keyValue = referenceKey.getValue();
if (keyValue != null) {
unreferencedKeys.add(keyValue);
}
}
return unreferencedKeys;
}
/**
*@see Hashtable
*/
@Override
public synchronized Object put(final Object key, final Object value) {
// check for nulls, ensuring semantics match superclass
Objects.requireNonNull(key, "key");
Objects.requireNonNull(value, "value");
// 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();
}
final Referenced keyRef = new Referenced(key, queue);
return super.put(keyRef, value);
}
/**
*@see Hashtable
*/
@Override
public void putAll(final Map t) {
if (t != null) {
final Set entrySet = t.entrySet();
for (final Object element : entrySet) {
final Map.Entry entry = (Map.Entry) element;
put(entry.getKey(), entry.getValue());
}
}
}
/**
*@see Hashtable
*/
@Override
public Collection values() {
purge();
return super.values();
}
/**
*@see Hashtable
*/
@Override
public synchronized Object remove(final 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
*/
@Override
public boolean isEmpty() {
purge();
return super.isEmpty();
}
/**
*@see Hashtable
*/
@Override
public int size() {
purge();
return super.size();
}
/**
*@see Hashtable
*/
@Override
public String toString() {
purge();
return super.toString();
}
/**
* @see Hashtable
*/
@Override
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() {
final List toRemove = new ArrayList();
synchronized (queue) {
WeakKey key;
while ((key = (WeakKey) queue.poll()) != null) {
toRemove.add(key.getReferenced());
}
}
// LOGGING-119: do the actual removal of the keys outside the sync block
// to prevent deadlock scenarios as purge() may be called from
// non-synchronized methods too
final int size = toRemove.size();
for (int i = 0; i < size; i++) {
super.remove(toRemove.get(i));
}
}
/**
* Purges one entry whose wrapped key
* has been garbage collected.
*/
private void purgeOne() {
synchronized (queue) {
final 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(final Object key, final Object value) {
this.key = key;
this.value = value;
}
@Override
public boolean equals(final Object o) {
boolean result = false;
if (o instanceof Map.Entry) {
final 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;
}
@Override
public int hashCode() {
return (getKey()==null ? 0 : getKey().hashCode()) ^
(getValue()==null ? 0 : getValue().hashCode());
}
@Override
public Object setValue(final Object value) {
throw new UnsupportedOperationException("Entry.setValue is not supported.");
}
@Override
public Object getValue() {
return value;
}
@Override
public Object getKey() {
return key;
}
}
/** Wrapper giving correct symantics for equals and hash code */
private final static class Referenced {
private final WeakReference reference;
private final int hashCode;
/**
*
* @throws NullPointerException if referant is {@code null}
*/
private Referenced(final 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 {@code null}
*/
private Referenced(final Object key, final 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();
}
@Override
public int hashCode() {
return hashCode;
}
private Object getValue() {
return reference.get();
}
@Override
public boolean equals(final Object o) {
boolean result = false;
if (o instanceof Referenced) {
final Referenced otherKey = (Referenced) o;
final Object thisKeyValue = getValue();
final Object otherKeyValue = otherKey.getValue();
if (thisKeyValue == null) {
result = otherKeyValue == null;
// Since our hash code was calculated from the original
// non-null referant, the above check breaks the
// hash code/equals contract, as two cleared Referenced
// objects could test equal but have different hash codes.
// We can reduce (not eliminate) the chance of this
// happening by comparing hash codes.
result = 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 {@code value} and also makes accessible
* the Referenced object holding it.
*/
private final static class WeakKey extends WeakReference {
private final Referenced referenced;
private WeakKey(final Object key,
final ReferenceQueue queue,
final Referenced referenced) {
super(key, queue);
this.referenced = referenced;
}
private Referenced getReferenced() {
return referenced;
}
}
}