Java固定内存映射

14

是否存在一种简单、高效的 Map 实现,允许对地图使用的内存进行限制。

我的使用情况是:我想要在创建时动态地分配大多数可用内存,但我不想在未来的任何时候遇到 OutOFMemoryError。基本上,我想将这个 map 用作缓存,但我要避免像 EHCache 这样的重型缓存实现。我的需求很简单(最多一个 LRU 算法)

我应进一步澄清,我的缓存中的对象是 char[] 或类似基元类型,它们不会持有其他对象的引用。

我可以为每个条目设置最大大小上限。


1
从我理解你的问题,你的想法是在启动时分配一定量的内存用于缓存。问题在于,你无法提前知道内存的类型(我们在这里谈论的是缓存对象,对吧?)。与C语言不同,你不能简单地分配一块内存,然后将指针强制转换为该子部分以被视为X对象。因此,对于通用解决方案来说,在Java中固定内存缓存并不实用。此外,你认为这可以保护你免受OutOfMemoryErrors的影响是值得怀疑的。 - Durandal
5个回答

13

您可以使用 LinkedHashMap 来限制 Map 中条目的数量:

removeEldestEntry(Map.Entry<K,V> eldest): Returns true if this map should remove its eldest entry. This method is invoked by put and putAll after inserting a new entry into the map. It provides the implementor with the opportunity to remove the eldest entry each time a new one is added. This is useful if the map represents a cache: it allows the map to reduce memory consumption by deleting stale entries.

Sample use: this override will allow the map to grow up to 100 entries and then delete the eldest entry each time a new entry is added, maintaining a steady state of 100 entries.

private static final int MAX_ENTRIES = 100;

protected boolean removeEldestEntry(Map.Entry eldest) {
    return size() > MAX_ENTRIES;
}

相关问题


1
WeakHashMap在没有对键的强引用时会删除条目。这听起来不适合作为缓存。而且,如果对键保持足够的强引用,仍然可能出现OOM错误。所以我不明白WeakHashMap如何能在这里有所帮助。 - Enno Shioji
@Zwei:我只是把它作为一种可能性留在那里;现在我已经将其删除,仅将LinkedHashMap作为一个选项。 - polygenelubricants
@juber:其中一个相关问题提到了org.apache.commons.collections.map.LRUMap;你可以去看看。我实际上不太熟悉它。 - polygenelubricants

6
对于缓存而言,SoftHashMap 要比 WeakHashMap 更加适合。当你想要与一个对象保持关联,但不阻止其被回收时,通常使用 WeakhashMap。
相反,SoftReference 与内存分配更密切相关。有关差异的详细信息,请参见 No SoftHashMap?WeakHashMap 通常也不适用于缓存,因为它的关联方式对于缓存来说是错误的 - 它使用弱键和硬值。也就是说,当垃圾回收器清除了 时,键和值将从映射中删除。这通常不是缓存的预期行为 - 缓存通常会在 引用被清除时回收键/值。
Commons集合库提供了一个 ReferenceMap,可以插入您希望用于键和值的引用类型。对于内存敏感的缓存,您可能会为键使用强引用,为值使用软引用。
要获取给定数量 N 的引用的 LRU 语义,需要维护最近 N 个从缓存中提取的条目列表 - 在从缓存中检索条目时,将其添加到列表头部(并删除列表尾部)。为了确保它不占用过多内存,可以创建一个软引用,并将其用作从列表末尾驱逐一定百分比条目的触发器。 (然后为下一个触发器创建一个新的软引用。)

3

Java平台解决方案

如果您只是想要一个可以清除键以避免OutOfMemoryErrors的Map,您可能需要查看WeakHashMap。它使用WeakReferences,以便允许垃圾收集器收回映射条目。但是,它不会强制执行任何LRU语义,除了在分代垃圾收集中存在的语义。

还有LinkedHashMap,文档中有这样的说明:

提供了一种特殊的构造函数来创建一个链接哈希映射,其迭代顺序是其条目上次被访问的顺序,从最近访问的到最近访问的(访问顺序)。这种映射非常适合构建LRU缓存。调用put或get方法会导致对相应条目的访问(假设在调用完成后存在)。putAll方法为指定映射中提供的每个映射生成一个条目访问,按照指定映射的条目集迭代器提供的键值映射的顺序。没有其他方法生成条目访问。特别是,集合视图上的操作不会影响支持映射的迭代顺序。

因此,如果您使用此构造函数创建一个Iterator按LRU迭代的映射,则很容易修剪映射。但是,LinkedHashMap完全不同步,因此您需要自己进行并发控制。您可以将其包装在同步包装器中,但这可能会导致吞吐量问题。

自定义解决方案

如果我必须为此用例编写自己的数据结构,我可能会创建某种具有映射、队列和ReadWriteLock以及清理程序线程的数据结构。当映射中的条目太多时,它将处理清理。可能会略微超出所需的最大大小,但在稳态下,您将保持在其下。


我最近也发现了Google Collections MapMaker:http://google-collections.googlecode.com/svn/trunk/javadoc/com/google/common/collect/MapMaker.html。它看起来非常有用。 - jasonmp85

1

WeakHashMap 不一定能够达到你的目的,因为如果你的应用程序持有足够强的键引用,你将会看到 OOME。

或者你可以考虑使用 SoftReference,它会在堆空间不足时将内容置为空。然而,我看到的大部分评论都表明,它只有在堆空间真的很低并且大量 GC 开始启动时才会将引用置为空,并且会对性能造成严重影响(因此我不建议你为此使用它)。

我的建议是使用一个简单的 LRU 映射,例如 http://commons.apache.org/collections/apidocs/org/apache/commons/collections/LRUMap.html


0

谢谢大家的回复!

正如jasonmp85所指出的,LinkedHashMap有一个允许访问顺序的构造函数。当我查看API文档时,我错过了这一点。实现看起来也相当高效(见下文)。再加上每个条目的最大大小限制,这应该可以解决我的问题。

我还将仔细研究SoftReference。只是为了记录,Google Collections似乎对SoftKeys、SoftValues和Maps有很好的API。

这里是Java LikedHashMap类的一段代码片段,展示了它们如何维护LRU行为。

    /**
     * Removes this entry from the linked list.
     */
    private void remove() {
        before.after = after;
        after.before = before;
    }

    /**
     * Inserts this entry before the specified existing entry in the list.
     */
    private void addBefore(Entry<K,V> existingEntry) {
        after  = existingEntry;
        before = existingEntry.before;
        before.after = this;
        after.before = this;
    }

    /**
     * This method is invoked by the superclass whenever the value
     * of a pre-existing entry is read by Map.get or modified by Map.set.
     * If the enclosing Map is access-ordered, it moves the entry
     * to the end of the list; otherwise, it does nothing.
     */
    void recordAccess(HashMap<K,V> m) {
        LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
        if (lm.accessOrder) {
            lm.modCount++;
            remove();
            addBefore(lm.header);
        }

显然是的。噢,好吧。至少我已经养成了在我参与的帖子中给其他人点赞的习惯(假设他们的答案有些帮助而不是极其错误)。 - jasonmp85
抱歉各位,这是我第一次在StackOverflow上发布问题。由于我没有注册,他们不允许我投票。我接受了自己的答案,因为我在那里总结了我发现有用的答案。而且,Joson,你的答案最有帮助,谢谢。 - juber

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接