Suppose you’ve got a chunk of code that you need to run as part of your application but you’re concerned that it might bring down your app or introduce a memory leak. Fortunately, the .NET runtime provides an easy mechanism to run arbitrary code in a separate AppDomain. Not only can you isolate all exceptions to that AppDomain, but when the AppDomain unloads you can reclaim all the memory that was consumed.
Here’s a quick walkthrough that demonstrates creating an AppDomain and running some isolated code.
Create a new AppDomain
First we’ll create a new AppDomain based off the information of the currently running AppDomain.AppDomainSetup currentSetup = AppDomain.CurrentDomain.SetupInformation; var info = new AppDomainSetup() { ApplicationBase = currentSetup.ApplicationBase, LoaderOptimization = currentSetup.LoaderOptimization }; var domain = AppDomain.CreateDomain("Widget Domain", null, info);
Unwrap your MarshalByRefObject
Next we’ll create an object in that AppDomain and serialize a handle to it so that we can control the code in the remote AppDomain. It’s important to make sure the object you’re creating inherits from MarshalByRefObject and is marked as serializable. If you forget this step, the entire object will serialize over to the original AppDomain and you lose all isolation.string assemblyName = "AppDomainExperiment"; string typeName = "AppDomainExperiment.MemoryEatingWidget"; IWidget widget = (IWidget)domain.CreateInstanceAndUnwrap(assemblyName, typeName);
Unload the domain
Once we’ve finished with the object, we can broom the entire AppDomain which frees up all resources attached to it. In the example below, I’ve deliberately created a static reference to an object to prevent it from going out of scope.AppDomain.Unload(domain);
Putting it all together
Here’s a sample that shows all the moving parts.namespace AppDomainExperiment { using System; using System.Collections.Generic; using System.IO; using Microsoft.VisualStudio.TestTools.UnitTesting; [Test] public class AppDomainLoadTests { [TestMethod] public void RunMarshalByRefObjectInSeparateAppDomain() { Console.WriteLine("Executing in AppDomain: {0}", AppDomain.CurrentDomain.Id); WriteMemory("Before creating the runner"); using(var runner = new WidgetRunner("AppDomainExperiment", "AppDomainExperiment.MemoryEatingWidget")) { WriteMemory("After creating the runner"); runner.Run(Console.Out); WriteMemory("After executing the runner"); } WriteMemory("After disposing the runner"); } private static void WriteMemory(string where) { GC.Collect(); GC.WaitForPendingFinalizers(); long memory = GC.GetTotalMemory(false); Console.WriteLine("Memory used '{0}': {1}", where, memory.ToString()); } } public interface IWidget { void Run(TextWriter writer); } public class WidgetRunner { private readonly string _assemblyName; private readonly string _typeName; private AppDomain _domain; public WidgetRunner(string assemblyName, string typeName) { _assemblyName = assemblyName; _typeName = typeName; } #region IWidget Members public void Run(TextWriter writer) { AppDomainSetup currentSetup = AppDomain.CurrentDomain.SetupInformation; var info = new AppDomainSetup() { ApplicationBase = currentSetup.ApplicationBase, LoaderOptimization = currentSetup.LoaderOptimization }; _domain = AppDomain.CreateDomain("Widget Domain", null, info); var widget = (IWidget)_domain.CreateInstanceAndUnwrap(_assemblyName, _typeName); if (!(widget is MarshalByRefObject)) { throw new NotSupportedException("Widget must be MarshalBeRefObject"); } widget.Run(writer); } #endregion #region IDisposable Members public void Dispose() { GC.SuppressFinalize(this); AppDomain.Unload(_domain); } #endregion } [Serializable] public class MemoryEatingWidget : MarshalByRefObject, IWidget { private IList<string> _memoryEater; private static IWidget Instance; #region IAppLauncher Members public void Run(TextWriter writer) { writer.WriteLine("Executing in AppDomain: {0}", AppDomain.CurrentDomain.Id); _memoryEater = new List<string>(); // create some really big strings for(int i = 0; i < 100; i++) { var s = new String('c', i*100000); _memoryEater.Add(s); } // THIS SHOULD PREVENT THE MEMORY FROM BEING GC'd Instance = this; } #endregion #region IDisposable Members public void Dispose() { } #endregion } }Running the test shows the following output:
Executing in AppDomain: 2 Memory used 'Before creating the runner': 569060 Memory used 'After creating the runner': 487508 Executing in AppDomain: 3 Memory used 'After executing the runner': 990525340 Memory used 'After disposing the runner': 500340Based on this output, the main take away is that the memory is reclaimed when the AppDomain is unloaded. Why do the numbers not match up in the beginning and end? It’s one of those mysteries of the managed garbage collector, it reminds me of my favorite Norm McDonald joke from SNL:
“Who are safer drivers? Men, or women?? Well, according to a new survey, 55% of adults feel that women are most responsible for minor fender-benders, while 78% blame men for most fatal crashes. Please note that the percentages in these pie graphs do not add up to 100% because the math was done by a woman. [Crowd groans.] For those of you hissing at that joke, it should be noted that that joke was written by a woman. So, now you don't know what the hell to do, do you? [Laughter] Nah, I'm just kidding, we don't hire women”Happy Coding.