使用DirectorySearcher.FindAll()时出现内存泄漏问题

26

我有一个需要频繁在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中运行代码,但似乎也没有帮助。


那只是为了说明问题,在实际应用中显然不是这样的。 - Can Gencer
1
你如何“注意到应用程序中的内存泄漏”? - Cheng Chen
不幸的是,在此处使用 while(true) 会防止 DirectorySearcher 和 DirectoryEntry 被正确处理。你可能想将其放在 "using" 的第一层,然后检查发生了什么。 - Larry
@Can Gencer - 你尝试过使用WinDebug和托管扩展来检查内存使用情况吗?我已经成功地使用它来解决了内存泄漏问题。 - Haplo
1
@Andrew 这就是实际泄漏内存的代码。你可以运行它,它会泄漏内存。如果我能防止上面的代码泄漏,那么我就可以在我的真实代码中使用它,该代码有很多与此问题无关的额外代码,这就是为什么我跳过它的原因。 - Can Gencer
显示剩余9条评论
5个回答

18

虽然可能有些奇怪,但如果您不对搜索结果进行任何操作,似乎只会发生内存泄漏。 将问题中的代码修改如下不会泄漏任何内存:

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

我的应用程序泄漏问题是由于其他未受管控的缓冲区没有正确释放,而我所做的测试是误导性的。现在该问题已解决。


从您的解释来看,这似乎是.NET库中的一个错误。您应该考虑提交Connect bug:http://connect.microsoft.com/VisualStudio - Martin Liversage
使用枚举器不就相当于在 SearchResultCollection 上执行 foreach 循环吗? - Abhijeet Patel
@AbhijeetPatel 是的,没有区别。只是为了说明这个 bug。 - Can Gencer

8
托管包装器实际上并不会泄漏任何东西。如果您不调用Dispose,未使用的资源仍将在垃圾回收期间被回收。
但是,托管代码是基于COM的ADSI API的包装器,当您创建DirectoryEntry时,底层代码将调用ADsOpenObject函数。返回的COM对象在DirectoryEntry被处理或在终结期间释放。 当您与一组凭据和WinNT提供程序一起使用ADsOpenObject API时存在已记录的内存泄漏
  • 这种内存泄漏发生在所有版本的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中运行搜索,然后可以回收以卸载ADSI并回收内存。但是,即使内存碎片化可能会增加对未托管内存的需求,我仍然希望有一个上限,以便了解需要多少未托管内存。除非当然您有泄漏。
此外,您可以尝试玩弄DirectorySearcher.CacheResults属性。将其设置为false是否会消除泄漏?

不错的发现!就是这样。每次很少,但随着时间的推移会逐渐增加,肯定有漏洞存在,我每次都创建了一个新的 DirectoryEntry (带有凭证)。 - Can Gencer
@Martin,重新阅读链接后,我并没有使用WinNNT提供程序,而是使用LDAP提供程序。此外,泄漏似乎发生在搜索时,而不是使用新的DirectoryEntry时(如果我将循环更改为这样)。看来我有点过于激动了。但可能COM API中还存在其他内存泄漏问题。 - Can Gencer
@Ian Ringrose:卸载一个AppDomain也会卸载在该AppDomain中加载的任何COM DLL。但是,我不太确定该DLL分配的任何非托管堆内存会发生什么情况。很有可能它依赖于C++库进行内存分配,而这个库及其相关资源在AppDomain卸载后仍可能加载到进程中。显然,当它卸载时,COM DLL将delete/free内存(除非它有泄漏),但该段仍可能映射到进程中。 - Martin Liversage
@Martin,从我作为C++程序员的角度来看,我记得大多数delete/free实现不会将任何内存返回给操作系统,并且无法很好地处理碎片。你还必须希望另一个使用相同C/C++运行时的COM dll没有被不同的应用程序域加载-对我来说有太多的“如果”了! - Ian Ringrose
@Martin,谜团已经解开了。如果您不对搜索结果进行任何操作,似乎会泄漏内存。请查看我的答案。 - Can Gencer
显示剩余3条评论

3
由于实现限制,SearchResultCollection类在垃圾回收时无法释放其所有未托管的资源。为了防止内存泄漏,在不再需要SearchResultCollection对象时,必须调用Dispose方法。 http://msdn.microsoft.com/en-us/library/system.directoryservices.directorysearcher.findall.aspx 编辑:
我已经能够使用perfmon重现明显的泄漏,并在测试应用程序(我的是Experiments.vshost)的进程名称上添加了Private Bytes计数器
当应用程序循环时,Private Bytes计数器将稳步增长,它从大约40,000,000开始,然后每隔几秒钟就会增加约一百万字节。好消息是当您终止应用程序时,计数器会恢复正常(35,237,888),因此最终发生了某种清理。
我附上了一个屏幕截图,显示了它泄漏时的perfmon情况perfmon screenshot of memory leak 更新:
我尝试了一些解决方法,例如禁用DirectoryServer对象上的缓存,但没有帮助。
FindOne()命令不会泄漏内存,但我不确定您需要做什么才能使该选项适用于您,可能需要不断编辑过滤器,在我的AD控制器上,只有一个域,因此findall和findone给出相同的结果。
我还尝试了将10,000个线程池工作程序排队以执行相同的DirectorySearcher.FindAll()。它完成得更快,但仍然泄漏内存,并且实际上私有字节增加到约80MB,而不仅仅是48MB的“正常”泄漏。
因此,对于此问题,如果您可以让FindOne()适用于您,则可以解决问题。祝你好运!

3
“using”作用域等同于调用“dispose”,而且多次调用“dispose”也不会产生任何影响。 - Can Gencer
希望你是对的,MSDN文档似乎表明需要显式调用Dispose方法,因为该集合具有非托管资源。我今天早上会测试一下。 - Ivan Bohannon
似乎手动释放或使用对泄漏没有影响。我会看看能否确定正在泄漏的内存类型。 - Ivan Bohannon
我还验证了一下,如果你注释掉对FindAll()的调用,泄漏就会停止。当然此时代码是无用的 :) - Ivan Bohannon

2

你尝试过使用usingDispose()吗? 来自这里的信息。

更新

using结束之前调用de.Close();

很抱歉我实际上没有一个活动域服务来测试这个。


1
将Dispose方法放在using块中实际上会调用Dispose两次...而且似乎没有任何效果(如预期)。我还使用.NET Reflector查看了使用这些类的Dispose代码,它们都检查对象是否已被处理,否则不执行任何操作。 - Can Gencer
检查您的源代码中最低级别使用的内容会很有趣:链接中的示例显示了其他需要Dispose()的AD对象,例如Properties对象。 - Larry
Properties对象没有实现IDisposable接口,只有DirectorySearcher、DirectoryEntry和SearchResultCollection实现了。 - Can Gencer
关闭没有任何影响。我使用Reflector进行了验证。Close()和Dispose()都调用Unbind()函数,本质上是相同的。 - Can Gencer
有没有一个我们可以连接到的测试活动目录服务,以便在网上测试代码?或者在本地设置简单吗? - William Mioch
据我所知没有这样的功能。您可以从微软下载Server 2008 R2 180天试用虚拟机,并将“域控制器”角色添加到服务器上。http://www.microsoft.com/windowsserver2008/en/us/trial-software.aspx - Can Gencer

0
找到了一个快速而不太干净的方法来解决这个问题。
我在我的程序中遇到了类似的内存增长问题,但通过将.GetDirectoryEntry().Properties("cn").Value更改为

.GetDirectoryEntry().Properties("cn").Value.ToString并在前面加上一个if以确保.value不为空

我能够告诉GC.Collect放弃我的foreach中的临时值。看起来.value实际上使对象保持活动状态,而不是允许其被收集。


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