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

时间:2022-08-17 08:58:30

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.

我有一个长期运行的进程,它需要经常在Active Directory上执行大量查询。为此,我一直在使用这个系统。DirectoryServices命名空间,使用DirectorySearcher和DirectoryEntry类。我注意到应用程序中出现了内存泄漏。

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?

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

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

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

5 个解决方案

#1


14  

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 :

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

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.

除非初始化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;
        }
    ..
   }
}

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

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

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.

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

#2


6  

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

托管包装实际上不会泄漏任何东西。如果不调用Dispose,则在垃圾收集期间仍然会回收未使用的资源。

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.

但是,托管代码是基于com的ADSI API的包装器,当您创建一个DirectoryEntry时,底层代码将调用ADsOpenObject函数。返回的COM对象在处理DirectoryEntry或结束期间释放。

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

当您使用ADsOpenObject API以及一组凭据和WinNT提供程序时,会有一个有文档记录的内存泄漏:

  • 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.
  • 这种内存泄漏发生在Windows XP、Windows Server 2003、Windows Vista、Windows Server 2008、Windows 7和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.
  • 只有在使用WinNT提供程序和凭据时,才会发生此内存泄漏。LDAP提供程序不会以这种方式泄漏内存。

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.

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

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.

调用DirectorySearcher。FindAll将执行需要大量清理的搜索。这种清理是在directorysearcher.com . dispose中完成的。在代码中,这种清理是在循环的每次迭代中执行的,而不是在垃圾收集期间执行的。

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.

除非LDAP ADSI API中确实存在未文档化的内存泄漏,否则我唯一能想到的解释就是非托管堆的碎片化。ADSI API由一个进程内的COM服务器实现,每个搜索可能会在进程的非托管堆上分配一些内存。如果这个内存变得碎片化,那么当为新的搜索分配空间时,堆可能不得不增加。

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.

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

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

此外,您可以尝试使用DirectorySearcher。CacheResults财产。设置为false是否会删除泄漏?

#3


3  

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.

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

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

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 )

我可以使用perfmon对明显的泄漏进行修改,并在测试应用程序的进程名称中添加一个私有字节计数器(实验)。对我来说vshost)

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.

当应用程序循环时,私有字节计数器将稳步增长,它开始大约4000万,然后每隔几秒钟增长大约100万字节。好消息是,当你终止应用程序时,计数器会恢复到正常状态(35,237,888),因此最终会进行一些清理工作。

I've attached a screen shot of what perfmon looks like when its leaking使用DirectorySearcher.FindAll()时内存泄漏

我附上了一个屏幕截图,显示香水泄露时的样子

Update:

更新:

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

我尝试了一些变通方法,比如在DirectoryServer对象上禁用缓存,但这并没有帮助。

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.

FindOne()命令不会泄漏内存,但是我不确定要让这个选项为您工作需要做什么,可能要经常编辑过滤器,在我的AD控制器上,只有一个域,所以findall & FindOne给出相同的结果。

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.

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

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

因此,对于这个问题,如果您可以让FindOne()为您工作,那么您就有了一个解决方案。好运!

#4


2  

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

您试过使用和Dispose()吗?信息从这里

Update

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

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

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

我实际上没有一个有效的域服务来测试这个,抱歉。

#5


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”)。价值

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

.GetDirectoryEntry(). properties(cn)value。使用if字符串确保.value不是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.

我可以告诉GC。收集以摆脱我的暂时价值在我的每个。看起来。value实际上保持了对象的活性,而不是允许它被收集。

#1


14  

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 :

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

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.

除非初始化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;
        }
    ..
   }
}

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

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

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.

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

#2


6  

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

托管包装实际上不会泄漏任何东西。如果不调用Dispose,则在垃圾收集期间仍然会回收未使用的资源。

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.

但是,托管代码是基于com的ADSI API的包装器,当您创建一个DirectoryEntry时,底层代码将调用ADsOpenObject函数。返回的COM对象在处理DirectoryEntry或结束期间释放。

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

当您使用ADsOpenObject API以及一组凭据和WinNT提供程序时,会有一个有文档记录的内存泄漏:

  • 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.
  • 这种内存泄漏发生在Windows XP、Windows Server 2003、Windows Vista、Windows Server 2008、Windows 7和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.
  • 只有在使用WinNT提供程序和凭据时,才会发生此内存泄漏。LDAP提供程序不会以这种方式泄漏内存。

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.

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

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.

调用DirectorySearcher。FindAll将执行需要大量清理的搜索。这种清理是在directorysearcher.com . dispose中完成的。在代码中,这种清理是在循环的每次迭代中执行的,而不是在垃圾收集期间执行的。

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.

除非LDAP ADSI API中确实存在未文档化的内存泄漏,否则我唯一能想到的解释就是非托管堆的碎片化。ADSI API由一个进程内的COM服务器实现,每个搜索可能会在进程的非托管堆上分配一些内存。如果这个内存变得碎片化,那么当为新的搜索分配空间时,堆可能不得不增加。

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.

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

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

此外,您可以尝试使用DirectorySearcher。CacheResults财产。设置为false是否会删除泄漏?

#3


3  

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.

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

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

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 )

我可以使用perfmon对明显的泄漏进行修改,并在测试应用程序的进程名称中添加一个私有字节计数器(实验)。对我来说vshost)

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.

当应用程序循环时,私有字节计数器将稳步增长,它开始大约4000万,然后每隔几秒钟增长大约100万字节。好消息是,当你终止应用程序时,计数器会恢复到正常状态(35,237,888),因此最终会进行一些清理工作。

I've attached a screen shot of what perfmon looks like when its leaking使用DirectorySearcher.FindAll()时内存泄漏

我附上了一个屏幕截图,显示香水泄露时的样子

Update:

更新:

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

我尝试了一些变通方法,比如在DirectoryServer对象上禁用缓存,但这并没有帮助。

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.

FindOne()命令不会泄漏内存,但是我不确定要让这个选项为您工作需要做什么,可能要经常编辑过滤器,在我的AD控制器上,只有一个域,所以findall & FindOne给出相同的结果。

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.

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

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

因此,对于这个问题,如果您可以让FindOne()为您工作,那么您就有了一个解决方案。好运!

#4


2  

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

您试过使用和Dispose()吗?信息从这里

Update

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

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

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

我实际上没有一个有效的域服务来测试这个,抱歉。

#5


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”)。价值

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

.GetDirectoryEntry(). properties(cn)value。使用if字符串确保.value不是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.

我可以告诉GC。收集以摆脱我的暂时价值在我的每个。看起来。value实际上保持了对象的活性,而不是允许它被收集。