检测内存即将耗尽的情况(获取“可用物理内存”量)

6
我正在将高帧率相机拍摄的图像传输到内存缓冲区(一个列表),由于这些图像非常大,计算机很快就会耗尽内存。
我想要做的是在应用程序耗尽内存之前停止传输。在我的测试中,我发现“可用物理内存”指标接近零时一致出现这种情况。
现在的问题是,我找不到一种以编程方式实际获取此值的方法;在XP中,它甚至没有显示在任何地方(仅在Vista/7任务管理器中)。
我已经尝试了所有我能找到的方法(WMI、性能计数器、MemoryStatus等),但这些方法得到的都只是“可用物理内存”,这显然不同。
有什么想法吗?
更新 不幸的是,我需要数据保留在内存中(是的,我知道我不能“保证”它将在物理内存中,但仍然需要),因为数据是实时流式传输的,我需要在存储到内存后在内存中预览它。

2
不妨尝试解决问题?也许你可以使用固定缓冲区或写入内存映射文件(其大小可以大于物理内存)。 - Tergiver
也许可以使用两个缓冲区,交替将它们刷新到磁盘上? - Machinarius
这在保存时可能有效,但是我需要内存中的数据进行内存回放(我无法从普通硬盘每秒读取几百兆字节...) - Darko Kenda
预览是否必须是完整帧率和分辨率?您可以转码为合理的预览大小,以便更轻松地流回。 - Paolo
不行,必须是完整的分辨率,甚至可能更高的帧速率。 - Darko Kenda
这并不是一个解决方案(因此是一条评论),但我想指出存在 <gcAllowVeryLargeObjects> 这个选项,你可以将其添加到你的应用程序配置中,以允许使用大于2GB的数组(即使你的应用程序是x64,这也不是默认启用的)。 - AnorZaken
6个回答

9
相关性不等于因果关系。即使有大量的物理内存仍然可用,你也可能会“用尽内存”。物理内存几乎肯定是无关紧要的;你可能正在用尽的是地址空间。人们往往认为“内存”占用芯片上的空间,但这在过去十年中已经不再成立了。现代操作系统中的内存通常被视为一个大型磁盘文件,其上方有一个大型硬件缓存来加速它。物理内存只是基于磁盘的内存的一种性能优化。
如果你的物理内存不足,那么你的性能将非常糟糕。但实际上你用尽的是地址空间。一个大列表必须有一个大的连续的地址空间块,而你想要的大小可能没有足够大的块留下。
不要这样做。拉下一个合理大小的块,将其转储到磁盘上,然后根据需要处理磁盘上的文件。

1
很遗憾,我无法将其转储到磁盘上。首先,因为它不够快,其次因为我需要在内存中重新播放。 - Darko Kenda

8

我来晚了,不过你有没有考虑使用System.Runtime.MemoryFailPoint类呢?它会完成很多工作,以确保所请求的分配将成功,并在失败时抛出InsufficientMemoryException异常;你可以捕获它并停止传输。你可以预测传入帧的平均大小,并尝试分配其中3或4个,然后在失败时停止获取。也许类似这样做?

const int AverageFrameSize = 10 * 1024 * 1024 // 10MB

void Image_OnAcquired(...)
{
    try
    {
        var memoryCheck = new MemoryFailPoint(AverageFrameSize * 3);
    }
    catch (InsufficientMemoryException ex)
    {
        _camera.StopAcquisition();
        StartWaitingForFreeMemory();
        return;
    }

    // if you get here, there's enough memory for at least a few
    // more frames
}

我不认为它会百分之百地可靠,但这是一个开始。它绝对比性能计数器更可靠,原因在其他答案中有解释。


非常好!不知道MemoryFailPoint类+1。但是你的代码有一个错误:构造函数需要一个以兆字节为单位的参数,而不是字节。上面的代码正在请求30TB的空闲内存!!这肯定会在消费者硬件上失败。 :P 顺便指出,_"MemoryFailPoint以16 MB的粒度运行。任何小于16 MB的值都被视为16 MB,其他值则被视为下一个最大的16 MB的倍数。"_ - AnorZaken
我检查了参考源代码,如果有人担心的话,没有溢出的风险。在左移20位之前,sizeInMegabytes参数被转换为ulong(这与乘以1024 * 1024相同)。 - AnorZaken
实际上,我仔细查看了文档,发现还有一个错误:MemoryFailPoint实例应该保持活动状态,直到分配了预期数量的内存,然后再进行处理。在单线程场景中,这个错误并不是致命的,但如果多个线程使用MemoryFailPoint来检查内存,则如果MemoryFailPoint实例没有在代码中的正确位置处被处理,它可能会失败(导致意外的OutOfMemory)。 (MemoryFailPoint保留了进程范围内的保留内存记录,其构造函数增加,而其“Dispose()”减少。) - AnorZaken

2

我想添加自己的答案,因为除了 OwenP的答案之外,在使用System.Runtime.MemoryFailPoint时有两个重要错误。

第一个错误非常简单,可以轻松解决:构造函数签名是public MemoryFailPoint(int sizeInMegabytes),因此AverageFrameSize参数应该以兆字节为单位,而不是字节。还要注意以下关于大小的内容:

MemoryFailPoint以16 MB的粒度运行。任何小于16 MB的值都将视为16 MB,其他值都将视为下一个最大的16 MB倍数。

第二个错误是必须保持MemoryFailPoint实例存活,直到分配了您希望使用的内存,然后才能进行处理!

这可能有点难以解决,并且可能需要根据OP实际代码进行设计更改。

您需要以这种方式处理它的原因是,MemoryFailPoint类会从其构造函数中保留内存分配的进程级记录。这样做是为了确保如果两个线程在大约相同的时间内执行内存检查,则它们不会同时成功,除非有足够的内存来满足两个线程的需求。(否则,MemoryFailPoint类在多线程应用程序中将无用!)
构造函数保留的内存在调用Dispose()时取消保留。因此,线程应该尽快在分配所需内存后释放MemoryFailPoint实例,但不要在此之前释放。 (“尽快”部分是首选但不是关键。延迟处理可能会导致其他内存检查不必要地失败,但至少可以保守处理。)
上述要求需要修改代码设计。检查内存的方法必须执行分配,或者它必须将MemoryFailPoint实例传递给调用者,这使得调用者有责任在正确的时间处置它。(这是MSDN上示例代码所做的事情。)
使用第一种方法(和固定的缓冲区大小)可能看起来像这样:
const int FrameSizeInMegabytes = 10; // 10MB (perhaps more is needed?)
const int FrameSizeInBytes = FrameSizeInMegabytes << 20;
// shifting by 20 is the same as multiplying with 1024 * 1024.

bool TryCreateImageBuffer(int numberOfImages, out byte[,] imageBuffer)
{
    // check that it is theoretically possible to allocate the array.
    if (numberOfImages  < 0 || numberOfImages > 0x7FFFFFC7)
        throw new ArgumentOutOfRangeException("numberOfImages",
            "Outside allowed range: 0 <= numberOfImages <= 0x7FFFFFC7");

    // check that we have enough memory to allocate the array.
    MemoryFailPoint memoryReservation = null;
    try
    {
        memoryReservation =
            new MemoryFailPoint(FrameSizeInMegabytes * numberOfImages);
    }
    catch (InsufficientMemoryException ex)
    {
        imageBuffer = null;
        return false;
    }

    // if you get here, there's likely to be enough memory
    // available to create the buffer. Normally we can't be
    // 100% sure because another thread might allocate memory
    // without first reserving it with MemoryFailPoint in
    // which case you have a race condition for the allocate.
    // Because of this the allocation should be done as soon
    // as possible - the longer we wait the higher the risk.
    imageBuffer = new byte[numberOfImages, FrameSizeInBytes];

    //Now that we have allocated the memory we can go ahead and call dispose
    memoryReservation.Dispose();

    return true;
}

0x7FFFFFC7是任何单字节类型的数组在任何维度上允许的最大索引器,可以在MSDN有关数组的页面中找到。

第二种方法(调用方负责MemoryFailPoint实例)可能如下所示:

const int AverageFrameSizeInMegabytes = 10; // 10MB

/// <summary>
/// Tries to create a MemoryFailPoint instance for enough megabytes to
/// hold as many images as specified by <paramref name="numberOfImages"/>.
/// </summary>
/// <returns>
/// A MemoryFailPoint instance if the requested amount of memory was
/// available (at the time of this call), otherwise null.
/// </returns>
MemoryFailPoint GetMemoryFailPointFor(int numberOfImages)
{
    MemoryFailPoint memoryReservation = null;
    try
    {
        memoryReservation =
            new MemoryFailPoint(AverageFrameSizeInMegabytes * numberOfImages);
    }
    catch (InsufficientMemoryException ex)
    {
        return null;
    }
    return memoryReservation;
}

这看起来简单得多(并且更灵活),但现在需要调用者处理MemoryFailPoint实例,并在正确的时间点处处理它。(由于我没有想出一个好的和描述性的方法名称,所以添加了一些强制性文档。)
重要提示:在此上下文中,“保留”是什么意思
内存不是“保留”的意思是它被保证可用(对于调用线程)。它只意味着当线程使用MemoryFailPoint检查内存时,假设成功,它会将其内存大小添加到进程范围(静态)的“保留”量中,而MemoryFailPoint类会跟踪此量。此保留将导致任何其他对MemoryFailPoint的调用(例如来自其他线程)将把总剩余内存量视为实际减去当前进程范围(静态)的“保留”量。(当MemoryFailPoint实例被处理时,它们会从保留总量中扣除其数量。)然而,实际的内存分配系统本身并不知道或关心这种所谓的“保留”,这也是MemoryFailPoint没有强烈保证的原因之一。
请注意,所谓的“保留”内存只是作为一定数量进行跟踪。由于它不是对特定内存段的实际保留,这进一步削弱了保证,正如在参考源代码中发现的以下沮丧评论所说明的那样:“//请注意,多个线程仍然可以在我们的空闲地址空间上竞争,这很难解决。” 不难猜出被审查的词是什么。
这是一篇关于如何克服数组的2GB限制的有趣文章。
此外,如果您需要分配非常大的数据结构,您需要了解<gcAllowVeryLargeObjects>,您可以在应用配置中设置它。
值得注意的是,这与仅涉及物理内存没有任何关系,正如 OP 真正想要的那样。事实上,在放弃并报告失败之前,MemoryFailPoint 将尝试做的其中一件事情是增加页面文件的大小。但如果使用正确,它将非常好地避免出现 OutOfMemoryException,这至少是 OP 想要的一半。
如果你真的想要将数据强制写入物理内存,那么据我所知,你必须使用 AllocateUserPhysicalPages 进行本地操作。这并不是世界上最简单的事情,会有大量可能出错的事情,并且需要适当的权限,几乎肯定是过度的。操作系统不喜欢被告知如何管理内存,因此不容易做到这一点……

2
在Vista/7中,你不能仅凭免费内存计数器来作为指南,因为它可能一直接近于零。这是因为Vista/7的超级取回机制使用免费内存缓存磁盘上的东西,认为你可能会用到它们。
此外,如果你运行一个32位的C#进程,每个进程最多只能使用2GB的内存(实际上在达到不稳定之前,实际上更接近1.5GB),所以即使你的电脑显示有大量的空闲内存,当你的进程达到2GB的限制时,你仍然会遇到内存不足的异常。
正如Tergiver在上面的评论中所说,真正的解决方案是避免将所有文件都保存在内存中,而是根据需要交换图像的部分内容。

1

感谢所有的回答。

我再次思考了一下,得出结论,要实现我最初想要的功能是相当困难(如果不是不可能的),即在应用程序即将耗尽内存时,以某种方式检测到它。

所有的答案似乎都指向同一个方向(以某种方式将数据保持在内存之外),但不幸的是,我不能这样做,因为我真的“需要”数据留在内存中(如果可能的话,是物理内存)。

由于我必须做出妥协,我决定为用户创建一个设置,以便决定捕获数据的内存使用限制。 这至少很容易实现。


0

OutOfMemoryException的意思是当前内存分配无法被满足,但这并不一定意味着系统甚至进程即将耗尽内存。想象一个Hello World类型的应用程序,它开始时分配了2GB的内存块。在32位系统上,这很可能会触发异常,尽管此时该进程实际上还没有分配任何重要的内存。

OutOfMemoryExceptions的常见原因是没有足够的连续内存可用。也就是说,有足够的内存可用,但没有足够大的块来满足当前请求。换句话说,通过监视空闲内存计数器来避免OOM并不是真正可行的方法。


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