25

我有一个长时间运行的进程,需要经常对 Active Directory 进行大量查询。为此,我一直在使用 System.DirectoryServices 命名空间,使用 DirectorySearcher 和 DirectoryEntry 类。我注意到应用程序中存在内存泄漏。

可以使用以下代码复制它:

while (true)
{
    using (var de = new DirectoryEntry("LDAP://hostname", "user", "pass"))
    {
        using (var mySearcher = new DirectorySearcher(de))
        {
            mySearcher.Filter = "(objectClass=domain)";
            using (SearchResultCollection src = mySearcher.FindAll())
            {
            }            
         }
    }
}

这些类的文档说,如果不调用 Dispose(),它们会泄漏内存。我也尝试过不使用 dispose ,在这种情况下它只会泄漏更多内存。我已经用框架版本 2.0 和 4.0 对此进行了测试。以前有人遇到过这个吗?有什么解决方法吗?

更新:我尝试在另一个 AppDomain 中运行代码,但似乎也没有帮助。

4

5 回答 5

16

尽管可能很奇怪,但似乎只有在您对搜索结果不做任何事情时才会发生内存泄漏。如下修改问题中的代码不会泄漏任何内存:

using (var src = mySearcher.FindAll())
{
   var enumerator = src.GetEnumerator();
   enumerator.MoveNext();
}

这似乎是由于内部 searchObject 字段具有延迟初始化,使用 Reflector 查看 SearchResultCollection 引起的:

internal UnsafeNativeMethods.IDirectorySearch SearchObject
{
    get
    {
        if (this.searchObject == null)
        {
            this.searchObject = (UnsafeNativeMethods.IDirectorySearch) this.rootEntry.AdsObject;
        }
        return this.searchObject;
    }
}

除非 searchObject 被初始化,否则 dispose 不会关闭非托管句柄。

protected virtual void Dispose(bool disposing)
{
    if (!this.disposed)
    {
        if (((this.handle != IntPtr.Zero) && (this.searchObject != null)) && disposing)
        {
            this.searchObject.CloseSearchHandle(this.handle);
            this.handle = IntPtr.Zero;
        }
    ..
   }
}

在 ResultsEnumerator 上调用 MoveNext 会在集合上调用 SearchObject,从而确保它也被正确处理。

public bool MoveNext()
{
  ..
  int firstRow = this.results.SearchObject.GetFirstRow(this.results.Handle);
  ..
}

我的应用程序中的泄漏是由于其他一些非托管缓冲区未正确释放,并且我所做的测试具有误导性。现在问题已经解决了。

于 2011-05-23T14:22:53.607 回答
7

托管包装器并没有真正泄漏任何东西。如果你不调用Dispose未使用的资源,垃圾回收期间仍然会回收。

但是,托管代码是基于 COM 的 ADSI API 之上的包装器,当您创建DirectoryEntry底层代码时,将调用该ADsOpenObject函数。返回的 COM 对象在释放时DirectoryEntry或在完成期间释放。

当您将 ADsOpenObject API 与一组凭据和 WinNT 提供程序一起使用时,有一个记录在案的内存泄漏

  • 此内存泄漏发生在 Windows XP、Windows Server 2003、Windows Vista、Windows Server 2008、Windows 7 和 Windows Server 2008 R2 的所有版本上。
  • 仅当您将 WinNT 提供程序与凭据一起使用时,才会发生此内存泄漏。LDAP 提供程序不会以这种方式泄漏内存。

但是,泄漏只有 8 个字节,据我所知,您使用的是 LDAP 提供程序,而不是 WinNT 提供程序。

调用DirectorySearcher.FindAll将执行需要大量清理的搜索。此清理在DirectorySearcher.Dispose. 在您的代码中,此清理是在循环的每次迭代中执行的,而不是在垃圾收集期间执行的。

除非 LDAP ADSI API 中确实存在未记录的内存泄漏,否则我能想到的唯一解释是非托管堆的碎片。ADSI API 由进程内 COM 服务器实现,每次搜索可能会在进程的非托管堆上分配一些内存。如果此内存变得碎片化,则在为新搜索分配空间时堆可能必须增长。

如果我的假设成立,一种选择是在单独的 AppDomain 中运行搜索,然后可以回收该 AppDomain 以卸载 ADSI 并回收内存。然而,尽管内存碎片可能会增加对非托管内存的需求,但我预计需要多少非托管内存会有一个上限。当然,除非你有泄漏。

此外,您可以尝试使用该DirectorySearcher.CacheResults属性。是否设置它以false消除泄漏?

于 2011-05-09T11:45:32.943 回答
3

由于实现限制,SearchResultCollection 类在垃圾回收时无法释放其所有非托管资源。为防止内存泄漏,您必须在不再需要 SearchResultCollection 对象时调用 Dispose 方法。

http://msdn.microsoft.com/en-us/library/system.directoryservices.directorysearcher.findall.aspx

编辑:

我已经能够使用 perfmon 重现明显的泄漏,并在测试应用程序的进程名称上添加 Private Bytes 计数器(对我来说是 Experiments.vshost)

Private Bytes 计数器将在应用程序循环时稳步增长,它从大约 40,000,000 开始,然后每隔几秒增长大约一百万字节。好消息是,当您终止应用程序时,计数器会恢复正常 (35,237,888),因此最终会进行某种清理。

我附上了 perfmon 泄漏时的屏幕截图perfmon 内存泄漏截图

更新:

我尝试了一些解决方法,例如禁用 DirectoryServer 对象上的缓存,但没有帮助。

FindOne() 命令不会泄漏内存,但我不确定您需要做什么才能使该选项适合您,可能会不断编辑过滤器,在我的 AD 控制器上,只有一个域,所以findall & findone 给出相同的结果。

我还尝试将 10,000 个线程池工作人员排队以创建相同的 DirectorySearcher.FindAll()。它完成得更快,但它仍然泄漏内存,实际上私有字节增加到大约 80MB,而不是“正常”泄漏的 48MB。

所以对于这个问题,如果你可以让 FindOne() 为你工作,你有一个解决方法。祝你好运!

于 2011-05-09T11:06:32.653 回答
2

你试过usingDispose()?来自这里的信息

更新

尝试de.Close();在使用结束之前调用。

抱歉,我实际上没有活动域服务来对此进行测试。

于 2011-04-12T08:10:16.037 回答
0

Found a quick and dirty way around this.

I had a similar issue in my program with memory growth but by changing .GetDirectoryEntry().Properties("cn").Value to

.GetDirectoryEntry().Properties("cn").Value.ToString with a if before hand to make sure .value was not null

i was able to tell GC.Collect to get rid of the temporary value in my foreach. It looks like the .value was actually keeping the object alive rather then allowing it to be collected.

于 2013-09-16T20:07:10.650 回答