#1287 - Fixed potential memory leak when creating many links.
We now use a copy of Spring Framework's MimeTypeUtil's ConcurrentLruCache implementation to strictly only LRU cache 1024 instances of UriTemplate and fall back to creation if that size is exceeded. This should solve the memory leak for now but should still be revisited across the Spring ecosystem.
This commit is contained in:
@@ -25,6 +25,11 @@ import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -32,7 +37,6 @@ import java.util.stream.Collectors;
|
||||
import org.springframework.hateoas.TemplateVariable.VariableType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
|
||||
@@ -55,7 +59,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
|
||||
private static final Pattern VARIABLE_REGEX = Pattern.compile("\\{([\\?\\&#/]?)([\\w\\,*]+)\\}");
|
||||
private static final long serialVersionUID = -1007874653930162262L;
|
||||
|
||||
private static final Map<String, UriTemplate> CACHE = new ConcurrentReferenceHashMap<>();
|
||||
private static final ConcurrentLruCache<String, UriTemplate> CACHE = new ConcurrentLruCache<>(1024, UriTemplate::new);
|
||||
|
||||
private final TemplateVariables variables;
|
||||
private String baseUri;
|
||||
@@ -134,7 +138,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
|
||||
|
||||
Assert.hasText(template, "Template must not be null or empty!");
|
||||
|
||||
return CACHE.computeIfAbsent(template, UriTemplate::new);
|
||||
return CACHE.get(template);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,7 +151,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
|
||||
|
||||
Assert.hasText(template, "Template must not be null or empty!");
|
||||
|
||||
return CACHE.computeIfAbsent(template, UriTemplate::new).with(variables);
|
||||
return CACHE.get(template).with(variables);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -426,4 +430,84 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
|
||||
|
||||
this.factory = createFactory(baseUri);
|
||||
}
|
||||
|
||||
// Copy of ConcurrentLruCache of Spring Framework's MimeTypeUtils
|
||||
|
||||
/**
|
||||
* Simple Least Recently Used cache, bounded by the maximum size given to the class constructor.
|
||||
* <p>
|
||||
* This implementation is backed by a {@code ConcurrentHashMap} for storing the cached values and a
|
||||
* {@code ConcurrentLinkedQueue} for ordering the keys and choosing the least recently used key when the cache is at
|
||||
* full capacity.
|
||||
*
|
||||
* @param <K> the type of the key used for caching
|
||||
* @param <V> the type of the cached values
|
||||
*/
|
||||
private static class ConcurrentLruCache<K, V> {
|
||||
|
||||
private final int maxSize;
|
||||
|
||||
private final ConcurrentLinkedDeque<K> queue = new ConcurrentLinkedDeque<>();
|
||||
|
||||
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
|
||||
|
||||
private final ReadWriteLock lock;
|
||||
|
||||
private final Function<K, V> generator;
|
||||
|
||||
private volatile int size = 0;
|
||||
|
||||
public ConcurrentLruCache(int maxSize, Function<K, V> generator) {
|
||||
Assert.isTrue(maxSize > 0, "LRU max size should be positive");
|
||||
Assert.notNull(generator, "Generator function should not be null");
|
||||
this.maxSize = maxSize;
|
||||
this.generator = generator;
|
||||
this.lock = new ReentrantReadWriteLock();
|
||||
}
|
||||
|
||||
public V get(K key) {
|
||||
V cached = this.cache.get(key);
|
||||
if (cached != null) {
|
||||
if (this.size < this.maxSize) {
|
||||
return cached;
|
||||
}
|
||||
this.lock.readLock().lock();
|
||||
try {
|
||||
if (this.queue.removeLastOccurrence(key)) {
|
||||
this.queue.offer(key);
|
||||
}
|
||||
return cached;
|
||||
} finally {
|
||||
this.lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
this.lock.writeLock().lock();
|
||||
try {
|
||||
// Retrying in case of concurrent reads on the same key
|
||||
cached = this.cache.get(key);
|
||||
if (cached != null) {
|
||||
if (this.queue.removeLastOccurrence(key)) {
|
||||
this.queue.offer(key);
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
// Generate value first, to prevent size inconsistency
|
||||
V value = this.generator.apply(key);
|
||||
int cacheSize = this.size;
|
||||
if (cacheSize == this.maxSize) {
|
||||
K leastUsed = this.queue.poll();
|
||||
if (leastUsed != null) {
|
||||
this.cache.remove(leastUsed);
|
||||
cacheSize--;
|
||||
}
|
||||
}
|
||||
this.queue.offer(key);
|
||||
this.cache.put(key, value);
|
||||
this.size = cacheSize + 1;
|
||||
return value;
|
||||
} finally {
|
||||
this.lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user