.NET MemoryCache:它如何强制执行内存限制?

8

.NET的MemoryCache是C#对象的缓存。有些对象可能具有复杂的结构,而其他对象可能具有不安全的引用。对于实现PhysicalMemoryLimit,C#是否使用某些魔法,还是只计算每个对象的浅层大小?

我怀疑后者是正确的。但是,如果我将同一个对象多次放入缓存中(例如跟踪丢失的项),那么该大小会被计算一次,还是对包含该实例的每个条目都会计算一次?


我非常确定缓存不会尝试计算缓存中对象的大小。这样做是徒劳的,因为(a)任意对象的大小在插入缓存后可能会发生变化,(b)可能存在其他引用指向被缓存对象字段引用的对象,因此从缓存中删除并不能释放所有内存。 - Joe
1
无论它做什么,都是通过这个方法实现的: http://referencesource.microsoft.com/#mscorlib/system/gc.cs,6da6dff768f373f5 - fernacolo
修改了问题的内容。 - John Meyer
4个回答

7

.NET MemoryCache类似于ASP.NET缓存类。如果我们查看ASP.NET缓存,我们会看到一个名为CacheItemRemovedCallback的函数。当从缓存中删除一个项目时,就会触发此函数。

此函数使用回调函数给出CacheItemRemovedReason。如果我们查看原因,我们会发现,系统可以通过释放内存来从缓存中删除一个项目。因此,虽然PhysicalMemoryLimit给出了单个线程可以使用的物理内存百分比,但我认为如果达到限制,它们会让系统清除缓存。

如果您使用Add函数将Cache项目放到缓存中,它将作为新的CacheItem实例添加,因此会被多次计算。如果您使用AddOrGetExisting函数,它将检查该项是否已经在缓存中。如果是,则使用该实例而不是新实例。因此,它只会被计算一次。
希望这可以帮助您朝着正确的方向前进。

我不确定这是否是正确的情况。该测量是在整个MemoryCache上执行的:http://referencesource.microsoft.com/#System.Runtime.Caching/System/Caching/CacheMemoryMonitor.cs,54。因此,它可以很好地仅计算重复对象一次。 - fernacolo
您还没有考虑过使用AddOrGetExisting与不同的键但相同值实例的情况。这个值计算一次还是多次? - fernacolo

2
阅读文档后,发现缓存没有试图计算其缓存的对象的大小。这是有道理的,因为对于任意类型,从进程内部进行计算是不可能做到的(您可以针对固定大小的结构体或固定大小结构体的数组进行计算,但仅限于此)。通过一些搜索,您可以确认这一点。但是,它确实知道计算机上有多少可用RAM;您可以从new Microsoft.VisualBasic.Devices.ComputerInfo().AvailablePhysicalMemory获取此信息。因此,缓存应该会做两件事:
  1. 跟踪每个对象上次使用的时间。
  2. 以一定的间隔轮询内存统计信息。
然后,在每次轮询时,要么可用内存量在可接受范围内,要么不在。如果在可接受的范围内,则不执行任何操作。如果不在,则开始删除项目,并首先删除最近访问时间最久远的项目。它会继续删除项目,直到内存回到可接受范围内。
如果您考虑一下,这基本上就是您可以使用缓存提供的信息所能做的全部内容。
这种策略是可以的,但如果有其他对象持有对缓存项的引用,则显然会出现问题,因为从缓存中删除该项不会释放它以进行垃圾回收。这就是回调的目的,以执行清理操作,以确保没有更多对该对象的引用。

1
如果您查看源代码,您会发现它实际上获取了缓存中对象的大小。它使用一个特殊指针,在垃圾回收器在其收集周期期间估计对象图大小。MemoryCache的文档严重缺乏。例如,这种内存计数方法意味着缓存项应该相当自包含,没有对“父”对象等的引用,否则对象图大小将完全无用。 - Mike Marynowski

1
这是源代码。如果您查看Add方法的实现referencesource.microsoft.com,那么对于您的第二个问题的答案是显而易见的,因为它调用了AddOrGetExisting。
我不知道大小,但我猜您的假设是正确的,没有任何魔法。如果您感兴趣,您可以深入检查源代码。

那个源代码并没有解答任何问题。用户可以将同一个对象以不同的键放入缓存中。MemoryCache并不检查值是否已经添加,它只检查键是否已经存在,并且为了缓存目的,通常值占用更多的空间。此外,该代码实际上并没有计算大小或做出任何决定来清除项目以遵守内存限制。 - fernacolo
这不是重点(至少我理解的是这样)。为了缓存目的,对象本身并不重要。只有键才重要。这是合理的,因为系统中可能会使用相同的对象多次,并且它们需要被单独存储,因为键描述了对象在此机制中的目的。 - Sergey.quixoticaxis.Ivanov
1
还有调用非托管代码以获取当前内存使用情况的方法: if (UnsafeNativeMethods.GlobalMemoryStatusEx(ref memoryStatusEx) != 0) { s_totalPhysical = memoryStatusEx.ullTotalPhys; s_totalVirtual = memoryStatusEx.ullTotalVirtual; } 这在计算压力时被使用: if (memory >= 0x100000000) { _pressureHigh = 99; } else if (memory >= 0x80000000) { _pressureHigh = 98; } 也许我没有理解你的意思,但是源代码中有很多答案。 - Sergey.quixoticaxis.Ivanov
1
所以它很可能直接从操作系统获取缓存大小。 - Sergey.quixoticaxis.Ivanov
1
哦,没错,这里有些魔法......在代码中查找SRef。它是一种特殊的“大小引用”,使GC报告它所指向的对象图的大小。 - Mike Marynowski

-2

您无法轻松设置MemoryCache类的PhysicalMemoryLimit或CacheMemoryLimit成员,因为它们没有实现setter方法(至少在.Net 4.0版本中)。

我同意其他答案中提到的,如果您只想缓存一个对象的单个实例,则应使用AddOrGetExisting。如果不是,则可以使用不同的键缓存备用项。


1
.NET 4文档似乎表明不同 - https://msdn.microsoft.com/zh-cn/library/system.runtime.caching.configuration.memorycacheelement(v=vs.100).aspx。具体来说,[`CacheMemoryLimitMegabytes`](https://msdn.microsoft.com/zh-cn/library/system.runtime.caching.configuration.memorycacheelement.cachememorylimitmegabytes(v=vs.100).aspx)。 - Dan Atkinson
除了配置之外,您还可以通过NamedValueCollection(键值对)在构造函数中设置它们。 - Christian.K

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