可变大小LRU缓存

4

我正在尝试在Java中实现一个LRU缓存,它应该能够:
动态改变大小。我的计划是将其作为SoftReference订阅到ReferenceQueue中。因此,缓存大小将根据内存消耗而变化。

我计划使用ConcurrentHashMap,其中值将是一个软引用,并定期清除队列以更新地图。
但上述方法的问题是,如何使其成为LRU?

我知道我们无法控制GC,但我们能否管理对值(在缓存中)的引用,以便所有可能的缓存对象都会根据使用情况(即最后访问时间),而不是以某种随机方式成为软引用(在GC下)。


Guava的CacheBuilder应该很适合这里,它混合了从ConcurrentLinkedHashMap派生的LRU特性。 - Ben Manes
3个回答

4
既不弱化,也不软化引用真的不太适合这个问题。WeakReferences在没有更强的引用时会立即被清除,而soft references只有在堆增长到其最大大小并且否则需要抛出OutOufMemoryError时才会被清除。
通常,使用基于时间的方法与定期强引用比使用Reference子类更有效率(对于程序和GC更容易处理,同时不会占用额外的引用内存)。即释放所有未被使用一段时间的对象。您可以使用定期TimerTask检查此功能,您必须使用引用队列来操作它。这个想法是,如果创建该对象需要10ms,并且您将其保留在上次使用后1秒钟内,则平均而言只比永久保存所有对象慢1%左右。但由于它可能使用的内存较少,因此实际上会更快。
编辑:一种实现方法是在内部使用3个桶。放入缓存中的对象始终插入到桶0中。当请求对象时,缓存按顺序在所有3个桶中查找并将其放入桶0中(如果尚未存在)。 TimerTask以固定间隔调用,并仅删除bucket 2并将空桶放置在桶列表的前面,以便新桶0为空,原始桶0变为1,原始桶1现在为bucket 2。这将确保空闲对象至少存活一个或最多两个计时器间隔,并且多次访问的对象检索速度非常快。对于此类数据结构的总维护开销将比基于引用对象和引用队列的任何开销都要小。

我同意这个说法:无论是TimerTask还是Reference线程,它们都必须遍历相同的元素,因此将使用相同的时间复杂度。并且,如果不过多地操作内存,它将更加可预测和可靠。但是大小呢?Neo4J也使用相同的机制(https://github.com/neo4j/community/blob/master/kernel/src/main/java/org/neo4j/kernel/impl/cache/WeakLruCache.java),他们似乎对此感到非常满意和自豪。可能对上述所有不同技术进行分析会使其更好。但总体而言,我仍然认为带有SoftRef的Map应该是最好的选择。 - Jatin
如果您想限制使用我上面提出的实现方式缓存对象的总数,您可以在插入新对象后达到总大小限制时简单地触发桶旋转,并留下一个标志以供计时作业知道这次不需要做任何事情(计时器重置标志)。这将使您的缓存自动从基于时间的模式切换到基于大小限制的模式(反之亦然)。如果桶0变为即将达到总最大大小的一半,您还应该启动旋转,以维护缓存的LRU特性并改善其最坏情况行为。 - x4u

1

如果您想同时拥有多个缓存,那么您的问题才有意义。如果您只有一个缓存,请不要设置大小限制,并始终使用WeakReference。这样,缓存将自动使用所有可用的空闲内存。

然而,准备好与系统管理员进行一些激烈的讨论,因为他们会抱怨您的应用程序存在内存泄漏,并且“随时都会崩溃!”叹气

另一个选择是使用成熟的缓存库,比如EHCache,因为它已经知道关于缓存的所有内容,并花费了数年时间来完善它们。除非您想花费数年时间调试代码以使其适用于Java内存模型的每个角落案例,否则我建议您避免这次重新发明轮子。


我觉得我没有恰当地表达。我不想在弱引用和软引用之间切换。我想要的是一个带有软引用的缓存(以便它可以相应地扩展),它作为一个LRU缓存。 - Jatin
1
啊,那么你将不得不使用 LinkedHashMap 或编写自己的映射。Java 中没有其他地图可以限制大小。例如,Guava 使用他们自己的 CacheBuilder 来构建具有特定限制的缓存,因为 Java 运行时中没有足够的控制。 - Aaron Digulla
那么基本上,带有一些额外努力来决定大小的LinkedHashMap是最好的方法,对吗? - Jatin
1
这是一种最省力但仍然能返回有用结果的方法,再加上LRU。如果你可以不使用LRU,那么WeakHashMap也同样适用。 - Aaron Digulla

0

我会使用LinkedHashMap,因为它支持访问顺序并可用作LRU映射。它可以具有可变的最大大小。

基于使用情况在弱引用和软引用之间切换非常难以正确处理,因为很难确定a)您的缓存独占使用了多少,b)系统正在使用多少,c)Full GC后将使用多少。

您应该注意,弱引用和软引用仅在GC时才会被清除,并且丢弃它们或更改它们不会在运行GC之前释放内存。


问题是:我将不得不根据LinkedHashMap的总内存手动更改大小。至于最后一句话,我不明白为什么那会成为一个问题。 - Jatin
只有当你期望它产生影响时,这才是一个问题。在我看来,我不会增加没有帮助的复杂性。没有办法知道总内存使用量,除非进行完整的GC,然后没有办法强制删除的条目被释放,而不进行另一次GC。 - Peter Lawrey
我觉得我没有恰当地表达。我不想在弱引用和软引用之间切换。我想要的是一个带有软引用的缓存(以便它可以相应地扩展),它作为一个LRU缓存。 - Jatin
2
你可以使用 LinkedHashMap<Key, SoftReference<Value>> 吗? - Peter Lawrey
不,这仍然不能使其成为LRU。但是由于映射的受限大小(因此在特定时间段内是LRU),它确实会使对象持续更长时间。但是,映射的大小需要多大仍然是一个更大的问题。PS:我已经编辑了问题以获得更清晰的表述。 - Jatin
LHM通过将其设置为访问顺序而不是插入顺序来支持LRU。 - Peter Lawrey

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