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

发布于 2024-11-01 01:34:54 字数 714 浏览 1 评论 0原文

我有一个长时间运行的进程,需要经常对 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(),它们将泄漏内存。我也尝试过不释放,在这种情况下它只会泄漏更多内存。我已经用框架版本 2.0 和 4.0 对此进行了测试,以前有人遇到过这个吗?有什么解决方法吗?

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

I have a long running process that needs to do a lot of queries on Active Directory quite often. For this purpose I have been using the System.DirectoryServices namespace, using the DirectorySearcher and DirectoryEntry classes. I have noticed a memory leak in the application.

It can be reproduced with this code:

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

The documentation for these classes say that they will leak memory if Dispose() is not called. I have tried without dispose as well, it just leaks more memory in that case. I have tested this with both framework versions 2.0 and 4.0 Has anyone run into this before? Are there any workarounds?

Update: I tried running the code in another AppDomain, and it didn't seem to help either.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

梦魇绽荼蘼 2024-11-08 01:34:54

尽管可能很奇怪,但似乎只有当您不对搜索结果执行任何操作时才会发生内存泄漏。按如下方式修改问题中的代码不会泄漏任何内存:

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

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

As strange as it may be, it seems that the memory leak only occurs if you don't do anything with the search results. Modifying the code in the question as follows does not leak any memory:

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

This seems to be caused by the internal searchObject field having lazy initialization , looking at SearchResultCollection with Reflector :

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

The dispose will not close the unmanaged handle unless searchObject is initialized.

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

Calling MoveNext on the ResultsEnumerator calls the SearchObject on the collection thus making sure it is disposed properly as well.

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

The leak in my application was due to some other unmanaged buffer not being released properly and the test I made was misleading. The issue is resolved now.

岁月如刀 2024-11-08 01:34:54

托管包装器实际上并没有泄漏任何内容。如果您不调用 Dispose,未使用的资源仍将在垃圾收集期间被回收。

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

当您将 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 是否可以消除泄漏?

The managed wrapper doesn't really leak anything. If you don't call Dispose unused resources will still be reclaimed during garbage collection.

However, the managed code is a wrapper on top of the COM-based ADSI API and when you create a DirectoryEntry the underlying code will call the ADsOpenObject function. The returned COM object is released when the DirectoryEntry is disposed or during finalization.

There is a documented memory leak when you use the ADsOpenObject API together with a set of credentials and a WinNT provider:

  • This memory leak occurs on all versions of Windows XP, of Windows Server 2003, of Windows Vista, of Windows Server 2008, of Windows 7, and of Windows Server 2008 R2.
  • This memory leak occurs only when you use the WinNT provider together with credentials. The LDAP provider does not leak memory in this manner.

However, the leak is only 8 bytes and and as far as I can see you are using the LDAP provider and not the WinNT provider.

Calling DirectorySearcher.FindAll will perform a search that requires considerable cleanup. This cleanup is done in DirectorySearcher.Dispose. In your code this cleanup is performed in each iteration of the loop and not during garbage collection.

Unless there really is an undocumented memory leak in the LDAP ADSI API the only explanation I can come up with is fragmentation of the unmanaged heap. The ADSI API is implemented by an in-process COM server and each search will probably allocate some memory on the unmanaged heap of your process. If this memory becomes fragmented the heap may have to grow when space is allocated for new searches.

If my hypothesis is true, one option would be to run the searches in a separate AppDomain that then can be reclaimed to unload ADSI and recycle the memory. However, even though memory fragmentation may increase the demand for unmanaged memory I would expect that there would be an upper limit to how much unmanaged memory is required. Unless of course you have a leak.

Also, you could try to play around with the DirectorySearcher.CacheResults property. Does setting it to false remove the leak?

爱人如己 2024-11-08 01:34:54

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

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

编辑:

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

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

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

更新:

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

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

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

因此,对于这个问题,如果您能让 FindOne() 为您工作,您就有了解决方法。祝你好运!

Due to implementation restrictions, the SearchResultCollection class cannot release all of its unmanaged resources when it is garbage collected. To prevent a memory leak, you must call the Dispose method when the SearchResultCollection object is no longer needed.

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

EDIT:

I've been able to repro the apparent leak using perfmon, and adding a counter for Private Bytes on the process name of the test app (Experiments.vshost for me )

the Private Bytes counter will steadily grow while the app is looping, it starts around 40,000,000, and then grows by about a million bytes every few seconds. The good news is the counter drops back to normal (35,237,888) when you terminate the app, so some sort of cleanup is finally occurring then.

I've attached a screen shot of what perfmon looks like when its leakingperfmon screenshot of memory leak

Update:

I've tried a few workarounds, like disabling caching on the DirectoryServer object, and it didn't help.

The FindOne() command doesn't leak memory, but i'm not sure what you would have to do to make that option work for you, probably edit the filter constantly, on my AD controller, there is just a single domain, so findall & findone give the same result.

I also tried queuing 10,000 threadpool workers to make the same DirectorySearcher.FindAll(). It finished alot faster, however it still leaked memory, and actually private bytes went up to about 80MB, instead of just 48MB for the "normal" leak.

So for this issue, if you can make FindOne() work for you, you have a workaround. Good Luck!

农村范ル 2024-11-08 01:34:54

您是否尝试过使用usingDispose()
来自此处的信息

更新

尝试致电de。使用结束前关闭();

抱歉,我实际上没有活动域服务来测试它。

Have you tried using and Dispose()?
Info from here

Update

Try calling de.Close(); before the end of the using.

I don't actually have an Active Domain Service to test this on, sorry.

百变从容 2024-11-08 01:34:54

找到了一个快速而肮脏的方法来解决这个问题。

我在程序中遇到了类似的内存增长问题,但通过将 .GetDirectoryEntry().Properties("cn").Value 更改为

.GetDirectoryEntry().Properties("cn")。 Value.ToString 事先使用 if 来确保 .value 不为 null

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

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文