blob: 150b9d752bbd947356b0e9241b08a94f7f817e58 [file]
/*
* Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.internal.util;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.HashMap;
import java.util.Objects;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.internal.access.SharedSecrets;
import jdk.internal.misc.CDS;
/**
* This class provides management of {@link Map maps} where it is desirable to
* remove entries automatically when the key is garbage collected. This is
* accomplished by using a backing map where the keys are either a
* {@link WeakReference} or a {@link SoftReference}.
* <p>
* To create a {@link ReferencedKeyMap} the user must provide a {@link Supplier}
* of the backing map and whether {@link WeakReference} or
* {@link SoftReference} is to be used.
*
* {@snippet :
* // Use HashMap and WeakReference
* Map<Long, String> map = ReferencedKeyMap.create(false, HashMap::new);
* map.put(10_000_000L, "a");
* map.put(10_000_001L, "b");
* map.put(10_000_002L, "c");
* map.put(10_000_003L, "d");
* map.put(10_000_004L, "e");
*
* // Use ConcurrentHashMap and SoftReference
* map = ReferencedKeyMap.create(true, ConcurrentHashMap::new);
* map.put(20_000_000L, "v");
* map.put(20_000_001L, "w");
* map.put(20_000_002L, "x");
* map.put(20_000_003L, "y");
* map.put(20_000_004L, "z");
* }
*
* @implNote Care must be given that the backing map does replacement by
* replacing the value in the map entry instead of deleting the old entry and
* adding a new entry, otherwise replaced entries may end up with a strongly
* referenced key. {@link HashMap} and {@link ConcurrentHashMap} are known
* to be safe.
*
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
*
* @since 21
*/
public final class ReferencedKeyMap<K, V> implements Map<K, V> {
/**
* true if {@link SoftReference} keys are to be used,
* {@link WeakReference} otherwise.
*/
private final boolean isSoft;
/**
* Backing {@link Map}.
*/
private final Map<ReferenceKey<K>, V> map;
/**
* {@link ReferenceQueue} for cleaning up entries.
*/
private final ReferenceQueue<K> stale;
/**
* @return a supplier to create a {@code ConcurrentHashMap} appropriate for use in the
* create methods.
* @param <K> the type of keys maintained by the new map
* @param <V> the type of mapped values
*/
public static <K, V> Supplier<Map<ReferenceKey<K>, V>> concurrentHashMapSupplier() {
return new Supplier<>() {
@Override
public Map<ReferenceKey<K>, V> get() {
return new ConcurrentHashMap<>();
}
};
}
/**
* Private constructor.
*
* @param isSoft true if {@link SoftReference} keys are to
* be used, {@link WeakReference} otherwise.
* @param map backing map
* @param stale {@link ReferenceQueue} for cleaning up entries
*/
private ReferencedKeyMap(boolean isSoft, Map<ReferenceKey<K>, V> map, ReferenceQueue<K> stale) {
this.isSoft = isSoft;
this.map = map;
this.stale = stale;
}
/**
* Create a new {@link ReferencedKeyMap} map.
*
* @param isSoft true if {@link SoftReference} keys are to
* be used, {@link WeakReference} otherwise.
* @param supplier {@link Supplier} of the backing map
*
* @return a new map with {@link Reference} keys
*
* @param <K> the type of keys maintained by the new map
* @param <V> the type of mapped values
*/
public static <K, V> ReferencedKeyMap<K, V>
create(boolean isSoft, Supplier<Map<ReferenceKey<K>, V>> supplier) {
return new ReferencedKeyMap<K, V>(isSoft, supplier.get(), new ReferenceQueue<>());
}
/**
* {@return a key suitable for a map entry}
*
* @param key unwrapped key
*/
@SuppressWarnings("unchecked")
private ReferenceKey<K> entryKey(Object key) {
if (isSoft) {
return new SoftReferenceKey<>((K)key, stale);
} else {
return new WeakReferenceKey<>((K)key, stale);
}
}
/**
* {@return a key suitable for lookup}
*
* @param key unwrapped key
*/
@SuppressWarnings("unchecked")
private ReferenceKey<K> lookupKey(Object key) {
return new StrongReferenceKey<>((K)key);
}
@Override
public int size() {
removeStaleReferences();
return map.size();
}
@Override
public boolean isEmpty() {
removeStaleReferences();
return map.isEmpty();
}
@Override
public boolean containsKey(Object key) {
Objects.requireNonNull(key, "key must not be null");
removeStaleReferences();
return map.containsKey(lookupKey(key));
}
@Override
public boolean containsValue(Object value) {
Objects.requireNonNull(value, "value must not be null");
removeStaleReferences();
return map.containsValue(value);
}
@Override
public V get(Object key) {
removeStaleReferences();
return getNoCheckStale(key);
}
// Internal get(key) without removing stale references that would modify the keyset.
// Use when iterating or streaming over the keys to avoid ConcurrentModificationException.
private V getNoCheckStale(Object key) {
Objects.requireNonNull(key, "key must not be null");
return map.get(lookupKey(key));
}
@Override
public V put(K key, V newValue) {
Objects.requireNonNull(key, "key must not be null");
Objects.requireNonNull(newValue, "value must not be null");
removeStaleReferences();
ReferenceKey<K> entryKey = entryKey(key);
// If {@code put} returns non-null then was actually a {@code replace}
// and older key was used. In that case the new key was not used and the
// reference marked stale.
V oldValue = map.put(entryKey, newValue);
if (oldValue != null) {
entryKey.unused();
}
return oldValue;
}
@Override
public V remove(Object key) {
// Rely on gc to clean up old key.
return map.remove(lookupKey(key));
}
@Override
public void putAll(Map<? extends K, ? extends V> m) {
removeStaleReferences();
for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
K key = entry.getKey();
V value = entry.getValue();
put(key, value);
}
}
@Override
public void clear() {
removeStaleReferences();
// Rely on gc to clean up old keys.
map.clear();
}
/**
* Common routine for collecting the current set of keys.
*
* @return {@link Stream} of valid keys (unwrapped)
*/
private Stream<K> filterKeySet() {
return map.keySet()
.stream()
.map(ReferenceKey::get)
.filter(Objects::nonNull);
}
@Override
public Set<K> keySet() {
removeStaleReferences();
return filterKeySet().collect(Collectors.toSet());
}
@Override
public Collection<V> values() {
removeStaleReferences();
return map.values();
}
@Override
public Set<Entry<K, V>> entrySet() {
removeStaleReferences();
return filterKeySet()
.map(k -> new AbstractMap.SimpleEntry<>(k, getNoCheckStale(k)))
.collect(Collectors.toSet());
}
@Override
public V putIfAbsent(K key, V newValue) {
removeStaleReferences();
ReferenceKey<K> entryKey = entryKey(key);
// If {@code putIfAbsent} returns non-null then was actually a
// {@code replace} and older key was used. In that case the new key was
// not used and the reference marked stale.
V oldValue = map.putIfAbsent(entryKey, newValue);
if (oldValue != null) {
entryKey.unused();
}
return oldValue;
}
@Override
public boolean remove(Object key, Object value) {
// Rely on gc to clean up old key.
return map.remove(lookupKey(key), value);
}
@Override
public boolean replace(K key, V oldValue, V newValue) {
removeStaleReferences();
// If replace is successful then the older key will be used and the
// lookup key will suffice.
return map.replace(lookupKey(key), oldValue, newValue);
}
@Override
public V replace(K key, V value) {
removeStaleReferences();
// If replace is successful then the older key will be used and the
// lookup key will suffice.
return map.replace(lookupKey(key), value);
}
@Override
public String toString() {
removeStaleReferences();
return filterKeySet()
.map(k -> k + "=" + getNoCheckStale(k))
.collect(Collectors.joining(", ", "{", "}"));
}
/**
* Removes enqueued weak references from map.
*/
public void removeStaleReferences() {
while (true) {
Object key = stale.poll();
if (key == null) {
break;
}
map.remove(key);
}
}
@SuppressWarnings("unchecked")
public void prepareForAOTCache() {
// We are running the AOT assembly phase. The JVM has a single Java thread, so
// we don't have any concurrent threads that may modify the map while we are
// iterating its keys.
//
// Also, the java.lang.ref.Reference$ReferenceHandler thread is not running,
// so even if the GC has put some of the keys on the pending ReferencePendingList,
// none of the keys would have been added to the stale queue yet.
assert CDS.isSingleThreadVM();
for (ReferenceKey<K> key : map.keySet()) {
Object referent = key.get();
if (referent == null) {
// We don't need this key anymore. Add to stale queue
((Reference)key).enqueue();
} else {
// Make sure the referent cannot be collected. Otherwise, when
// the referent is collected, the GC may push the key onto
// Universe::reference_pending_list() at an unpredictable time,
// making it difficult to correctly serialize the key's
// state into the CDS archive.
//
// See aotReferenceObjSupport.cpp for more info.
CDS.keepAlive(referent);
}
Reference.reachabilityFence(referent);
}
// Remove all keys enqueued above
removeStaleReferences();
}
/**
* Puts an entry where the key and the value are the same. Used for
* interning values in a set.
*
* @implNote Requires a {@link ReferencedKeyMap} whose {@code V} type
* is a {@code ReferenceKey<K>}. Otherwise, a {@link ClassCastException} will
* be thrown.
*
* @param setMap {@link ReferencedKeyMap} where interning takes place
* @param key key to add
*
* @param <T> type of key
*
* @return the old key instance if found otherwise the new key instance
*
* @throws ClassCastException if {@code V} is not {@code EntryKey<T>}
*/
static <T> T intern(ReferencedKeyMap<T, ReferenceKey<T>> setMap, T key) {
T value = existingKey(setMap, key);
if (value != null) {
return value;
}
return internKey(setMap, key);
}
/**
* Puts an entry where the key and the value are the same. Used for
* interning values in a set.
*
* @implNote Requires a {@link ReferencedKeyMap} whose {@code V} type
* is a {@code ReferenceKey<K>}. Otherwise, a {@link ClassCastException} will
* be thrown.
*
* @param setMap {@link ReferencedKeyMap} where interning takes place
* @param key key to add
* @param interner operation to apply to key before adding to map
*
* @param <T> type of key
*
* @return the old key instance if found otherwise the new key instance
*
* @throws ClassCastException if {@code V} is not {@code EntryKey<T>}
*
* @implNote This version of intern should not be called during phase1
* using a lambda. Use an UnaryOperator instance instead.
*/
static <T> T intern(ReferencedKeyMap<T, ReferenceKey<T>> setMap, T key, UnaryOperator<T> interner) {
T value = existingKey(setMap, key);
if (value != null) {
return value;
}
key = interner.apply(key);
return internKey(setMap, key);
}
/**
* Check if the key already exists in the map.
*
* @param setMap {@link ReferencedKeyMap} where interning takes place
* @param key key to test
*
* @param <T> type of key
*
* @return key if found otherwise null
*/
private static <T> T existingKey(ReferencedKeyMap<T, ReferenceKey<T>> setMap, T key) {
setMap.removeStaleReferences();
ReferenceKey<T> entryKey = setMap.get(setMap.lookupKey(key));
return entryKey != null ? entryKey.get() : null;
}
/**
* Attempt to add key to map.
*
* @param setMap {@link ReferencedKeyMap} where interning takes place
* @param key key to add
*
* @param <T> type of key
*
* @return the old key instance if found otherwise the new key instance
*/
private static <T> T internKey(ReferencedKeyMap<T, ReferenceKey<T>> setMap, T key) {
ReferenceKey<T> entryKey = setMap.entryKey(key);
T interned;
do {
setMap.removeStaleReferences();
ReferenceKey<T> existing = setMap.map.putIfAbsent(entryKey, entryKey);
if (existing == null) {
return key;
} else {
// If {@code putIfAbsent} returns non-null then was actually a
// {@code replace} and older key was used. In that case the new
// key was not used and the reference marked stale.
interned = existing.get();
if (interned != null) {
entryKey.unused();
}
}
} while (interned == null);
return interned;
}
/**
* Attempt to add key to map if absent.
*
* @param setMap {@link ReferencedKeyMap} where interning takes place
* @param key key to add
*
* @param <T> type of key
*
* @return true if the key was added
*/
static <T> boolean internAddKey(ReferencedKeyMap<T, ReferenceKey<T>> setMap, T key) {
ReferenceKey<T> entryKey = setMap.entryKey(key);
setMap.removeStaleReferences();
ReferenceKey<T> existing = setMap.map.putIfAbsent(entryKey, entryKey);
if (existing == null) {
return true;
} else {
// If {@code putIfAbsent} returns non-null then was actually a
// {@code replace} and older key was used. In that case the new
// key was not used and the reference marked stale.
entryKey.unused();
return false;
}
}
}