在Java中强制释放大缓存对象

13

我使用一个包含数百万条目的哈希映射表来缓存算法所需的值,键是两个对象的组合形成的长整型。由于它不断增长(因为映射表中的键发生变化,老键已不再需要),因此在执行过程中强制清除其中的所有数据并重新开始将是不错的选择,在Java中是否有一种有效的方法实现这一点?

我的意思是释放关联的内存(约1-1.5GB的哈希映射表),并从空的哈希映射表重新启动。

7个回答

17
你可以调用HashMap.clear()。这将删除所有数据。请注意,这只会丢弃所有条目,但保留用于存储条目的内部数组的大小(而不是缩小为初始容量)。如果您还需要消除它,最简单的方法是丢弃整个HashMap并用新实例替换它。当然,这仅适用于您控制谁有指向该地图的指针。
至于回收内存,您必须让垃圾收集器完成其工作。
你的值也是长整型吗?在这种情况下,您可能希望查看比通用HashMap更(内存)高效的实现,例如在GNU Trove library中找到的TLongLongHashMap。那应该节省很多内存。

13

听起来你需要使用WeakHashMap

WeakHashMap是基于哈希表的Map实现,它具有弱引用键。当一个键不再被正常使用时,WeakHashMap中相应的条目将自动删除。更准确地说,在给定键的映射存在时,该键不会阻止垃圾回收器废弃该键,即使经过最终化并回收后也是如此。当一个键已被废弃时,其条目在实际上从地图中删除,因此这个类的行为与其他Map实现略有不同。

不过,我不确定这对于以Long作为键值是否有效。此外,你可能会对以下内容感兴趣:

WeakHashMap不是缓存!了解WeakReference和SoftReference


5
我不确定这是否适用于长键。毕竟,相同的Long实例可能在完全无关的代码中使用,更重要的是,键Long也可以作为单独的实例或基本类型存储,以便引用不会保持活动状态(即使仍然处于活动状态)。 - Thilo

3

6
我认为明确调用gc()永远不是一个好建议,但我可能错了。 - polygenelubricants
2
@zneak:我认为显式调用垃圾回收是一种不好的做法。你应该让JVM自己处理。 - Thilo
2
@Thilo:显然,运行垃圾收集器会使整个系统停止运行,因此这是一个不好的想法,因为JVM通常比您更了解何时需要更多内存。除此之外,在特殊情况下,如果目标是回收1.5GB的内存,我会尝试一下。特别是因为它将以非常确定的方式被调用(当哈希映射被清除时)。 - zneak
3
显然,运行垃圾收集器会使整个程序停止。但这并非总是如此。现在大部分垃圾回收都可以并发进行。而且无法保证gc()将执行何种类型的收集(如果有的话)。我认为在当前的Hotspot JVM上,您需要连续调用gc()三到四次才能强制进行完整的收集。再次问一下,为什么您想立即回收内存?当您需要内存时,JVM会为您回收它。 - Thilo
2
@Thilo:JVM 了解自己,但不了解其他进程。即使 JVM 占用了 1.5GB 的内存,其中大部分是垃圾,看起来也可能完全正常,但这可能对我的系统不可接受。话虽如此,说垃圾回收不再停止世界听起来并不像是反对直接调用垃圾回收的论点。我愿意相信这是一种不好的做法,但你能解释一下为什么吗?我读到的大多数文章都说正是因为这种倾向才是一种不好的做法。 - zneak
显示剩余7条评论

3
对于一个内存感知缓存,您可能想使用Apache Commons collections,特别是它们的org.apache.commons.collections.map.ReferenceMap类。Java的特殊操作是软引用。Java提供了WeakHashMap用于弱引用,但弱引用不适合用于缓存。Java没有提供SoftHashMap,但是来自Apache Commons的ReferenceMap可以成为可行的替代品。
软引用的内存感知有些粗糙和不灵活。您可以尝试一些Java选项来进行配置,特别是-XX:SoftRefLRUPolicyMSPerMB值,该值表示(以毫秒为单位)保留软引用值在内存中的时间(当它们不再直接可达时)。例如,使用以下内容:
java -XX:SoftRefLRUPolicyMSPerMB=2500

如果使用软引用无法满足您的需求,那么您将需要实现自己的缓存策略,并手动清空映射。这是您最初的问题。对于清空操作,您可以使用clear()方法,或者简单地创建一个新的HashMap。两种方法的区别应该很小,您甚至可能难以简单地测量到这种区别。

在“完整缓存”和“空缓存”之间交替也可能被认为有点粗糙,因此您可以维护多个映射。例如,您可以维护十个映射。当您查找缓存值时,您会在所有映射中查找,但当您拥有一个值时,您只将其放入第一个映射中。当您想要清空时,您旋转映射:第一个映射变为第二个映射,第二个映射变为第三个映射,依此类推,直到第十个映射被丢弃。然后创建一个新的第一个映射。这看起来像是这样的:

如果JVM使用WeakHashMap,则它将尝试保留缓存值比原本多2.5秒。

import java.util.*;

public class Cache {

    private static final int MAX_SIZE = 500000;

    private Map[] backend;
    private int size = 0;

    public Cache(int n)
    {
        backend = new Map[n];
        for (int i = 0; i < n; i ++)
            backend[i] = new HashMap();
    }

    public int size()
    {
        return size;
    }

    public Object get(Object key)
    {
        for (Map m : backend) {
            if (m.containsKey(key))
                return m.get(key);
        }
        return null;
    }

    public Object put(Object key, Object value)
    {
        if (backend[0].containsKey(key))
            return backend[0].put(key, value);
        int n = backend.length;
        for (int i = 1; i < n; i ++) {
            Map m = backend[i];
            if (m.containsKey(key)) {
                Object old = m.remove(key);
                backend[0].put(key, value);
                return old;
            }
        }
        backend[0].put(key, value);
        size ++;
        while (size > MAX_SIZE) {
            size -= backend[n - 1].size();
            System.arraycopy(backend, 0, backend, 1, n - 1);
            backend[0] = new HashMap();
        }
        return null;
    }
}

上述代码完全未经测试,应该使用泛型进行改进。然而,它说明了主要的思想:在读取(get())时测试所有映射,所有新值都进入第一个映射,总大小保持不变,并且当大小超过给定限制时,旋转映射。请注意,当为已知的键放置新值时,需要做一些特殊处理。此外,在此版本中,在找到缓存的值时没有执行任何特殊操作,但是我们可以“恢复”访问的缓存值:在 get()时,当找到一个值但不在第一个映射中时,可以将其移动到第一个映射中。因此,经常访问的值将永远保留在缓存中。

差异应该很小,你甚至可能会有困难来简单地测量这个差异。这个差异是保存映射条目的数组(在clear()方法中保留)。对于数百万个条目,该数组将达到几MB(我认为每个条目大约需要四个字节的Object[])。 - Thilo
2
数组大小比所指向的对象的总大小要小得多。此外,如果这是一个您经常清空的缓存,那么根据定义,您将再次填充它,而大数组 在某个时候回来。使用 clear() 意味着您保留数组,但避免以后重新分配它,并且在内部数组增长时避免一定程度的重新哈希。我认为从长远来看,这不会产生可衡量的差异。 - Thomas Pornin

0
如果您有一点闲置内存,可以实现一个超时缓存,其中哈希映射中的每个值都包含您的长整型值和毫秒级别的插入时间戳 - 然后有一个后台线程每隔X秒迭代一次这些值,并删除任何超过X秒/毫秒的旧数据。

仅供参考 :)


0

不要使用HashMap或其他地图实现作为高速缓存,您可以尝试使用专门用于缓存的框架。 Java中一个众所周知的缓存框架是Ehcache

缓存框架通常允许您根据时间(例如存活时间、空闲时间)或使用情况(例如最不经常使用、最近最少使用)配置过期策略,一些甚至允许您指定最大内存使用量。


0

1
WeakHashMap 使用对象标识符进行操作。不确定它是否适用于 Long 类型的键。 - Thilo
是的,我在帖子中也提到了这个疑问。 - polygenelubricants

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