MemoryCache在configuration中不遵守内存限制

我在应用程序中使用.NET 4.0 MemoryCache类,试图限制最大caching大小,但是在我的testing中,并没有显示caching实际上服从的是限制。

我正在使用根据MSDN ,应该限制caching大小的设置:

  1. CacheMemoryLimitMegabytes :对象实例可以增长到的最大内存大小(以兆字节为单位)。
  2. PhysicalMemoryLimitPercentage : “高速caching可使用的物理内存百分比,表示为1到100之间的整数值。默认值为零,表示MemoryCache实例根据安装在内存中的内存量pipe理其自己的内存1电脑。” 1.这是不完全正确的 – 低于4的任何值都被忽略,并被replace为4。

我知道这些值是近似的而不是硬限制,因为清除caching的线程每隔x秒触发一次,并且也取决于轮询间隔和其他未公开的variables。 但是,即使考虑到这些差异,在将CacheMemoryLimitMegabytesPhysicalMemoryLimitPercentage设置在一起或在testing应用程序中单独设置时,第一个项目将从caching中逐出时,会看到极其不一致的caching大小。 为了确保我跑了10次testing,并计算出平均数字。

这些是在具有3GB RAM的32位Windows 7 PC上testing以下示例代码的结果。 caching的大小是在每次testing首次调用CacheItemRemoved()之后进行的。 (我知道caching的实际大小会比这大)

MemLimitMB MemLimitPct AVG Cache MB on first expiry 1 NA 84 2 NA 84 3 NA 84 6 NA 84 NA 1 84 NA 4 84 NA 10 84 10 20 81 10 30 81 10 39 82 10 40 79 10 49 146 10 50 152 10 60 212 10 70 332 10 80 429 10 100 535 100 39 81 500 39 79 900 39 83 1900 39 84 900 41 81 900 46 84 900 49 1.8 GB approx. in task manager no mem errros 200 49 156 100 49 153 2000 60 214 5 60 78 6 60 76 7 100 82 10 100 541 

这里是testing应用程序:

 using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Runtime.Caching; using System.Text; namespace FinalCacheTest { internal class Cache { private Object Statlock = new object(); private int ItemCount; private long size; private MemoryCache MemCache; private CacheItemPolicy CIPOL = new CacheItemPolicy(); public Cache(long CacheSize) { CIPOL.RemovedCallback = new CacheEntryRemovedCallback(CacheItemRemoved); NameValueCollection CacheSettings = new NameValueCollection(3); CacheSettings.Add("CacheMemoryLimitMegabytes", Convert.ToString(CacheSize)); CacheSettings.Add("physicalMemoryLimitPercentage", Convert.ToString(49)); //set % here CacheSettings.Add("pollingInterval", Convert.ToString("00:00:10")); MemCache = new MemoryCache("TestCache", CacheSettings); } public void AddItem(string Name, string Value) { CacheItem CI = new CacheItem(Name, Value); MemCache.Add(CI, CIPOL); lock (Statlock) { ItemCount++; size = size + (Name.Length + Value.Length * 2); } } public void CacheItemRemoved(CacheEntryRemovedArguments Args) { Console.WriteLine("Cache contains {0} items. Size is {1} bytes", ItemCount, size); lock (Statlock) { ItemCount--; size = size - 108; } Console.ReadKey(); } } } namespace FinalCacheTest { internal class Program { private static void Main(string[] args) { int MaxAdds = 5000000; Cache MyCache = new Cache(1); // set CacheMemoryLimitMegabytes for (int i = 0; i < MaxAdds; i++) { MyCache.AddItem(Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); } Console.WriteLine("Finished Adding Items to Cache"); } } } 

为什么MemoryCache不遵守configuration的内存限制?

哇,所以我只是花了太多的时间在reflection镜的CLR上进行挖掘,但是我想我最终能够很好的掌握这里发生的事情。

这些设置正确地被读取,但是CLR本身似乎有一个深层次的问题,看上去它会使内存限制设置本质上无用。

下面的代码从System.Runtime.Caching DLL中反映出来,用于CacheMemoryMonitor类(有一个类似的类来监视物理内存并处理另一个设置,但是这是更重要的一个):

 protected override int GetCurrentPressure() { int num = GC.CollectionCount(2); SRef ref2 = this._sizedRef; if ((num != this._gen2Count) && (ref2 != null)) { this._gen2Count = num; this._idx ^= 1; this._cacheSizeSampleTimes[this._idx] = DateTime.UtcNow; this._cacheSizeSamples[this._idx] = ref2.ApproximateSize; IMemoryCacheManager manager = s_memoryCacheManager; if (manager != null) { manager.UpdateCacheSize(this._cacheSizeSamples[this._idx], this._memoryCache); } } if (this._memoryLimit <= 0L) { return 0; } long num2 = this._cacheSizeSamples[this._idx]; if (num2 > this._memoryLimit) { num2 = this._memoryLimit; } return (int) ((num2 * 100L) / this._memoryLimit); } 

您可能会注意到的第一件事是,它甚至不尝试在Gen2垃圾回收之后查看caching的大小,而只是回退到cacheSizeSamples中的现有存储大小值。 所以你永远不可能直接击中目标,但是如果剩下的工作,我们至less可以在我们遇到麻烦之前进行尺寸测量。

因此,假设发生了Gen2 GC,我们遇到了问题2,即ref2.ApproximateSize在真正接近caching大小方面做了一个糟糕的工作。 通过CLR垃圾阻塞我发现这是一个System.SizedReference,这是它正在做什么来获取值(IntPtr是MemoryCache对象本身的句柄):

 [SecurityCritical] [MethodImpl(MethodImplOptions.InternalCall)] private static extern long GetApproximateSizeOfSizedRef(IntPtr h); 

我假设外部声明意味着它在这个时候潜入非托pipe窗口的土地,我不知道如何开始发现它在那里做什么。 从我所观察到的,虽然它尝试近似整体大小的事情是一个可怕的工作。

第三个值得注意的事情是调用manager.UpdateCacheSize,这听起来像应该做些什么。 不幸的是,在任何正常的示例应该如何工作s_memoryCacheManager将始终为空。 该字段是从公共静态成员ObjectCache.Host中设置的。 如果用户select这样的话,这是暴露给用户的,我实际上可以通过将我自己的IMemoryCacheManager实现放在一起,将其设置为ObjectCache.Host,然后运行示例。 在这一点上,似乎你可能只是做自己的caching实现,甚至不打扰所有这些东西,特别是因为我不知道如果设置你自己的类ObjectCache.Host(静态,所以它会影响每一个这些可能是在那里进行)来衡量caching可能会搞砸其他的东西。

我必须相信,至less部分(如果不是几个部分)只是一个直接的错误。 从MS的某个人那里听到与这件事有关的事情真是太好了。

这个巨型答案的TLDR版本:假设CacheMemoryLimitMegabytes在这个时间点完全被破坏。 您可以将它设置为10 MB,然后继续填充caching到〜2GB,并吹出一个内存不足的例外,不删除项目删除。

我知道这个答案很晚了,但迟到比从未好。 我想让你知道我写了一个MemoryCache版本,可以为你自动解决Gen 2的收集问题。 因此,只要轮询间隔表示内存压力,它就会修剪。 如果您遇到这个问题,请立即行动!

http://www.nuget.org/packages/SharpMemoryCache

你也可以在GitHub上find它,如果你对我如何解决它感到好奇。 代码有点简单。

https://github.com/haneytron/sharpmemorycache

我(幸好)昨天第一次尝试使用MemoryCache时偶然发现了这个有用的post。 我认为这将是一个设置值和使用类的简单情况,但我遇到了上述类似的问题。 尝试看看发生了什么事情,我使用ILSpy提取源代码,然后设置一个testing并遍历代码。 我的testing代码与上面的代码非常相似,所以我不会发布它。 从我的testing中我注意到,caching大小的测量从来没有特别精确(如上所述),并且考虑到当前的实现永远无法可靠地工作。 然而,物理测量是好的,如果在每次轮询中测量物理记忆,那么在我看来,代码将可靠地工作。 所以,我删除了MemoryCacheStatistics中的gen 2垃圾收集检查; 在正常情况下,除非自上次测量以来已经存在另外的第二代垃圾回收,否则不会进行存储器测量。

在一个testing场景中,这显然会造成一个很大的差别,因为caching不断被打中,所以对象从来没有机会到达第二代。我想我们将使用这个DLL的修改版本在我们的项目上,并使用官方的MS当.net 4.5出来(根据上面提到的连接文章应该有修复它)build立。 从逻辑上讲,我可以看到为什么第2代检查已经到位,但实际上我不确定这是否合理。 如果内存达到90%(或者设置的任何限制),那么不pipe是否发生第2代收集都无关紧要,无论如何都应该驱逐物品。

我的testing代码运行了15分钟,物理内存限制百分比设置为65%。 在testing过程中,我看到内存使用率保持在65-68%之间,看到东西被逐出。 在我的testing中,我将pollingInterval设置为5秒,physicalMemoryLimitPercentage设置为65,physicalMemoryLimitPercentage设置为0以默认设置。

遵循上述build议; IMemoryCacheManager的实现可以从caching中驱逐事物。 然而,它会遭受上述第二代检查问题的困扰。 尽pipe根据情况,这可能不是生产代码中的问题,并且可以为人们充分工作。

我也遇到过这个问题。 我正在caching每秒被解雇的进程数十次。

我发现以下configuration和使用大部分时间每5秒释放一次

App.config中:

记下cacheMemoryLimitMegabytes 。 当这个设置为零时,清除程序不会在合理的时间内发射。

  <system.runtime.caching> <memoryCache> <namedCaches> <add name="Default" cacheMemoryLimitMegabytes="20" physicalMemoryLimitPercentage="0" pollingInterval="00:00:05" /> </namedCaches> </memoryCache> </system.runtime.caching> 

添加到caching:

 MemoryCache.Default.Add(someKeyValue, objectToCache, new CacheItemPolicy { AbsoluteExpiration = DateTime.Now.AddSeconds(5), RemovedCallback = cacheItemRemoved }); 

确认caching删除正在工作:

 void cacheItemRemoved(CacheEntryRemovedArguments arguments) { System.Diagnostics.Debug.WriteLine("Item removed from cache: {0} at {1}", arguments.CacheItem.Key, DateTime.Now.ToString()); } 

如果您使用以下修改后的类并通过任务pipe理器监视内存,实际上它会被修剪:

 internal class Cache { private Object Statlock = new object(); private int ItemCount; private long size; private MemoryCache MemCache; private CacheItemPolicy CIPOL = new CacheItemPolicy(); public Cache(double CacheSize) { NameValueCollection CacheSettings = new NameValueCollection(3); CacheSettings.Add("cacheMemoryLimitMegabytes", Convert.ToString(CacheSize)); CacheSettings.Add("pollingInterval", Convert.ToString("00:00:01")); MemCache = new MemoryCache("TestCache", CacheSettings); } public void AddItem(string Name, string Value) { CacheItem CI = new CacheItem(Name, Value); MemCache.Add(CI, CIPOL); Console.WriteLine(MemCache.GetCount()); } } 

我已经用@Canacourse的例子和@woany的修改做了一些testing,我认为有一些关键的调用阻塞了内存caching的清理。

 public void CacheItemRemoved(CacheEntryRemovedArguments Args) { // this WriteLine() will block the thread of // the MemoryCache long enough to slow it down, // and it will never catch up the amount of memory // beyond the limit Console.WriteLine("..."); // ... // this ReadKey() will block the thread of // the MemoryCache completely, till you press any key Console.ReadKey(); } 

但为什么@woany的修改似乎将内存保持在同一水平? 首先,RemovedCallback未设置,并且没有控制台输出或等待可能阻塞内存高速caching的线程的input。

其次…

 public void AddItem(string Name, string Value) { // ... // this WriteLine will block the main thread long enough, // so that the thread of the MemoryCache can do its work more frequently Console.WriteLine("..."); } 

一个Thread.Sleep(1)每〜1000个AddItem()会有相同的效果。

那么这个问题并不是很深入的研究,但是看起来好像MemoryCache的线程没有足够的CPU时间进行清理,而添加了许多新的元素。