Memory Leak when using DirectorySearcher.FindAll()

2019-01-14 06:40发布

问题:

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.

回答1:

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.



回答2:

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?



回答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.

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 leaking

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!



回答4:

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.



回答5:

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.