91

我正在使用应用程序中的 .NET 4.0 MemoryCache类并尝试限制最大缓存大小,但在我的测试中,缓存似乎并未真正遵守限制。

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

  1. CacheMemoryLimitMegabytes:对象实例可以增长到的最大内存大小(以 MB 为单位)。”
  2. PhysicalMemoryLimitPercentage “缓存可以使用的物理内存的百分比,表示为1到100的整数值。默认为零,表示MemoryCache实例根据安装的内存量管理自己的内存1计算机。” 1.这并不完全正确——任何低于 4 的值都会被忽略并替换为 4。

我知道这些值是近似值而不是硬限制,因为清除缓存的线程每 x 秒触发一次,并且还取决于轮询间隔和其他未记录的变量。然而,即使考虑到这些差异,当在测试应用程序中一起或单独设置CacheMemoryLimitMegabytesPhysicalMemoryLimitPercentage后,从缓存中逐出第一项时,我看到缓存大小非常不一致。可以肯定的是,我每次测试都运行了 10 次并计算了平均值。

这些是在具有 3GB RAM 的 32 位 Windows 7 PC 上测试以下示例代码的结果。在每次测试中第一次调用CacheItemRemoved()后获取缓存的大小。(我知道缓存的实际大小会比这个大)

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

这是测试应用程序:

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不遵守配置的内存限制?

4

7 回答 7

103

哇,所以我花了太多时间在带有反射器的 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 垃圾回收之后才尝试查看缓存的大小,而只是回退到 cacheSizeSamples 中现有的存储大小值。因此,您将永远无法直接击中目标,但如果其余的工作正常,我们至少会在遇到真正麻烦之前进行尺寸测量。

所以假设发生了 Gen2 GC,我们遇到了问题 2,即 ref2.ApproximateSize 在实际近似缓存大小方面做得很糟糕。通过 CLR junk 我发现这是一个 System.SizedReference,这就是它为获取值所做的事情(IntPtr 是 MemoryCache 对象本身的句柄):

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

我假设 extern 声明意味着它在这一点上潜入非托管窗口领域,我不知道如何开始找出它在那里做了什么。从我所观察到的情况来看,尽管它在尝试近似整体事物的大小方面做得很糟糕。

第三个值得注意的事情是对 manager.UpdateCacheSize 的调用,听起来它应该做点什么。不幸的是,在任何应该如何工作的正常示例中,s_memoryCacheManager 将始终为空。该字段是从公共静态成员 ObjectCache.Host 设置的。如果用户愿意,这会暴露给用户来搞乱,实际上我可以通过将我自己的 IMemoryCacheManager 实现拼凑在一起,将其设置为 ObjectCache.Host,然后运行示例,从而使这件事像预期的那样工作. 不过,在那一点上,您似乎还不如自己做缓存实现,甚至不用理会所有这些东西,尤其是因为我不知道是否将您自己的类设置为 ObjectCache.Host (静态,

我必须相信至少有一部分(如果不是几个部分)只是一个直接的错误。很高兴能从 MS 的某个人那里听到这件事的处理方式。

这个巨大答案的 TLDR 版本:假设 CacheMemoryLimitMegabytes 在这个时间点完全被破坏。您可以将其设置为 10 MB,然后继续将缓存填充到 ~2GB 并吹出内存不足异常,而不会触发项目删除。

于 2011-09-11T05:51:52.663 回答
30

I know this answer is crazy late, but better late than never. I wanted to let you know that I wrote a version of MemoryCache that resolves the Gen 2 Collection issues automatically for you. It therefore trims whenever the polling interval indicates memory pressure. If you're experiencing this issue, give it a go!

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

You can also find it on GitHub if you're curious about how I solved it. The code is somewhat simple.

https://github.com/haneytron/sharpmemorycache

于 2014-04-06T21:21:21.550 回答
5

我也遇到过这个问题。我正在缓存每秒数十次被触发到我的进程中的对象。

我发现以下配置和用法在大多数情况下每 5 秒释放一次项目。

应用程序配置:

注意cacheMemoryLimitMegabytes。当此值设置为零时,清除例程将不会在合理的时间内触发。

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

添加到缓存:

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

确认缓存删除工作正常:

void cacheItemRemoved(CacheEntryRemovedArguments arguments)
{
    System.Diagnostics.Debug.WriteLine("Item removed from cache: {0} at {1}", arguments.CacheItem.Key, DateTime.Now.ToString());
}
于 2015-08-12T18:18:18.800 回答
4

我已经用@Canacourse 的例子和@woany 的修改做了一些测试,我认为有一些关键的调用会阻止清理内存缓存。

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 并且没有控制台输出或等待输入可能会阻塞内存缓存的线程。

第二...

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("...");
}

每 ~1000 个 AddItem() 一个 Thread.Sleep(1) 将具有相同的效果。

嗯,对问题的调查不是很深,但是看起来MemoryCache的线程没有得到足够的CPU时间来清理,同时又增加了很多新元素。

于 2013-11-11T14:20:11.163 回答
3

我(谢天谢地)昨天第一次尝试使用 MemoryCache 时偶然发现了这篇有用的帖子。我认为这将是设置值和使用类的简单案例,但我遇到了上述类似的问题。为了尝试查看发生了什么,我使用 ILSpy 提取了源代码,然后设置了一个测试并逐步执行代码。我的测试代码与上面的代码非常相似,所以我不会发布它。从我的测试中,我注意到缓存大小的测量从来都不是特别准确(如上所述),并且鉴于当前的实现永远不会可靠地工作。然而,物理测量很好,如果在每次轮询时都测量物理内存,那么在我看来,代码可以可靠地工作。因此,我删除了 MemoryCacheStatistics 中的第 2 代垃圾收集检查;

在测试场景中,这显然会产生很大的不同,因为缓存会不断被命中,因此对象永远没有机会进入第 2 代。我认为我们将在我们的项目中使用此 dll 的修改版本并使用官方 MS当 .net 4.5 出来时构建(根据上面提到的连接文章应该有修复)。从逻辑上讲,我可以看到为什么要进行第 2 代检查,但实际上我不确定它是否有意义。如果内存达到 90%(或它设置的任何限制),那么是否发生第 2 代收集无关紧要,无论如何都应该驱逐项目。

我让我的测试代码运行了大约 15 分钟,并将physicalMemoryLimitPercentage 设置为 65%。在测试期间,我看到内存使用率保持在 65-68% 之间,并且看到事情被正确驱逐。在我的测试中,我将 pollingInterval 设置为 5 秒,physicalMemoryLimitPercentage 设置为 65,physicalMemoryLimitPercentage 设置为 0 以默认设置。

遵循上述建议;可以使 IMemoryCacheManager 的实现从缓存中驱逐事物。然而,它会受到提到的第 2 代检查问题的影响。尽管根据场景的不同,这在生产代码中可能不是问题,并且对人们来说可能足够工作。

于 2011-11-24T09:44:08.247 回答
3

事实证明这不是错误,您需要做的就是设置池时间跨度以强制执行限制,似乎如果您不设置池,它将永远不会触发。我只是对其进行了测试,不需要包装器或任何额外的代码:

 private static readonly NameValueCollection Collection = new NameValueCollection
        {
            {"CacheMemoryLimitMegabytes", "20"},
           {"PollingInterval", TimeSpan.FromMilliseconds(60000).ToString()}, // this will check the limits each 60 seconds

        };

根据缓存的增长速度设置“”的值PollingInterval,如果增长过快,则增加轮询检查的频率,否则保持检查不是很频繁,以免造成开销。

于 2018-12-07T14:12:33.727 回答
1

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

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());
    }
}
于 2012-07-12T10:48:02.847 回答