【问题标题】:Memory leak in unmanaged code?非托管代码中的内存泄漏?
【发布时间】:2013-03-28 21:04:31
【问题描述】:

我已经追踪了很远的泄漏,但我自己似乎无法理解(修复)它。我首先使用 ANTS 内存分析器来确保我的代码实际上是在堆叠内存。它从使用 25 MB 开始,但在一个小时左右的时间内使用超过 100 MB。我正在为其编写此代码的朋友实际上一直在使用这个有问题的程序,他用了整个 18 GB 的内存并得到了内存不足的异常。

泄漏部分对程序来说并不重要,但如果没有 RefreshSessions() 方法,它几乎没有用处。

我一直在从 Code Project 扩展项目 Vista Core Audio API Master Volume Control

这是似乎泄漏的部分。不使用经过测试,不泄漏。

更新:

public void RefreshSessions()
    {
        Marshal.ThrowExceptionForHR(_AudioSessionManager.GetSessionEnumerator(out _SessionEnum));
        _Sessions.Refresh(_SessionEnum);
    }

(从这里删除了类代码)

我没有写太多代码,所以我可能遗漏了一些东西,但如果需要更多详细信息,您可以实际下载源代码,或者我可以尽我所能回答。

(此处删除了不必要的代码)

使用这个简单的控制台应用程序测试了泄漏:

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            MMDeviceEnumerator DevEnum = new MMDeviceEnumerator();
            MMDevice device = DevEnum.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia);

            Console.ReadKey();
            int i = 0;
            while (i < 10000)
            {
                device.AudioSessionManager.RefreshSessions();
                i++;
            }
            Console.ReadKey();
        }
   }
}

更新 2

我想我已经解决了。必须运行一些更长的测试,但至少看起来内存使用情况已经稳定。这个想法来自 dialer,他找到了 C++ 泄漏的修复方法。

public void RefreshSessions()
        {
            _Sessions.Release(); //added this
            IAudioSessionEnumerator _SessionEnum;
            Marshal.ThrowExceptionForHR(_AudioSessionManager.GetSessionEnumerator(out _SessionEnum));
            _Sessions.Refresh(_SessionEnum);
        }

这是SessionCollection中的部分:

public void Release()
        {
            Marshal.ReleaseComObject(_AudioSessionEnumerator);
        }

这并不完全是建议的代码拨号器(我最终还是使用了它),但仍然如此。 正如他所说,这可能不是实现这一目标的最佳方式,但我会继续使用它,因为它似乎对我的应用程序没有任何不利影响。

【问题讨论】:

  • 我没有看到SessionoCollection类的代码,但是当你调用RefreshSessions方法时,会创建一个新的SessionCollection对象,但是_Sessions之前的值可能需要一些释放/处置代码的附加资源。您还创建了一个会话枚举器对象,因此请确保它也被正确处理。
  • 不幸的是我的电脑坏了,甚至无法尝试太多。你的建议可能来自我给出的描述,但我无法解决问题。我已经能够更准确地指出它,但起源在代码的 COM 部分。我只是在尝试解决这个问题时才了解 COM,所以我在这方面的技能很弱。
  • 您是否尝试构建一个包含此版本的RefreshSessions() 的最小可执行应用程序,该应用程序使用具有几千次迭代的循环来演示泄漏?
  • 现在做。制作了一个小型控制台应用程序,它只创建 MMDevice 等待按下一个键(这样我就可以看到它使用了多少内存)然后调用 RefreshSessions() 1000 次并再次等待。内存消耗从 19 056K 上升到 23 680K 并保持不变。 10000 次迭代将其提升到 60 072K。
  • @Degath 您可以将应用程序的代码添加到问题中吗?

标签: c# memory-leaks unmanaged


【解决方案1】:

另一个编辑

public void RefreshSessions()
{
    if (_SessionEnum != null)
    {
        Marshal.ReleaseComObject(_SessionEnum);
    }
    Marshal.ThrowExceptionForHR(_AudioSessionManager.GetSessionEnumerator(out _SessionEnum));
}

以上代码显式释放 SessionEnum 并修复了 C# 中的泄漏。 这可能应该以更好的方式处理。

编辑:

下面的 C++ 程序等效于您在循环测试程序中所做的。在 for 循环末尾的 Release 调用修复了泄漏。我今天需要去,也许你可以玩一会儿,试着自己解决。或者也许其他人可以找出并解释为什么 CLR 垃圾收集器不会在上面的 C# 程序中的某个时刻自动调用Release

#include <stdio.h>
#include <tchar.h>
#include <audiopolicy.h>
#include <mmdeviceapi.h>

#define SAFE_RELEASE(p) { if ( (p) ) { (p)->Release(); (p) = 0; } }
#define CHECK_HR(hr) if (FAILED(hr)) { goto done; }

const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioSessionManager2 = __uuidof(IAudioSessionManager2);

int _tmain(int argc, _TCHAR* argv[])
{
    HRESULT hr;

    CoInitialize(0);

    IMMDeviceEnumerator *deviceEnum = 0;
    CHECK_HR(hr = CoCreateInstance(
        CLSID_MMDeviceEnumerator, NULL,
        CLSCTX_ALL, IID_IMMDeviceEnumerator,
        (void**)&deviceEnum));;

    IMMDevice *endpoint = 0;
    CHECK_HR(deviceEnum->GetDefaultAudioEndpoint(eRender, eMultimedia, &endpoint));

    getchar();

    // lazy initialization as found in MMDevice.AudioSessionManager..get
    IAudioSessionManager2 *m = 0;
    CHECK_HR(endpoint->Activate(IID_IAudioSessionManager2, CLSCTX_ALL, 0, (void **)&m));

    for (int i = 0; i < 100000; i++)
    {
        IAudioSessionEnumerator *sessEnum = 0;
        m->GetSessionEnumerator(&sessEnum);
        sessEnum->Release(); // leak
    }

    getchar();

    printf("success");
    return 0;

done:
    printf("failure");
    return 1;
}

我的猜测

_AudioSessionManager.GetSessionEnumerator(out _SessionEnum) 产生一个枚举器。当您调用构造函数SessionCollection(_SessionEnum) 时,将枚举_SessionEnum。每个枚举步骤都会检索一个实际的 unmanaged 对象。

如果它是一个值类型,那么它实际上会被复制到会话集合中(请记住List(IEnumerable e) 构造函数复制每个元素)。然后副本将被垃圾收集,但原始对象是从非托管代码中分配的并产生泄漏。如果是这种情况,您应该在调用 Collection 构造函数后立即使用一些非管理内存释放函数释放内存。

如果它是一个引用类型,它也不会被释放,因为内存中的 actual 对象不是垃圾回收的,因为它是从非托管代码中分配的。如果是这种情况,您需要在不再需要对象时使用非托管库函数释放它们的内存。

【讨论】:

  • 泄漏已经发生在 Marshal 部分,注释掉现在不同的 _Sessions 更新方法不会影响泄漏。
  • 看来你是第一个!但是在我做出自己的解决方案之前,我没有注意到您的最新更新。感谢您的帮助!
  • @Degath 澄清一下,这应该不是首先是必要的,也许这个“修复”有不可预见的副作用。它可能是 CLR 或 codeproject 库的编组代码中的错误。也许你应该问另一个关于 SO 的问题,为什么这个特定的 COM 对象在它的唯一引用 _SessionEnum 被重新分配时没有被释放(也许它有一个奇怪的结果,因为它是一个 out 参数,但我没有这么想)。
【解决方案2】:

如果您有非托管代码,_Sessions 内存何时释放?如果您只是重新分配私有字段,则永远不会释放内存。

这是一个例子: http://social.msdn.microsoft.com/forums/en-US/clr/thread/b2162d42-0d7a-4513-b02c-afd6cdd854bd

你需要使用dll的方法来释放内存(C++中的delete[])

【讨论】:

    【解决方案3】:

    .NET 总是能够轻松泄漏内存 - 或者更确切地说,它会导致 GC 认为它们正在使用中永远不会被清理的未收集垃圾。最著名的事件是 DARPA 挑战小组,他们相信这种炒作,并认为漏洞存在于他们的 C 驱动程序代码中,可怜的人。

    从那时起,出现了相当多的内存泄漏分析器。我认为Redgate's ANTS 中最著名的一个,但还有很多其他的。运行您的应用程序,查看哪些对象受到欢迎,查看哪些对象引用了它们,在正确的位置放置一些代码以取消引用它们(例如,更多的 Dispose 和/或 using 语句)。

    【讨论】:

      猜你喜欢
      • 2023-03-05
      • 2012-01-30
      • 2020-07-11
      • 2010-12-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-06-03
      • 1970-01-01
      相关资源
      最近更新 更多