【发布时间】:2016-01-29 06:27:53
【问题描述】:
我已经使用ConcurrentLinkedHashMap 实现了一个 LRU 缓存。在同一张地图中,如果我的地图达到如下所示的特定限制,我将清除事件。
我有一个等于 3.7 GB 的 MAX_SIZE 变量,一旦我的地图达到该限制,我就会从我的地图中清除事件。
下面是我的代码:
import java.util.concurrent.ConcurrentMap;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import com.googlecode.concurrentlinkedhashmap.EvictionListener;
// does this really equal to 3.7 GB? can anyone explain this?
public static final int MAX_SIZE = 20000000; //equates to ~3.7GB with assumption that each event is 200 bytes AVG
public static EvictionListener<String, DataObject> listener = new EvictionListener<String, DataObject>() {
public void onEviction(String key, DataObject value) {
deleteEvents();
}
};
public static final ConcurrentMap<String, DataObject> holder = new ConcurrentLinkedHashMap.Builder<String, DataObject>()
.maximumWeightedCapacity(MAX_SIZE).listener(listener).build();
private static void deleteEvents() {
int capacity = MAX_SIZE - (MAX_SIZE * (20 / 100));
if (holder.size() >= capacity) {
int numEventsToEvict = (MAX_SIZE * 20) / 100;
int counter = 0;
Iterator<String> iter = holder.keySet().iterator();
while (iter.hasNext() && counter < numEventsToEvict) {
String address = iter.next();
holder.remove(address);
System.out.println("Purging Elements: " +address);
counter++;
}
}
}
// this method is called every 30 seconds from a single background thread
// to send data to our queue
public void submit() {
if (holder.isEmpty()) {
return;
}
// some other code here
int sizeOfMsg = 0;
Iterator<String> iter = holder.keySet().iterator();
int allowedBytes = MAX_ALLOWED_SIZE - ALLOWED_BUFFER;
while (iter.hasNext() && sizeOfMsg < allowedBytes) {
String key = iter.next();
DataObject temp = holder.get(key);
// some code here
holder.remove(key);
// some code here to send data to queue
}
}
// this holder map is used in below method to add the events into it.
// below method is being called from some other place.
public void addToHolderRequest(String key, DataObject stream) {
holder.put(key, stream);
}
以下是我为此使用的 maven 依赖项:
<dependency>
<groupId>com.googlecode.concurrentlinkedhashmap</groupId>
<artifactId>concurrentlinkedhashmap-lru</artifactId>
<version>1.4</version>
</dependency>
我不确定这是否是正确的方法?如果事件平均为 200 字节,这 MAX_SIZE 真的等于 3.7 GB 吗?有没有更好的方法来做到这一点?我还有一个后台线程,它每 30 秒调用一次 deleteEvents() 方法,同样的后台线程也调用 submit 方法从 holder 映射中提取数据并发送到队列。
所以想法是,在 addToHolderRequest 方法中将事件添加到 holder 映射,然后从后台每 30 秒调用一次 submit 方法,该方法将通过迭代此映射然后在提交方法完成后将数据发送到我们的队列,从同一后台线程调用deleteEvents() 方法,该方法将清除元素。我在生产中运行此代码,看起来它没有正确清除事件,并且我的持有者地图大小不断增长。我将最小/最大堆内存设置为 6GB。
【问题讨论】:
-
堆上的 Java 对象大小非常棘手,并且容易测量错误。如果平均 200 字节的假设成立,我会怀疑,这取决于它的计算方式。您的听众试图清除地图似乎很奇怪,这似乎是多余的。大量缓存也会受到 GC 影响,例如所有条目都被提升,并且可能需要更昂贵的(例如 STW)收集。但是,推荐哪种替代方案取决于您的应用程序。
-
@BenManes 那我该怎么办?在我的情况下,如果它达到特定的内存限制,我只需要开始从地图中删除元素?或者我可以限制地图中的元素数量,但地图中的元素数量应该小于目前设置为 6GB 的堆内存大小。如果该假设不正确,那么我该如何计算呢?想法是,我不希望我的
holder映射继续增长并最终耗尽内存,所以只需开始从 LRU 缓存中删除最旧的元素。顺便说一句,我在 Java 7 上。 -
保留多少的唯一衡量标准是在完整的 GC 之后。否则你不知道如果运行 Full GC 会保留多少。显然,您必须包括每个条目使用的所有内存,其中包括 Map.Entry、键的大小及其对象标头以及值的大小及其对象标头。这很容易达到大约 64 个字节或更多。
-
在 ConcurrentHastMap 上使用迭代器是一种非常糟糕的驱逐条目的方法。迭代器将从相同的段开始,因此您将从第一个段中删除,而最后一个段可能永远不会驱逐条目。你最好随机删除条目。理想情况下,您将使用条目队列来删除。例如,一种更简单的策略是拥有多个新旧 ConcurrentHashMap。仅添加到新地图,当地图半满时删除旧地图并添加新的“新”地图。您可以使用 4 张地图,因此您只需要在清理时丢弃 1/4 的条目。
-
@PeterLawrey CLHM 在以后的版本中使用 CHMv8 反向端口,因此段问题现在是 RB 树。该地图有一个 LRU 迭代器,但构建成本更高。
标签: java caching memory-management guava lru