You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@uima.apache.org by re...@apache.org on 2020/10/17 20:57:37 UTC
[uima-uimaj] 01/01: [UIMA-6276] Potential memory leak in
FSClassRegistry
This is an automated email from the ASF dual-hosted git repository.
rec pushed a commit to branch bugfix/UIMA-6276-Potential-memory-leak-in-FSClassRegistry
in repository https://gitbox.apache.org/repos/asf/uima-uimaj.git
commit 400712151ab8c02ff5f0cfbc684033d10f474758
Author: Richard Eckart de Castilho <re...@apache.org>
AuthorDate: Sat Oct 17 22:58:08 2020 +0200
[UIMA-6276] Potential memory leak in FSClassRegistry
- Adapted a WeakIdentityMap from Apache Lucene to store the per-classloader JCas class info
---
NOTICE | 8 +-
.../org/apache/uima/cas/impl/FSClassRegistry.java | 24 +-
.../apache/uima/internal/util/WeakIdentityMap.java | 318 +++++++++++++++++++++
.../uima/internal/util/WeakIdentityMapTest.java | 261 +++++++++++++++++
4 files changed, 606 insertions(+), 5 deletions(-)
diff --git a/NOTICE b/NOTICE
index e0fb1e8..07dd55b 100644
--- a/NOTICE
+++ b/NOTICE
@@ -23,4 +23,10 @@ commercially by FasterXML.com.
A list of contributors may be found from CREDITS file, which is included
in some artifacts (usually source distributions); but is always available
-from the source code management (SCM) system project uses.
\ No newline at end of file
+from the source code management (SCM) system project uses.
+
+## org.apache.uima.internal.util.WeakIdentityMap
+
+The class org.apache.uima.internal.util.WeakIdentityMap<K, V> was copied from
+the Apache Lucene project which in turn derived it from the Apache CXF project
+and is Apache License 2.0.
\ No newline at end of file
diff --git a/uimaj-core/src/main/java/org/apache/uima/cas/impl/FSClassRegistry.java b/uimaj-core/src/main/java/org/apache/uima/cas/impl/FSClassRegistry.java
index d4cc147..614613c 100644
--- a/uimaj-core/src/main/java/org/apache/uima/cas/impl/FSClassRegistry.java
+++ b/uimaj-core/src/main/java/org/apache/uima/cas/impl/FSClassRegistry.java
@@ -48,6 +48,7 @@ import org.apache.uima.cas.CASException;
import org.apache.uima.cas.CASRuntimeException;
import org.apache.uima.internal.util.Misc;
import org.apache.uima.internal.util.UIMAClassLoader;
+import org.apache.uima.internal.util.WeakIdentityMap;
import org.apache.uima.jcas.cas.TOP;
import org.apache.uima.util.Level;
import org.apache.uima.util.Logger;
@@ -165,8 +166,11 @@ public abstract class FSClassRegistry { // abstract to prevent instantiating; th
*
* Cache of FsGenerator[]s kept in TypeSystemImpl instance, since it depends on type codes.
* Current FsGenerator[] kept in CASImpl shared view data, switched as needed for PEARs.
+ * <p>
+ * <b>NOTE:</b> Access this map in a thread-safe way only via {@link #get_className_to_jcci} which
+ * synchronizes on the map object.
*/
- private static final Map<ClassLoader, Map<String, JCasClassInfo>> cl_to_type2JCas = Collections.synchronizedMap(new IdentityHashMap<>()); // identity: key is classloader
+ private static final WeakIdentityMap<ClassLoader, Map<String, JCasClassInfo>> cl_to_type2JCas = WeakIdentityMap.newHashMap(); // identity: key is classloader
// private static final Map<ClassLoader, Map<String, JCasClassInfo>> cl_4pears_to_type2JCas = Collections.synchronizedMap(new IdentityHashMap<>()); // identity: key is classloader
@@ -287,7 +291,7 @@ public abstract class FSClassRegistry { // abstract to prevent instantiating; th
// Class loader used for builtins is the UIMA framework's class loader
ArrayList<MutableCallSite> callSites_toSync = new ArrayList<>();
ClassLoader cl = tsi.getClass().getClassLoader();
- loadBuiltins(tsi.topType, cl, cl_to_type2JCas.computeIfAbsent(cl, x -> new HashMap<>()), callSites_toSync);
+ loadBuiltins(tsi.topType, cl, get_className_to_jcci(cl, false), callSites_toSync);
MutableCallSite[] sync = callSites_toSync.toArray(new MutableCallSite[callSites_toSync.size()]);
MutableCallSite.syncAll(sync);
@@ -1462,8 +1466,20 @@ public abstract class FSClassRegistry { // abstract to prevent instantiating; th
}
static Map<String, JCasClassInfo> get_className_to_jcci(ClassLoader cl, boolean is_pear) {
- final Map<ClassLoader, Map<String, JCasClassInfo>> cl2t2j = cl_to_type2JCas; /*is_pear ? cl_4pears_to_type2JCas :*/
- return cl2t2j.computeIfAbsent(cl, x -> new HashMap<>());
+ synchronized (cl_to_type2JCas) {
+ // This was used before switching from the normal synchronized map to the weak map
+ // and is part of a whole bunch of commented out code with special handling for the PEAR
+ // case. Not sure if this commented out code was kept for debugging or for historical
+ // reasons - so let's keep this for the moment as a comment here.
+ // REC - 2020-10-17
+ // final Map<ClassLoader, Map<String, JCasClassInfo>> cl2t2j = cl_to_type2JCas; /*is_pear ? cl_4pears_to_type2JCas :*/
+ Map<String, JCasClassInfo> cl_to_jcci = cl_to_type2JCas.get(cl);
+ if (cl_to_jcci == null) {
+ cl_to_jcci = new HashMap<>();
+ cl_to_type2JCas.put(cl, cl_to_jcci);
+ }
+ return cl_to_jcci;
+ }
}
static Lookup getLookup(ClassLoader cl) {
diff --git a/uimaj-core/src/main/java/org/apache/uima/internal/util/WeakIdentityMap.java b/uimaj-core/src/main/java/org/apache/uima/internal/util/WeakIdentityMap.java
new file mode 100644
index 0000000..85e9a45
--- /dev/null
+++ b/uimaj-core/src/main/java/org/apache/uima/internal/util/WeakIdentityMap.java
@@ -0,0 +1,318 @@
+/*
+ * 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.uima.internal.util;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Implements a combination of {@link java.util.WeakHashMap} and
+ * {@link java.util.IdentityHashMap}. Useful for caches that need to key off of
+ * a {@code ==} comparison instead of a {@code .equals}.
+ *
+ * <p>
+ * This class is not a general-purpose {@link java.util.Map} implementation! It
+ * intentionally violates Map's general contract, which mandates the use of the
+ * equals method when comparing objects. This class is designed for use only in
+ * the rare cases wherein reference-equality semantics are required.
+ *
+ * <p>
+ * This implementation was derived from
+ * <a href="http://lucene.apache.org/">Apache Lucene</a> Lucene in turn forked
+ * this class from from <a href="http://cxf.apache.org/">Apache CXF</a> but
+ * modified to <b>not</b> implement the {@link java.util.Map} interface and
+ * without any set views on it, as those are error-prone and inefficient, if not
+ * implemented carefully. The map only contains {@link Iterator} implementations
+ * on the values and not-GCed keys. Lucene's implementation also supports
+ * {@code null} keys, but those are never weak!
+ *
+ * <p>
+ * <a id="reapInfo"></a>The map supports two modes of operation:
+ * <ul>
+ * <li>{@code reapOnRead = true}: This behaves identical to a
+ * {@link java.util.WeakHashMap} where it also cleans up the reference queue on
+ * every read operation ({@link #get(Object)}, {@link #containsKey(Object)},
+ * {@link #size()}, {@link #valueIterator()}), freeing map entries of already
+ * GCed keys.</li>
+ * <li>{@code reapOnRead = false}: This mode does not call {@link #reap()} on
+ * every read operation. In this case, the reference queue is only cleaned up on
+ * write operations (like {@link #put(Object, Object)}). This is ideal for maps
+ * with few entries where the keys are unlikely be garbage collected, but there
+ * are lots of {@link #get(Object)} operations. The code can still call
+ * {@link #reap()} to manually clean up the queue without doing a write
+ * operation.</li>
+ * </ul>
+ * @param <K> the key type.
+ * @param <V> the value type.
+ */
+public final class WeakIdentityMap<K, V> {
+ private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
+ private final Map<IdentityWeakReference, V> backingStore;
+ private final boolean reapOnRead;
+
+ /**
+ * Creates a new {@code WeakIdentityMap} based on a non-synchronized
+ * {@link HashMap}. The map <a href="#reapInfo">cleans up the reference queue on
+ * every read operation</a>.
+ *
+ * @return the new map.
+ */
+ public static <K, V> WeakIdentityMap<K, V> newHashMap() {
+ return newHashMap(true);
+ }
+
+ /**
+ * Creates a new {@code WeakIdentityMap} based on a non-synchronized
+ * {@link HashMap}.
+ *
+ * @param reapOnRead
+ * controls if the map <a href="#reapInfo">cleans up the reference
+ * queue on every read operation</a>.
+ * @return the new map.
+ */
+ public static <K, V> WeakIdentityMap<K, V> newHashMap(boolean reapOnRead) {
+ return new WeakIdentityMap<>(new HashMap<IdentityWeakReference, V>(), reapOnRead);
+ }
+
+ /**
+ * Creates a new {@code WeakIdentityMap} based on a {@link ConcurrentHashMap}.
+ * The map <a href="#reapInfo">cleans up the reference queue on every read
+ * operation</a>.
+ *
+ * @return the new map.
+ */
+ public static <K, V> WeakIdentityMap<K, V> newConcurrentHashMap() {
+ return newConcurrentHashMap(true);
+ }
+
+ /**
+ * Creates a new {@code WeakIdentityMap} based on a {@link ConcurrentHashMap}.
+ *
+ * @param reapOnRead
+ * controls if the map <a href="#reapInfo">cleans up the reference
+ * queue on every read operation</a>.
+ * @return the new map.
+ */
+ public static <K, V> WeakIdentityMap<K, V> newConcurrentHashMap(boolean reapOnRead) {
+ return new WeakIdentityMap<>(new ConcurrentHashMap<IdentityWeakReference, V>(), reapOnRead);
+ }
+
+ /** Private only constructor, to create use the static factory methods. */
+ private WeakIdentityMap(Map<IdentityWeakReference, V> backingStore, boolean reapOnRead) {
+ this.backingStore = backingStore;
+ this.reapOnRead = reapOnRead;
+ }
+
+ /** Removes all of the mappings from this map. */
+ public void clear() {
+ backingStore.clear();
+ reap();
+ }
+
+ /**
+ * @param key
+ * the key to check for a mapping.
+ * @return {@code true} if this map contains a mapping for the specified key.
+ */
+ public boolean containsKey(Object key) {
+ if (reapOnRead)
+ reap();
+ return backingStore.containsKey(new IdentityWeakReference(key, null));
+ }
+
+ /**
+ * @param key
+ * the key for which to return the associated value.
+ * @return the value to which the specified key is mapped.
+ */
+ public V get(Object key) {
+ if (reapOnRead)
+ reap();
+ return backingStore.get(new IdentityWeakReference(key, null));
+ }
+
+ /**
+ * Associates the specified value with the specified key in this map. If the map
+ * previously contained a mapping for this key, the old value is replaced.
+ * @return the previous associated value.
+ * @param key the associate the value with.
+ * @param value the value to associate with the key.
+ */
+ public V put(K key, V value) {
+ reap();
+ return backingStore.put(new IdentityWeakReference(key, queue), value);
+ }
+
+ /**
+ * @return {@code true} if this map contains no key-value mappings.
+ */
+ public boolean isEmpty() {
+ return size() == 0;
+ }
+
+ /**
+ * Removes the mapping for a key from this weak hash map if it is present.
+ * Returns the value to which this map previously associated the key, or
+ * {@code null} if the map contained no mapping for the key. A return value of
+ * {@code null} does not necessarily indicate that the map contained.
+ * @param key the key to remove.
+ * @return the previous mapping.
+ */
+ public V remove(Object key) {
+ reap();
+ return backingStore.remove(new IdentityWeakReference(key, null));
+ }
+
+ /**
+ * @return the number of key-value mappings in this map. This result is a
+ * snapshot, and may not reflect unprocessed entries that will be removed before
+ * next attempted access because they are no longer referenced.
+ */
+ public int size() {
+ if (backingStore.isEmpty())
+ return 0;
+ if (reapOnRead)
+ reap();
+ return backingStore.size();
+ }
+
+ /**
+ * @return an iterator over all weak keys of this map. Keys already garbage
+ * collected will not be returned. This Iterator does not support removals.
+ */
+ public Iterator<K> keyIterator() {
+ reap();
+ final Iterator<IdentityWeakReference> iterator = backingStore.keySet().iterator();
+ // IMPORTANT: Don't use oal.util.FilterIterator here:
+ // We need *strong* reference to current key after setNext()!!!
+ return new Iterator<K>() {
+ // holds strong reference to next element in backing iterator:
+ private Object next = null;
+ // the backing iterator was already consumed:
+ private boolean nextIsSet = false;
+
+ @Override
+ public boolean hasNext() {
+ return nextIsSet || setNext();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public K next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ assert nextIsSet;
+ try {
+ return (K) next;
+ } finally {
+ // release strong reference and invalidate current value:
+ nextIsSet = false;
+ next = null;
+ }
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+
+ private boolean setNext() {
+ assert !nextIsSet;
+ while (iterator.hasNext()) {
+ next = iterator.next().get();
+ if (next == null) {
+ // the key was already GCed, we can remove it from backing map:
+ iterator.remove();
+ } else {
+ // unfold "null" special value:
+ if (next == NULL) {
+ next = null;
+ }
+ return nextIsSet = true;
+ }
+ }
+ return false;
+ }
+ };
+ }
+
+ /**
+ * @return an iterator over all values of this map. This iterator may return
+ * values whose key is already garbage collected while iterator is consumed,
+ * especially if {@code reapOnRead} is {@code false}.
+ */
+ public Iterator<V> valueIterator() {
+ if (reapOnRead)
+ reap();
+ return backingStore.values().iterator();
+ }
+
+ /**
+ * This method manually cleans up the reference queue to remove all garbage
+ * collected key/value pairs from the map. Calling this method is not needed if
+ * {@code reapOnRead = true}. Otherwise it might be a good idea to call this
+ * method when there is spare time (e.g. from a background thread).
+ *
+ * @see <a href="#reapInfo">Information about the <code>reapOnRead</code>
+ * setting</a>
+ */
+ public void reap() {
+ Reference<?> zombie;
+ while ((zombie = queue.poll()) != null) {
+ backingStore.remove(zombie);
+ }
+ }
+
+ // we keep a hard reference to our NULL key, so map supports null keys that
+ // never get GCed:
+ static final Object NULL = new Object();
+
+ private static final class IdentityWeakReference extends WeakReference<Object> {
+ private final int hash;
+
+ IdentityWeakReference(Object obj, ReferenceQueue<Object> queue) {
+ super(obj == null ? NULL : obj, queue);
+ hash = System.identityHashCode(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o instanceof IdentityWeakReference) {
+ final IdentityWeakReference ref = (IdentityWeakReference) o;
+ if (this.get() == ref.get()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/internal/util/WeakIdentityMapTest.java b/uimaj-core/src/test/java/org/apache/uima/internal/util/WeakIdentityMapTest.java
new file mode 100644
index 0000000..793d94c
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/internal/util/WeakIdentityMapTest.java
@@ -0,0 +1,261 @@
+/*
+ * 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.uima.internal.util;
+
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Random;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReferenceArray;
+
+import org.junit.Test;
+
+/**
+ * <p>This implementation was adapted from <a href="http://lucene.apache.org/">Apache Lucene</a>.
+ */
+public class WeakIdentityMapTest {
+
+ private Random random = new Random();
+
+ @Test
+ public void testSimpleHashMap() {
+ final WeakIdentityMap<String,String> map =
+ WeakIdentityMap.newHashMap(random.nextBoolean());
+ // we keep strong references to the keys,
+ // so WeakIdentityMap will not forget about them:
+ String key1 = new String("foo");
+ String key2 = new String("foo");
+ String key3 = new String("foo");
+
+ assertNotSame(key1, key2);
+ assertEquals(key1, key2);
+ assertNotSame(key1, key3);
+ assertEquals(key1, key3);
+ assertNotSame(key2, key3);
+ assertEquals(key2, key3);
+
+ // try null key & check its iterator also return null:
+ map.put(null, "null");
+ {
+ Iterator<String> it = map.keyIterator();
+ assertTrue(it.hasNext());
+ assertNull(it.next());
+ assertFalse(it.hasNext());
+ assertFalse(it.hasNext());
+ }
+ // 2 more keys:
+ map.put(key1, "bar1");
+ map.put(key2, "bar2");
+
+ assertEquals(3, map.size());
+
+ assertEquals("bar1", map.get(key1));
+ assertEquals("bar2", map.get(key2));
+ assertEquals(null, map.get(key3));
+ assertEquals("null", map.get(null));
+
+ assertTrue(map.containsKey(key1));
+ assertTrue(map.containsKey(key2));
+ assertFalse(map.containsKey(key3));
+ assertTrue(map.containsKey(null));
+
+ // repeat and check that we have no double entries
+ map.put(key1, "bar1");
+ map.put(key2, "bar2");
+ map.put(null, "null");
+
+ assertEquals(3, map.size());
+
+ assertEquals("bar1", map.get(key1));
+ assertEquals("bar2", map.get(key2));
+ assertEquals(null, map.get(key3));
+ assertEquals("null", map.get(null));
+
+ assertTrue(map.containsKey(key1));
+ assertTrue(map.containsKey(key2));
+ assertFalse(map.containsKey(key3));
+ assertTrue(map.containsKey(null));
+
+ map.remove(null);
+ assertEquals(2, map.size());
+ map.remove(key1);
+ assertEquals(1, map.size());
+ map.put(key1, "bar1");
+ map.put(key2, "bar2");
+ map.put(key3, "bar3");
+ assertEquals(3, map.size());
+
+ int c = 0, keysAssigned = 0;
+ for (Iterator<String> it = map.keyIterator(); it.hasNext();) {
+ assertTrue(it.hasNext()); // try again, should return same result!
+ final String k = it.next();
+ assertTrue(k == key1 || k == key2 | k == key3);
+ keysAssigned += (k == key1) ? 1 : ((k == key2) ? 2 : 4);
+ c++;
+ }
+ assertEquals(3, c);
+ assertEquals("all keys must have been seen", 1+2+4, keysAssigned);
+
+ c = 0;
+ for (Iterator<String> it = map.valueIterator(); it.hasNext();) {
+ final String v = it.next();
+ assertTrue(v.startsWith("bar"));
+ c++;
+ }
+ assertEquals(3, c);
+
+ // clear strong refs
+ key1 = key2 = key3 = null;
+
+ // check that GC does not cause problems in reap() method, wait 1 second and let GC work:
+ int size = map.size();
+ for (int i = 0; size > 0 && i < 10; i++) try {
+ System.runFinalization();
+ System.gc();
+ int newSize = map.size();
+ assertTrue("previousSize("+size+")>=newSize("+newSize+")", size >= newSize);
+ size = newSize;
+ Thread.sleep(100L);
+ c = 0;
+ for (Iterator<String> it = map.keyIterator(); it.hasNext();) {
+ assertNotNull(it.next());
+ c++;
+ }
+ newSize = map.size();
+ assertTrue("previousSize("+size+")>=iteratorSize("+c+")", size >= c);
+ assertTrue("iteratorSize("+c+")>=newSize("+newSize+")", c >= newSize);
+ size = newSize;
+ } catch (InterruptedException ie) {}
+
+ map.clear();
+ assertEquals(0, map.size());
+ assertTrue(map.isEmpty());
+
+ Iterator<String> it = map.keyIterator();
+ assertFalse(it.hasNext());
+ assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> {
+ it.next();
+ });
+
+ key1 = new String("foo");
+ key2 = new String("foo");
+ map.put(key1, "bar1");
+ map.put(key2, "bar2");
+ assertEquals(2, map.size());
+
+ map.clear();
+ assertEquals(0, map.size());
+ assertTrue(map.isEmpty());
+ }
+
+ @Test
+ public void testConcurrentHashMap() throws Exception {
+ // don't make threadCount and keyCount random, otherwise easily OOMs or fails otherwise:
+ final int threadCount = 4;
+ final int keyCount = 1024;
+ final ExecutorService exec = Executors.newFixedThreadPool(threadCount);
+ final WeakIdentityMap<Object,Integer> map =
+ WeakIdentityMap.newConcurrentHashMap(random.nextBoolean());
+ // we keep strong references to the keys,
+ // so WeakIdentityMap will not forget about them:
+ final AtomicReferenceArray<Object> keys = new AtomicReferenceArray<>(keyCount);
+ for (int j = 0; j < keyCount; j++) {
+ keys.set(j, new Object());
+ }
+
+ try {
+ for (int t = 0; t < threadCount; t++) {
+ final Random rnd = new Random(random.nextLong());
+ exec.execute(new Runnable() {
+ @Override
+ public void run() {
+ final int count = random.nextInt(5000) + 10000;
+ for (int i = 0; i < count; i++) {
+ final int j = rnd.nextInt(keyCount);
+ switch (rnd.nextInt(5)) {
+ case 0:
+ map.put(keys.get(j), Integer.valueOf(j));
+ break;
+ case 1:
+ final Integer v = map.get(keys.get(j));
+ if (v != null) {
+ assertEquals(j, v.intValue());
+ }
+ break;
+ case 2:
+ map.remove(keys.get(j));
+ break;
+ case 3:
+ // renew key, the old one will be GCed at some time:
+ keys.set(j, new Object());
+ break;
+ case 4:
+ // check iterator still working
+ for (Iterator<Object> it = map.keyIterator(); it.hasNext();) {
+ assertNotNull(it.next());
+ }
+ break;
+ default:
+ fail("Should not get here.");
+ }
+ }
+ }
+ });
+ }
+ } finally {
+ exec.shutdown();
+ while (!exec.awaitTermination(1000L, TimeUnit.MILLISECONDS));
+ }
+
+ // clear strong refs
+ for (int j = 0; j < keyCount; j++) {
+ keys.set(j, null);
+ }
+
+ // check that GC does not cause problems in reap() method:
+ int size = map.size();
+ for (int i = 0; size > 0 && i < 10; i++) try {
+ System.runFinalization();
+ System.gc();
+ int newSize = map.size();
+ assertTrue("previousSize("+size+")>=newSize("+newSize+")", size >= newSize);
+ size = newSize;
+ Thread.sleep(100L);
+ int c = 0;
+ for (Iterator<Object> it = map.keyIterator(); it.hasNext();) {
+ assertNotNull(it.next());
+ c++;
+ }
+ newSize = map.size();
+ assertTrue("previousSize("+size+")>=iteratorSize("+c+")", size >= c);
+ assertTrue("iteratorSize("+c+")>=newSize("+newSize+")", c >= newSize);
+ size = newSize;
+ } catch (InterruptedException ie) {}
+ }
+
+}
\ No newline at end of file