4242 * countermeasure to the non-predictability of the cache's eviction as explained above: at least for
4343 * the X most recently used elements, the retention is guaranteed.
4444 *
45+ * <p>Entries that have been garbage collected remain in the cache using the memory consumption of a
46+ * key and an empty reference. Not a lot, but the cache would grow indefinitely if such empty
47+ * entries were not removed. Therefore, such entries are removed from the cache on every "touch" the
48+ * cache, i.e. read, write or "size" operations. This is a very, very fast operation and can hardly
49+ * show in measurements. However, garbage collection often occurs in "bursts", meaning that it may
50+ * happen there is suddenly a large bunch of entries that need to be expunged. Theoretically, this
51+ * can block the current cache operation for a short period of time, say 2ms. In practice, this has
52+ * not been observed to be a problem. Even so, the behavior can be tuned via the {@code
53+ * maxExpungePerReadOperation} parameter in the constructor. The default value of 100 means that at
54+ * most GC'ed 100 entries will be expunged per read operation. If there are more entries to expunge,
55+ * they will be expunged on the next read operation. By contrast, write operations and "size"
56+ * operations will always expunge all GC'ed entries in one go.
57+ *
4558 * <p>The cache is fully thread-safe.
4659 *
4760 * <p>The cache only allows non-null values.
4861 *
49- * <p>The cache must be {@link #close() closed} when it is no longer needed. If not, the background
50- * thread will continue to run.
51- *
5262 * @implNote At any point in time, the map which is the "backend" for this cache, will contain both
5363 * real usable values and values which has been garbage collected and are therefore empty. The
5464 * latter, empty values, are effectively values that are "marked for deletion" from the map.
5767 * @param <K> key type
5868 * @param <V> value type
5969 */
60- public class MemorySensitiveCache <K , V > implements AutoCloseable {
70+ public class MemorySensitiveCache <K , V > {
6171
6272 public static final int USE_DEFAULT_MAP_INITIAL_CAPACITY = -1 ;
6373 private final ConcurrentMap <K , SoftValue <K , V >> map ;
6474 private final ReferenceQueue <V > referenceQueue = new ReferenceQueue <>();
6575 private final SimpleQueue <K , V > hardCache ;
6676 private final Class <K > keyClass ;
67- private volatile boolean closed = false ;
6877
6978 /**
7079 * Creates a cache.
7180 *
72- * <p>The cache has a background reaper thread which is responsible for removing garbage collected
73- * values from the cache's underlying map. Such values take up very little space in the map;
74- * nevertheless, they need to be removed to stop the map from growing indefinitely. Because they
75- * take up so little space, the reaper does not have to fire that often.
76- *
7781 * @param keyClass class for the key
7882 * @param hardRefSize number of elements which are "hard referenced" meaning they are never GC'ed.
7983 * Value must be ≥ 0. Setting this value too high may result in an eventual {@link
@@ -86,10 +90,17 @@ public class MemorySensitiveCache<K, V> implements AutoCloseable {
8690 * @param initialCapacity the initial capacity of the map used by this cache. If {@link
8791 * #USE_DEFAULT_MAP_INITIAL_CAPACITY} then the JDK's default value will be used, typically 16.
8892 * See {@link ConcurrentHashMap#ConcurrentHashMap(int)} for more information.
89- * @param reaperInitialDelay the initial delay before the reaper fires.
90- * @param reaperInterval the periodic interval at which the reaper fires.
93+ * @param maxExpungePerReadOperation Maximum number of GC'ed entries to expunge from the cache on
94+ * a single "touch" of the cache. This applies to read-operations only (i.e. get, contains,
95+ * etc). Write operations and "size" operations will always expunge all GC'ed entries in one
96+ * go. The value must be ≥ 0. Set to {@link Integer#MAX_VALUE} to effectively disable the
97+ * feature. Default value is 100.
9198 */
92- public MemorySensitiveCache (final Class <K > keyClass , int hardRefSize , int initialCapacity ) {
99+ public MemorySensitiveCache (
100+ final Class <K > keyClass ,
101+ int hardRefSize ,
102+ int initialCapacity ,
103+ int maxExpungePerReadOperation ) {
93104 Objects .requireNonNull (keyClass , "keyClass must not be null" );
94105
95106 this .keyClass = keyClass ;
@@ -107,20 +118,10 @@ public MemorySensitiveCache(final Class<K> keyClass, int hardRefSize, int initia
107118 * @param keyClass class for the key
108119 * @param hardRefSize number of elements which are "hard referenced" meaning they are never GC'ed.
109120 * (value must be ≥ 0)
110- * @see #MemorySensitiveCache(Class, int, int)
121+ * @see #MemorySensitiveCache(Class, int, int, int )
111122 */
112123 public MemorySensitiveCache (final Class <K > keyClass , int hardRefSize ) {
113- this (keyClass , hardRefSize , USE_DEFAULT_MAP_INITIAL_CAPACITY );
114- }
115-
116- private static ScheduledExecutorService defaultScheduledExecutorService () {
117- return Executors .newScheduledThreadPool (
118- 1 , // pool size
119- r -> {
120- Thread t = new Thread (r );
121- t .setDaemon (true );
122- return t ;
123- });
124+ this (keyClass , hardRefSize , USE_DEFAULT_MAP_INITIAL_CAPACITY , 100 );
124125 }
125126
126127 /**
@@ -130,13 +131,20 @@ private static ScheduledExecutorService defaultScheduledExecutorService() {
130131 * <p>The method is invoked for every access to the cache. According to the JDK's Javadoc (see
131132 * https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/ref/package-summary.html#notification-heading)
132133 * this is very fast and should not be a performance issue. It is also how WeakHashMap does it.
134+ * This has also been verified by oww measurements.
133135 */
134- private void expungeStaleEntries () {
136+ private void expungeStaleEntries (boolean allowBlocking ) {
135137 // Remove objects which have been GC'ed
138+ // long startTime = System.nanoTime();
136139 int count = 0 ;
137140
138- // Avoid handling bursts of garbage collected objects in one go.
139- while (count < 100 && (!closed )) {
141+ // Avoid handling bursts of garbage collected objects in one go for read operations
142+ // (allowBlocking = false)
143+ // in contrast to write operations (allowBlocking = true) where we want to get rid of all stale
144+ // entries as quickly as possible.
145+ // It is debatable if the split should be 100 or some other number. It is also debatable if
146+ // there should be a split at all since indeed it seems to be a very fast operation.
147+ while ((allowBlocking ) || (count < 100 )) {
140148 Reference <? extends V > ref = referenceQueue .poll ();
141149 if (ref == null ) {
142150 break ;
@@ -153,12 +161,15 @@ private void expungeStaleEntries() {
153161 K key = keyClass .cast (oKey );
154162 // The value may theoretically have entered into the cache (again)
155163 // in the meantime. So remove _conditionally_.
156- if (!closed ) {
157- removeIfEmpty (key , false );
158- }
164+ removeIfEmpty (key , false );
159165 }
160166 }
161167 }
168+ // if (count > 0) {
169+ // long endTime = System.nanoTime();
170+ // System.out.println("Took " + (endTime - startTime)/1000000 + " ms to expunge " + count +
171+ // " entries");
172+ // }
162173 }
163174
164175 /**
@@ -169,10 +180,7 @@ private void expungeStaleEntries() {
169180 */
170181 public V get (K key ) {
171182 Objects .requireNonNull (key , "key cannot be null" );
172- if (closed ) {
173- throw new IllegalStateException ("Cache has been closed" );
174- }
175- expungeStaleEntries ();
183+ expungeStaleEntries (false );
176184 SoftValue <K , V > softValue =
177185 map .compute (
178186 key ,
@@ -249,10 +257,7 @@ private SoftValue<K, V> compute00(
249257 }
250258
251259 private void compute0 (K key , Function <K , V > mappingFunction ) {
252- if (closed ) {
253- throw new IllegalStateException ("Cache has been closed" );
254- }
255- expungeStaleEntries ();
260+ expungeStaleEntries (true );
256261 map .compute (key , (k , v ) -> compute00 (key , mappingFunction , true ));
257262 }
258263
@@ -267,9 +272,6 @@ private void compute0(K key, Function<K, V> mappingFunction) {
267272 public V putIfAbsent (K key , V value ) {
268273 Objects .requireNonNull (key , "key cannot be null" );
269274 Objects .requireNonNull (value , "value cannot be null" );
270- if (closed ) {
271- throw new IllegalStateException ("Cache has been closed" );
272- }
273275 V existing = get (key );
274276 if (existing == null ) {
275277 put (key , value );
@@ -299,10 +301,7 @@ public V putIfAbsent(K key, V value) {
299301 public V computeIfAbsent (K key , Function <K , V > mappingFunction ) {
300302 Objects .requireNonNull (key , "key cannot be null" );
301303 Objects .requireNonNull (mappingFunction , "mappingFunction cannot be null" );
302- if (closed ) {
303- throw new IllegalStateException ("Cache has been closed" );
304- }
305- expungeStaleEntries ();
304+ expungeStaleEntries (false ); // is is read or write? We don't know, so let's say read
306305
307306 // Atomically check if the key is already associated with a value.
308307 // If so, check if the value is empty. In this case, treat as if the value
@@ -389,9 +388,6 @@ public final V removeIfEmpty(K key) {
389388
390389 private V removeIfEmpty (K key , boolean performHousekeeping ) {
391390 Objects .requireNonNull (key , "key cannot be null" );
392- if (closed ) {
393- throw new IllegalStateException ("Cache has been closed" );
394- }
395391 SoftValue <K , V > softValue =
396392 map .computeIfPresent (
397393 key ,
@@ -420,38 +416,18 @@ public synchronized void clear() {
420416 hardCache .clear ();
421417 }
422418
423- /**
424- * Closes the cache and relinquishes any resources it holds. This includes background thread.
425- *
426- * <p>The cache must <i>NOT</i> be used after it has been closed. Doing so will result in an
427- * {@link IllegalStateException}.
428- *
429- * @implNote The background reaper thread is a daemon thread, so it will not prevent the JVM from
430- * shutting down. However, it is still good practice to close the cache when it is no longer
431- * needed. Otherwise, the background reaper thread will continue to be scheduled periodically
432- * and although it will be a no-op when it runs, it is still a waste of resources.
433- */
434- @ Override
435- public void close () {
436- if (closed ) {
437- return ;
438- }
439- this .closed = true ;
440- clear ();
441- }
442-
443419 /**
444420 * Gets the size of the cache.
445421 *
446422 * <p>The method has constant time characteristics, <i>O(1)</i>.
447423 */
448424 public int size () {
449- expungeStaleEntries ();
425+ expungeStaleEntries (true );
450426 return map .size ();
451427 }
452428
453429 public boolean isEmpty () {
454- expungeStaleEntries ();
430+ expungeStaleEntries (true );
455431 return map .isEmpty ();
456432 }
457433
@@ -466,10 +442,7 @@ public boolean isEmpty() {
466442 * @return iterator
467443 */
468444 public Iterator <KeyValuePair <K , V >> iterator () {
469- if (closed ) {
470- throw new IllegalStateException ("Cache has been closed" );
471- }
472- expungeStaleEntries ();
445+ expungeStaleEntries (true );
473446
474447 final Iterator <SoftValue <K , V >> baseIterator = map .values ().iterator ();
475448
0 commit comments