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