Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #10085, LFUCache Memory leak problem #10086

Merged
merged 1 commit into from
Jun 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
214 changes: 35 additions & 179 deletions dubbo-common/src/main/java/org/apache/dubbo/common/utils/LFUCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,26 @@
*/
package org.apache.dubbo.common.utils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;

public class LFUCache<K, V> {

private Map<K, CacheNode<K, V>> map;
private Map<Long, CacheDeque<K, V>> freqTable;
private CacheDeque<K, V>[] freqTable;

private final int capacity;
private int evictionCount;
private int curSize = 0;
private long removeFreqEntryTimeout;

private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final ReentrantLock lock = new ReentrantLock();
private static final int DEFAULT_INITIAL_CAPACITY = 1000;

private static final float DEFAULT_EVICTION_FACTOR = 0.75f;

private static final long DEFAULT_REMOVE_FREQ_TABLE_TIME_OUT = 1800000L;

public LFUCache() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_EVICTION_FACTOR, DEFAULT_REMOVE_FREQ_TABLE_TIME_OUT);
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_EVICTION_FACTOR);
}

/**
Expand All @@ -69,36 +60,14 @@ public LFUCache(final int maxCapacity, final float evictionFactor) {
this.capacity = maxCapacity;
this.evictionCount = (int) (capacity * evictionFactor);
this.map = new HashMap<>();
this.freqTable = new TreeMap<>(Long::compareTo);
freqTable.put(1L, new CacheDeque<>());
}

/**
* Constructs and initializes cache with specified capacity and eviction
* factor. Unacceptable parameter values followed with
* {@link IllegalArgumentException}.
*
* @param maxCapacity cache max capacity
* @param evictionFactor cache proceedEviction factor
* @param removeFreqEntryTimeout cache queue remove timeout
*/
@SuppressWarnings("unchecked")
public LFUCache(final int maxCapacity, final float evictionFactor, final long removeFreqEntryTimeout) {
if (maxCapacity <= 0) {
throw new IllegalArgumentException("Illegal initial capacity: " +
maxCapacity);
this.freqTable = new CacheDeque[capacity + 1];
for (int i = 0; i <= capacity; i++) {
freqTable[i] = new CacheDeque<>();
}
boolean factorInRange = evictionFactor <= 1 && evictionFactor > 0;
if (!factorInRange || Float.isNaN(evictionFactor)) {
throw new IllegalArgumentException("Illegal eviction factor value:"
+ evictionFactor);
for (int i = 0; i < capacity; i++) {
freqTable[i].nextDeque = freqTable[i + 1];
}
this.capacity = maxCapacity;
this.evictionCount = (int) (capacity * evictionFactor);
this.removeFreqEntryTimeout = removeFreqEntryTimeout;
this.map = new HashMap<>();
this.freqTable = new TreeMap<>(Long::compareTo);
freqTable.put(1L, new CacheDeque<>());
freqTable[capacity].nextDeque = freqTable[capacity];
}

public int getCapacity() {
Expand All @@ -107,31 +76,31 @@ public int getCapacity() {

public V put(final K key, final V value) {
CacheNode<K, V> node;
lock.writeLock().lock();
lock.lock();
try {
node = map.get(key);
if (node != null) {
CacheNode.withdrawNode(node);
node.value = value;
moveToNextFreqQueue(node.incrFreq(), node);
freqTable[0].addLastNode(node);
map.put(key, node);
} else {
if (curSize + 1 > capacity) {
proceedEviction();
}
node = freqTable.get(1L).addLast(key, value);
node = freqTable[0].addLast(key, value);
map.put(key, node);
curSize++;
if (curSize > capacity) {
proceedEviction();
}
}
} finally {
lock.writeLock().unlock();
lock.unlock();
}
return node.value;
}

public V remove(final K key) {
CacheNode<K, V> node = null;
lock.writeLock().lock();
lock.lock();
try {
if (map.containsKey(key)) {
node = map.remove(key);
Expand All @@ -141,137 +110,51 @@ public V remove(final K key) {
curSize--;
}
} finally {
lock.writeLock().unlock();
lock.unlock();
}
return (node != null) ? node.value : null;
}

public V get(final K key) {
CacheNode<K, V> node = null;
lock.writeLock().lock();
lock.lock();
try {
if (map.containsKey(key)) {
node = map.get(key);
CacheNode.withdrawNode(node);
moveToNextFreqQueue(node.incrFreq(), node);
node.owner.nextDeque.addLastNode(node);
}
} finally {
lock.writeLock().unlock();
lock.unlock();
}
return (node != null) ? node.value : null;
}

/**
* Returns size of the freq table
*
* @return size
*/
public int getFreqTableSize(){
return freqTable.size();
}

/**
* Returns freq of the element
*
* @return freq
*/
public Long getFreq(final K key) {
CacheNode<K, V> node = null;
lock.readLock().lock();
try {
if (map.containsKey(key)) {
node = map.get(key);
return node.getFreq();
}
} finally {
lock.readLock().unlock();
}
return null;
}

/**
* Returns node list of this frequency
*
* @return node list
*/
private List<CacheNode<K,V>> getFreqList(final Long freq){
if(freq == null){
return null;
}
lock.writeLock().lock();
try {
if (freqTable.containsKey(freq)) {
if(freqTable.get(freq).nodeMap.size() > 0){
return new ArrayList<>(freqTable.get(freq).nodeMap.values());
}
}
} finally {
lock.writeLock().unlock();
}
return null;
}

/**
* Returns node list's size of this frequency
*
* @return node list's size
*/
public int getFreqListSize(final Long freq){
if(freq == null){
return 0;
}
lock.writeLock().lock();
try {
if (freqTable.containsKey(freq)) {
return freqTable.get(freq).size.get();
}
} finally {
lock.writeLock().unlock();
}
return 0;
}

/**
* Evicts less frequently used elements corresponding to eviction factor,
* specified at instantiation step.
*
* @return number of evicted elements
*/
private int proceedEviction() {
int targetSize = capacity - evictionCount - 1;
int targetSize = capacity - evictionCount;
int evictedElements = 0;
Set<Long> freqKeys = freqTable.keySet();
boolean evictionEnd = false;
for (Long freq : freqKeys) {
CacheDeque<K, V> q = freqTable.get(freq);

FREQ_TABLE_ITER_LOOP:
for (int i = 0; i <= capacity; i++) {
CacheNode<K, V> node;
if(!evictionEnd) {
while (!q.isEmpty()) {
node = q.pollFirst();
remove(node.key);
evictedElements++;
if (targetSize >= curSize) {
evictionEnd = true;
break;
}
while (!freqTable[i].isEmpty()) {
node = freqTable[i].pollFirst();
remove(node.key);
if (targetSize >= curSize) {
break FREQ_TABLE_ITER_LOOP;
}
}
// If the queue is empty for a long time, delete the queue
if (removeFreqEntryTimeout > 0 && freq > 1 && q.isEmpty() && (System.currentTimeMillis() - q.getLastReqTime()) >= removeFreqEntryTimeout) {
freqTable.remove(freq);
evictedElements++;
}
}
return evictedElements;
}

/**
* Move the node to the next cache queue
*/
private void moveToNextFreqQueue(long newFreq, CacheNode<K, V> node){
freqTable.putIfAbsent(newFreq, new CacheDeque<>());
freqTable.get(newFreq).addLastNode(node);
}

/**
* Returns cache current size.
*
Expand All @@ -287,7 +170,6 @@ static class CacheNode<K, V> {
CacheNode<K, V> next;
K key;
V value;
volatile AtomicLong freq = new AtomicLong(1);
CacheDeque<K, V> owner;

CacheNode() {
Expand All @@ -298,14 +180,6 @@ static class CacheNode<K, V> {
this.value = value;
}

long incrFreq(){
return freq.incrementAndGet();
}

long getFreq(){
return freq.get();
}

/**
* This method takes specified node and reattaches it neighbors nodes
* links to each other, so specified node will no longer tied with them.
Expand All @@ -322,8 +196,6 @@ static <K, V> CacheNode<K, V> withdrawNode(
node.prev.next = node.next;
if (node.next != null) {
node.next.prev = node.prev;
node.owner.nodeMap.remove(node.key);
node.owner.size.decrementAndGet();
}
}
return node;
Expand All @@ -344,9 +216,8 @@ static class CacheDeque<K, V> {

CacheNode<K, V> last;
CacheNode<K, V> first;
Map<K, CacheNode<K, V>> nodeMap;
long lastReqTime;
volatile AtomicInteger size = new AtomicInteger(0);
CacheDeque<K, V> nextDeque;

/**
* Constructs list and initializes last and first pointers.
*/
Expand All @@ -355,7 +226,6 @@ static class CacheDeque<K, V> {
first = new CacheNode<>();
last.next = first;
first.prev = last;
nodeMap = new HashMap<>();
}

/**
Expand All @@ -373,8 +243,6 @@ CacheNode<K, V> addLast(final K key, final V value) {
node.prev = last;
node.next.prev = node;
last.next = node;
this.setLastReqTime(System.currentTimeMillis());
this.size.incrementAndGet();
return node;
}

Expand All @@ -384,9 +252,6 @@ CacheNode<K, V> addLastNode(final CacheNode<K, V> node) {
node.prev = last;
node.next.prev = node;
last.next = node;
this.setLastReqTime(System.currentTimeMillis());
this.nodeMap.put(node.key, node);
this.size.incrementAndGet();
return node;
}

Expand All @@ -403,8 +268,6 @@ CacheNode<K, V> pollFirst() {
first.prev.next = first;
node.prev = null;
node.next = null;
this.nodeMap.remove(node.key);
this.size.decrementAndGet();
}
return node;
}
Expand All @@ -418,13 +281,6 @@ boolean isEmpty() {
return last.next == first;
}

public CacheDeque<K, V> setLastReqTime(long lastReqTime) {
this.lastReqTime = lastReqTime;
return this;
}

public long getLastReqTime() {
return lastReqTime;
}
}

}