弱引用是否适合作为缓存?

19
我有一个使用WeakReferences来缓存对象的缓存,以便在出现内存压力时自动从缓存中移除。我的问题是,缓存的对象在存储后很快就被回收了。该缓存在64位应用程序中运行,尽管仍有超过4GB的内存可用,所有缓存的对象都被回收了(通常此时它们存储在G2堆中)。根据进程资源监视器没有手动触发垃圾回收。
我可以采用哪些方法使对象存活时间变长?
7个回答

17

使用WeakReferences作为引用缓存对象的主要手段并不是一个好主意,因为正如Josh所说,你完全被依赖于WeakReference和GC任何未来行为变化的掌控之中。

然而,如果您的缓存需要任何形式的恢复能力,则对于待清除的项目使用WeakReferences是有用的。当一个项目满足驱逐条件时,而不是立即将其驱逐,您可以将其引用更改为弱引用。如果在它被GC之前有任何请求,您可以恢复它的强引用,这个对象就可以再次存在。我发现这对于一些具有难以预测命中率模式和足够频繁的“复活”操作的缓存非常有用。

如果您有可预测的命中率模式,那么我建议放弃WeakReference选项并执行显式清除操作。


2
这就是你的清除策略的工作。每个缓存都需要两个主要的东西...在缓存中注册实例的能力,以及一个基于一组规则确定哪些项目应该被清除的后台进程。有许多现有的清除策略可能会起作用:LRU(最近最少使用...即最旧的出去),LFU(最不经常使用...即10次命中被放弃,而100次命中的项目得到优先处理),等等。你不能永远缓存每个添加的对象...你必须添加一个后台线程来处理清除。 - jrista
但是那个后台线程只会偶尔清理缓存,并且最终会太晚。 - Rauhotz
问题。您需要考虑缓存和缓存的项目数量,因为如果您在64位系统上使用进程的所有可用内存,那么肯定存在严重问题。请确保只缓存真正需要缓存的内容。 - jrista
我真的不确定你如何完成这个任务。你的“缓存”不仅需要知道自己的内存使用情况,还需要知道整个程序以及外部无关进程的内存使用情况。在你的缓存中使用WeakReferences作为主要引用形式是行不通的,因为你的对象会尽快被收集,从而失去了缓存的目的。实际上,你需要钩入系统并监视有关内存请求的消息……这将严重影响你的缓存性能……使其变得无用。 - jrista
未使用的内存是无用的内存,为什么不使用它呢?缓存应该存储一些大表,每个表都有几百MB,并且获取代价高昂,因此如果可用,则要尽可能多地保留它们在内存中,但不要从系统中取走其余的内存。 - Rauhotz
显示剩余6条评论

6
有一种情况下,基于WeakReference的缓存可能很好用:当类中某个项的有用性取决于对它的引用是否存在时。在这种情况下,弱引用缓存可能很有用。例如,如果一个应用程序将反序列化许多大型不可变对象,其中许多对象预计是重复的,并且必须在它们之间执行许多比较。如果XY是对某个不可变类类型的引用,在两个变量都指向同一实例时,测试X.Equals(Y)会非常快,但如果它们指向相等的不同实例,则可能非常慢。如果反序列化对象恰好与已存在引用的另一个对象匹配,则从字典中获取对该后者对象的引用(需要进行一次缓慢的比较)可能加速未来的比较。另一方面,如果它与字典中的某个项匹配,但字典是该项的唯一引用,则使用字典对象而不是仅保留读入的对象的优势很小;可能没有足够的优势来证明比较的成本。对于一个内部缓存,使WeakReferences尽快失效,一旦没有其他引用存在于对象中,这是一件好事。

5
在.net中,WeakReference从垃圾回收的角度来看不被视为引用,因此只有弱引用的对象将在下一次GC运行(适当的代)中被收集。
这使得弱引用完全不适合缓存 - 正如您的经验所示。
您需要一个“真正的”缓存组件,而缓存最重要的是要选择一个驱逐策略(即何时从缓存中删除对象的规则)与您的应用程序使用模式相匹配的缓存组件。

3
不,WeakReference不适合这个任务,因为垃圾回收器的行为随时都可能发生变化,您的缓存不应该依赖于今天的行为。此外,许多不受您控制的因素可能会影响内存压力。
对于.NET,有许多缓存实现方式。您可以在CodePlex上找到大约十几个。我猜您需要添加的是一些东西,用于查看应用程序的当前工作集,并将其用作清除触发器。
关于为什么您的对象经常被收集的另一个注意事项。GC非常积极地清理Gen0对象。如果您的对象的生命周期非常短暂(直到唯一引用它的是弱引用),那么GC正在尽快清理,这是它设计的目的。

3
"CodePlex上有十几个,框架里也有一个。ASP.NET的缓存System.Web.Caching.Cache可以在非ASP.NET应用程序中使用,并且功能强大。微软文档表示不建议在客户端应用程序中使用它(但实际上没有解释原因),但我已经在各种应用程序中成功地使用过它。" - Joe
1
我尝试使用HttpRuntime.Cache,但它并不像我想的那样工作。我不断地向缓存中添加项目,很快就遇到了OutOfMemoryException而不是缓存驱逐项目。 - Rauhotz
HttpRuntime.Cache,但它不像我想的那样工作... OutOfMemoryException。令人惊讶的是,您可以尝试调整缓存配置 - 例如privateBytesLimit和percentagePhysicalMemoryUsedLimit属性。或者OutOfMemoryException可能有其他原因。 - Joe

2
我认为你遇到的问题是垃圾回收器不仅在内存压力下移除弱引用对象,有时候会非常积极地进行收集,因为运行时系统认为某些对象可能已经变得无法访问。建议你使用System.Runtime.Caching.MemoryCache,它可以配置内存限制或自定义项的驱逐策略。

0

答案实际上取决于您正在构建的缓存的使用特征。在我的许多项目中,我已经成功地使用了基于WeakReference的缓存策略来提高性能,其中缓存的对象预计会在短时间内进行多次读取。正如其他人指出的那样,从GC的角度来看,弱引用基本上是垃圾,并且将在下一个GC周期运行时收集。这与内存利用无关。

然而,如果您需要一个可以经受住GC的残酷对待的缓存,您需要使用或模仿System.Runtime.Caching命名空间提供的功能。请记住,当内存使用量超过您的阈值时,您需要一个额外的线程来清理缓存。


0
有点晚了,但这里有一个相关的用例:
我需要缓存两种类型的对象:大型(反序列化)数据文件,需要花费10分钟来加载,每个文件需要15G的内存,以及包含对这些数据文件的内部引用的较小(动态编译)对象(也缓存这些较小的对象,因为它们需要约10秒钟来生成)。这些缓存隐藏在提供对象的工厂内(前者组件不知道后者),并具有不同的逐出策略。
当我的“数据文件”缓存逐出一个对象时,它将其替换为弱引用,因此如果该对象在下次请求时仍然可用,我们可以恢复它(并更新其缓存超时时间)。通过这种方式,我们避免在对象真正失效之前(即没有在其他任何地方使用)丢失(或意外复制)任何对象。请注意,两个缓存都不需要知道彼此的存在,并且没有其他客户端对象需要知道是否存在任何缓存(例如:我们避免需要“保持活动状态”,回调,注册,检索和返回范围等 - 事情变得更简单)。

因此,尽管仅使用WeakReference(而不是缓存)是一个糟糕的想法(因为现代GC通常调整到L2 CPU缓存的大小,并且常规代码将每分钟多次通过此缓存),但它非常有用作为一种方法来隐藏您的缓存,使其不受代码的其余部分的影响。


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