MemoryCache不遵循配置中的内存限制


87

我正在使用应用程序中的.NET 4.0 MemoryCache类,并试图限制最大缓存大小,但是在我的测试中,似乎没有缓存实际上在遵守限制。

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

  1. CacheMemoryLimitMegabytes:对象实例可以增长到的最大内存大小(以兆字节为单位)。”
  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不遵守配置的内存限制?


2
for循环是错误的,没有i ++
xiaoyifang 2013年

4
我添加了一个MS连接报告这个错误(也许别人已经做了,但无论如何...)connect.microsoft.com/VisualStudio/feedback/details/806334/...
布鲁诺·布兰特

3
值得注意的是,Microsoft现在(截至9/2014)对上面链接的连接票证添加了相当全面的答复。它的TLDR是,MemoryCache不会在每个操作上固有地检查这些限制,而是仅在内部缓存调整时才遵守这些限制,该调整是基于动态内部计时器而定期进行的。
达斯蒂2015年

5
看起来他们更新了MemoryCache.CacheMemoryLimit的文档:“每次将新项添加到MemoryCache实例时,MemoryCache不会立即立即强制执行CacheMemoryLimit。从内存缓存中逐出额外项的内部启发式方法会逐步执行它……” msdn.microsoft .com / en-us / library /…
2015年

1
@Zeus,我认为MSFT消除了此问题。无论如何,MSFT在与我讨论后都解决了该问题,他们告诉我该限制仅在PoolingTime到期后才适用。
布鲁诺·布兰特

Answers:


100

哇,所以我只花了太多时间在带反射镜的CLR上闲逛,但是我认为我终于可以很好地处理这里发生的事情。

正确读取了设置,但是CLR本身似乎存在一个根深蒂固的问题,看起来它将使内存限制设置基本无用。

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

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.roximateSize实际逼近缓存的大小确实很可怕。遍历CLR垃圾,我发现这是System.SizedReference,这是获取值的操作(IntPtr是MemoryCache对象本身的句柄):

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

我假设extern声明意味着它会在此时进入非托管窗口,并且我不知道如何开始找出它在做什么。从我观察到的结果来看,它在尝试近似整体大小方面做得很糟糕。

第三点值得注意的是对manager.UpdateCacheSize的调用,这听起来像它应该做些事情。不幸的是,在任何正常的工作方式示例中,s_memoryCacheManager始终为null。该字段是从公共静态成员ObjectCache.Host设置的。如果用户愿意,这很容易引起用户的困扰,实际上我可以通过将我自己的IMemoryCacheManager实现放在一起,将其设置为ObjectCache.Host,然后运行该示例来完成某种工作。不过到那时,您似乎也可以自行创建缓存实现,甚至不必理会所有这些东西,尤其是因为我不知道是否将自己的类设置为ObjectCache.Host(静态,

我必须相信,至少其中一部分(如果不是几个部分)只是一个简单的错误。很高兴听到MS的某人与这件事达成了交易。

这个巨大答案的TLDR版本:假定CacheMemoryLimitMegabytes在此时间点已完全清除。您可以将其设置为10 MB,然后继续将高速缓存填满到〜2GB,并且在没有任何项目删除中断的情况下清除内存不足异常。


4
好的答案,谢谢。我放弃了尝试弄清楚这是怎么回事,而是现在通过计数项目的进/出并根据需要手动调用.Trim()来管理缓存大小。我认为System.Runtime.Caching对于我的应用程序来说是一个简单的选择,因为它似乎已被广泛使用,因此我认为不会有任何重大错误。
Canacourse 2011年

3
哇。这就是为什么我如此爱。我遇到了完全相同的行为,编写了一个测试应用程序,即使轮询时间低至10秒且缓存内存限制为1MB,也多次使PC崩溃。感谢您的所有见解。
布鲁诺·布兰特

7
我知道我只是在问题中提到它,但是出于完整性考虑,我将在这里再次提到它。为此,我在Connect上打开了一个问题。connect.microsoft.com/VisualStudio/feedback/details/806334/...
布鲁诺·布兰特

1
我将MemoryCache用于外部服务数据,当我通过将垃圾注入MemoryCache进行测试时,它自动修剪内容,但仅当使用百分比限制值时才如此。绝对大小对限制大小没有任何作用,至少在使用内存分析器进行检查时。没有在while循环中进行测试,而是通过更“实际”的使用进行了测试(这是一个后端系统,因此我添加了WCF服务,该服务可让我根据需要将数据注入到缓存中)。
斯文德(Svend)2014年

这仍然是.NET Core中的问题吗?
Павле

29

我知道这个答案太迟了,但是总比没有好。我想告诉你我写了一个版本MemoryCache,可以自动为您解决Gen 2 Collection问题。因此,每当轮询间隔指示内存压力时,它都会调整。如果您遇到此问题,请尝试一下!

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

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

https://github.com/haneytron/sharpmemorycache


2
这可以按预期工作,并使用生成器进行了测试,该生成器使用1000个字符的字符串填充缓存。虽然,将大约100MB的内容添加到缓存中实际上会增加200-300MB的缓存,我发现这很奇怪。也许我没有在听一些。
Karl Cassar 2014年

5
.NET中的@KarlCassar字符串2n + 20相对于字节的大小大致相同,其中n字符串的长度。这主要是由于Unicode支持。
Haney

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时间进行清理,而添加了许多新元素。


4

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

我发现以下配置和用法通常每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>  

添加到缓存:

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

3

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

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

我将我的测试代码运行了大约15分钟,并将physicalMemoryLimitPercentage设置为65%。我看到在测试过程中内存使用率保持在65-68%之间,并且看到情况逐渐消失。在我的测试中,我将pollingInterval设置为5秒,physicalMemoryLimitPercentage设置为65,physicalMemoryLimitPercentage设置为0,以将其默认设置。

遵循以上建议;可以实现IMemoryCacheManager的实现以从缓存中逐出东西。但是,它将遭受提到的第2代检查问题。尽管根据情况,这可能不是生产代码中的问题,并且可能足以满足人们的需求。


4
一个更新:我正在使用.NET Framework 4.5,并且绝不能解决问题。高速缓存的大小可能足以使计算机崩溃。
布鲁诺·布兰特

问题:您是否具有所提到的连接文章的链接?
布鲁诺·布兰特


3

事实证明这不是一个bug,您所需要做的就是设置池化时间跨度以执行限制,似乎如果您未设置池化就永远不会触发我只是对其进行了测试并且不需要包装或任何其他代码:

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

        };

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


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

您是说修剪还是不修剪?
Canacourse

是的,它确实被修剪了。考虑到人们似乎遇到的所有问题,这很奇怪MemoryCache。我想知道为什么这个示例有效。
DanielLidström2013年

1
我不遵守。我尝试重复该示例,但是缓存仍然无限期增长。
布鲁诺·布兰特

一个令人困惑的示例类:“ Statlock”,“ ItemCount”,“ size”是无用的... NameValueCollection(3)仅包含2个项目?...实际上,您创建了具有sizelimit和pollInterval属性的缓存,仅此而已!没有触及“不驱逐”物品的问题……
Bernhard
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.