How to get around the memory leak in the .NET Webbrowser control?

.NetMemoryMemory LeaksBrowserWebbrowser Control

.Net Problem Overview


This is a widely-known, old issue with the .NET Webbrowser control.

Summary: Having the .NET webbrowser control Navigate to a page increases memory usage that is never freed.

Reproduce the memory leak: Add a WebBrowser control to a form. Use it to Navigate to whatever pages you'd like. about:blank works, scrolling down on Google Images until your usage is 100MB+ and then browsing elsewhere to notice barely any of that memory is freed is a more dramatic demonstration.

My current requirements for an application include running it for long periods of time, displaying a limited IE7 browser window. Running IE7 itself with some bastard setup of hooks, BHOs and group policies isn't desired either, although that's looking like the fallback at this time. Embedding a browser into a Windows Forms application is. Using a different browser base is not an available option for me. IE7 is required.

Previous threads and articles relating to this known memory leak:

Often-proposed fixes that DO NOT WORK:

  • Going to different pages does not matter. about:blank triggers the leak. It does not require a page to have javascript, or any other extra technology.
  • Using different versions of Internet Explorer does not matter. 7, 8, and 9 all exhibit the same symptoms, and as far as I've heard, all versions have the same memory leak in the control.
  • Dispose()ing of the control does not help.
  • Garbage Collecting does not help. (In fact, research I've done into this indicates the leak is in unmanaged COM code that the Webbrowswer control wraps.)
  • Minimizing and setting process available memory to -1, -1(SetProcessWorkingSetSize() or simimlar.) only reduces physical memory usage, has no impact on virtual memory.
  • Calling WebBrowser.Stop() is not a solution, and breaks functionality for using anything but static webpages, without doing more than just minimizing the leak slightly.
  • Forcing a wait for a document to load completely before Navigating to another also doesn't help.
  • Loading the control in a separate appDomain does not fix the issue. (I've not done this myself, but research shows others having no success with this route.)
  • Using a different wrapper such as csexwb2 does not help, as this also suffers from the same issue.
  • Clearing the Temporary Internet Files cache does nothing. The problem is in active memory, not on disk.

Memory is cleared when the entire application is closed and restarted.

I'm willing to write my own browser control in COM or Windows API directly, if that's a for-sure fix to the problem. Of course, I would prefer a less complicated fix; I'd rather avoid going down to lower levels to do things, because I don't want to be reinventing the wheel in terms of supported features of a browser. Letalone duplicating IE7 features and nonstandard behaviours in a roll-your-own style browser.

Help?

.Net Solutions


Solution 1 - .Net

This leak appears to be a leak in unmanaged memory, so nothing you do in your process is going to reclaim that memory. From your post I can see that you've tried avoid the leak quite extensively and without success.

I would suggest a different approach if feasible. Create a separate application that uses web browser control and start it from your application. Use the method described here to embed newly created application within your own existing application. Communicate with that application using WCF or .NET remoting. Restart the child process from time to time to prevent it from taking to much memory.

This of course is quite complicated solution and the restart process could probably look ugly. You might maybe resort to restarting the whole browser application every time user navigates to another page.

Solution 2 - .Net

I took the udione's code (it worked for me, thanks!) and changed two small things:

  1. IKeyboardInputSite is a public interface and has method Unregister(), so we don't need to use reflection after we received a reference to _keyboardInputSinkChildren collection.

  2. As view not always has a direct reference to its window class (especially in MVVM) I added a method GetWindowElement(DependencyObject element) which returns the required reference by traversing through visual tree.

Thanks, udione

public void Dispose()
{
	_browser.Dispose();

	var window = GetWindowElement(_browser);

	if (window == null)
		return;

	var field = typeof(Window).GetField("_swh", BindingFlags.NonPublic | BindingFlags.Instance);

	var valueSwh = field.GetValue(window);
	var valueSourceWindow = valueSwh.GetType().GetField("_sourceWindow", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(valueSwh);
	var valuekeyboardInput = valueSourceWindow.GetType().GetField("_keyboardInputSinkChildren", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(valueSourceWindow);

	var inputSites = valuekeyboardInput as IEnumerable<IKeyboardInputSite>;

	if (inputSites == null)
		return;

	var currentSite = inputSites.FirstOrDefault(s => ReferenceEquals(s.Sink, _browser));

	if (currentSite != null)
		currentSite.Unregister();
}

private static Window GetWindowElement(DependencyObject element)
{
	while (element != null && !(element is Window))
	{
		element = VisualTreeHelper.GetParent(element);
	}

	return element as Window;
}

Thank you all!

Solution 3 - .Net

There is a way to do clear memory leaks by using reflection and removing references from private fields on mainForm. This is not a good solution but for desperate people here is the code:

//dispose to clear most of the references
this.webbrowser.Dispose();
BindingOperations.ClearAllBindings(this.webbrowser);

//using reflection to remove one reference that was not removed with the dispose 
var field = typeof(System.Windows.Window).GetField("_swh", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

var valueSwh = field.GetValue(mainwindow);

var valueSourceWindow = valueSwh.GetType().GetField("_sourceWindow", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(valueSwh);

var valuekeyboardInput = valueSourceWindow.GetType().GetField("_keyboardInputSinkChildren", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(valueSourceWindow);

System.Collections.IList ilist = valuekeyboardInput as System.Collections.IList;

lock(ilist)
{
    for (int i = ilist.Count-1; i >= 0; i--)
    {
        var entry = ilist[i];
        var sinkObject = entry.GetType().GetField("_sink", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        if (object.ReferenceEquals(sinkObject.GetValue(entry), this.webbrowser.webBrowser))
        {
            ilist.Remove(entry);
        }
    }
} 

Solution 4 - .Net

Having battled this exact Out of Memory issue from different directions (Win32 WorkingSet / COM SHDocVw interfaces, etc.) with the WPF WebBrowser component, I discovered the problem for us was jqGrid plugin holding onto unmanaged resources in the IE ActiveXHost and not releasing them after calling WebBrowser.Dispose(). This issue is often created by Javascript that is not behaving properly. The odd thing is that the Javascript works fine in regular IE - just not from within the WebBrowser control. I surmise that the garbage collection is different between the two integration points as IE can never really be closed.

One thing I'd suggest if you are authoring the source pages is to remove all JS components and slowly adding them back in. Once you identify the offending JS plugin (like we did) - it should be easy to remedy the problem. In our case we just used $("#jqgrid").jqGrid('GridDestroy') to properly remove the events and associated DOM elements it created. This took care of the issue for us by invoking this when the browser is closed via WebBrowser.InvokeScript.

If you don't have the ability to modify the source pages you navigate to - you would have to inject some JS into the page to clean up the DOM events and elements that are leaking memory. It would be nice if Microsoft found a resolution to this, but for now we are left probing for JS plugins that need cleansed.

Solution 5 - .Net

I think this question has gone unanswered for a long time now. So many threads with the same question but not conclusive answer.

I have found a work around for this issue and wanted to share with you all who are still facing this issue.

step1: Create a new form say form2 and add a web-browser control on it. step2: In the form1 where you have your webbrowser control, just remove it. step3: Now, go to Form2 and make the access modifier for this webbrowser control to be public so that it can be accessed in Form1 step4: Create a panel in form1 and create object of form2 and add this into panel. Form2 frm = new Form2 (); frm.TopLevel = false; frm.Show(); panel1.Controls.Add(frm); step5: Call the below code at regular intervals frm.Controls.Remove(frm.webBrowser1); frm.Dispose();

Thats it. Now when you run it, you can see that webbrowser control loaded and it will get disposed at regular intervals and there is no more hanging of the application.

You can add the below code to make it more efficient.

        IntPtr pHandle = GetCurrentProcess();
        SetProcessWorkingSetSize(pHandle, -1, -1);


        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

Solution 6 - .Net

The below solution worked for me:

Protected Sub disposeBrowers()
    If debug Then debugTrace()
    If Me.InvokeRequired Then
        Me.Invoke(New simple(AddressOf disposeBrowers))
    Else
        Dim webCliffNavigate As String = webCliff.Url.AbsoluteUri
        Me.DollarLogoutSub()
        If dollarLoggedIn Then
            Exit Sub
        End If

        'Dim webdollarNavigate As String = webDollar.Url.AbsoluteUri
        Me.splContainerMain.SuspendLayout()
        Me.splCliffDwellers.Panel2.Controls.Remove(webCliff)
        Me.splDollars.Panel2.Controls.Remove(webDollar)
        RemoveHandler webCliff.DocumentCompleted, AddressOf webCliff_DocumentCompleted
        RemoveHandler webDollar.DocumentCompleted, AddressOf webDollar_DocumentCompleted
        RemoveHandler webCliff.GotFocus, AddressOf setDisposeEvent
        RemoveHandler webCliff.LostFocus, AddressOf setDisposeEvent
        RemoveHandler webDollar.GotFocus, AddressOf setDisposeEvent
        RemoveHandler webDollar.LostFocus, AddressOf setDisposeEvent
        webCliff.Stop()
        webDollar.Stop()

        Dim tmpWeb As SHDocVw.WebBrowser = webCliff.ActiveXInstance
        System.Runtime.InteropServices.Marshal.ReleaseComObject(tmpWeb)
        webCliff.Dispose()

        tmpWeb = webDollar.ActiveXInstance
        System.Runtime.InteropServices.Marshal.ReleaseComObject(tmpWeb)
        webDollar.Dispose()
        tmpWeb = Nothing

        webCliff = Nothing
        webDollar = Nothing
        GC.AddMemoryPressure(50000)
        GC.Collect()
        GC.WaitForPendingFinalizers()
        GC.Collect()
        GC.WaitForFullGCComplete()
        GC.Collect()
        GC.RemoveMemoryPressure(50000)
        webCliff = New WebBrowser()
        webDollar = New WebBrowser()
        webCliff.CausesValidation = False
        webCliff.Dock = DockStyle.Fill
        webDollar.CausesValidation = webCliff.CausesValidation
        webDollar.Dock = webCliff.Dock
        webDollar.ScriptErrorsSuppressed = True
        webDollar.Visible = True
        webCliff.Visible = True
        Me.splCliffDwellers.Panel2.Controls.Add(webCliff)
        Me.splDollars.Panel2.Controls.Add(webDollar)
        Me.splContainerMain.ResumeLayout()

        'AddHandler webCliff.DocumentCompleted, AddressOf webCliff_DocumentCompleted
        'AddHandler webDollar.DocumentCompleted, AddressOf webDollar_DocumentCompleted
        'AddHandler webCliff.GotFocus, AddressOf setDisposeEvent
        'AddHandler webCliff.LostFocus, AddressOf setDisposeEvent
        'AddHandler webDollar.GotFocus, AddressOf setDisposeEvent
        'AddHandler webDollar.LostFocus, AddressOf setDisposeEvent

        webCliff.Navigate(webCliffNavigate)
        disposeOfBrowsers = Now.AddMinutes(20)
    End If
End Sub

Best of luck, Layla

Solution 7 - .Net

Believe its more of an .Net Framework issue rather than Web Browser control. Hooking to the navigated event of the browser with a handler which will dispose the browser and then navigating to about:blank will be a good workaround. For example:

private void RemoveButton_Click(object sender, RoutedEventArgs e)
{
  var browser = (WebBrowser) _stackPanel.Children[_stackPanel.Children.Count - 1];
  _stackPanel.Children.RemoveAt(_stackPanel.Children.Count-1);

  NavigatedEventHandler dispose = null;
  dispose = (o, args) =>
  {
    browser.Navigated -= dispose;
    browser.Dispose();
  };
  browser.Navigated += dispose;
  browser.Navigate(new Uri("about:blank"));
}

Solution 8 - .Net

THIS WORKS, especially if yours is a Winforms application! Handles will be released immediately after closing the webbrowser.

USE CODE:

using system.windows.forms;
[DllImport("KERNEL32.DLL", EntryPoint = "SetProcessWorkingSetSize", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
internal static extern bool SetProcessWorkingSetSize(IntPtr pProcess, int dwMinimumWorkingSetSize, int dwMaximumWorkingSetSize);

[DllImport("KERNEL32.DLL", EntryPoint = "GetCurrentProcess", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
internal static extern IntPtr GetCurrentProcess();

// **Release handles invoking the above**
IntPtr pHandle = GetCurrentProcess();
SetProcessWorkingSetSize(pHandle, -1, -1);

Happy coding!

Solution 9 - .Net

Try this solution, i know its not ideal. Paste the code after each page load

System.Diagnostics.Process loProcess = System.Diagnostics.Process.GetCurrentProcess();
try
{
     loProcess.MaxWorkingSet = (IntPtr)((int)loProcess.MaxWorkingSet - 1);
     loProcess.MinWorkingSet = (IntPtr)((int)loProcess.MinWorkingSet - 1);
}
catch (System.Exception)
{

     loProcess.MaxWorkingSet = (IntPtr)((int)1413120);
     loProcess.MinWorkingSet = (IntPtr)((int)204800);
}
           

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAzuvectorView Question on Stackoverflow
Solution 1 - .NetIvanView Answer on Stackoverflow
Solution 2 - .NetSergey KostrukovView Answer on Stackoverflow
Solution 3 - .NetudioneView Answer on Stackoverflow
Solution 4 - .NetSliverNinja - MSFTView Answer on Stackoverflow
Solution 5 - .Netnh53019View Answer on Stackoverflow
Solution 6 - .NetLaylaView Answer on Stackoverflow
Solution 7 - .NetAsish SinhaView Answer on Stackoverflow
Solution 8 - .NetsanrnsView Answer on Stackoverflow
Solution 9 - .NetStefan KrugerView Answer on Stackoverflow