【问题标题】:How to remove elements from map if it reaches a memory size limit?如果达到内存大小限制,如何从地图中删除元素?
【发布时间】: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


【解决方案1】:
  1. 您可以使用“最常用于实现内存敏感缓存”(SoftReference) 的软引用来代替估计 JVM 中对象的大小并使用强引用来引用它们。例如CacheBuilder.softValues() 来自google/guava: Google Core Libraries for Java 6+:“为了响应内存需求,软引用对象将以全局最近最少使用的方式进行垃圾收集。”不过,我建议您先熟悉 CachesExplained · google/guava Wiki(特别是 Reference-based Eviction 部分)。
  2. 作为使用软引用的一种调整,您还可以尝试here 中描述的“受害者缓存方法”,它使用“驱逐到 [a] 软缓存的普通缓存,并在可能的情况下恢复未命中的条目”。
  3. 如果您确定要实际估​​计对象的大小,请查看Ehcache 及其Sizing Storage Tiers。它有 Built-In Sizing Computation and Enforcement 用于内存有限的缓存。

【讨论】:

  • 感谢您推荐 Guava Cache。我浏览了 wiki,但有点困惑如何在我的示例中使用 Guava Cache?你能举个例子,在我的情况下我将如何使用它?
  • 软引用总体上是负面的,因为它们在过度使用时会导致完全 GC。这会导致性能下降。
猜你喜欢
  • 1970-01-01
  • 2012-07-13
  • 1970-01-01
  • 2018-12-13
  • 1970-01-01
  • 2015-01-29
  • 2014-05-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多