32

I have some C# code which is using CSharpCodeProvider.CompileAssemblyFromSource to create an assembly in memory. After the assembly has been garbage collected, my application uses more memory than it did before creating the assembly. My code is in a ASP.NET web app, but I've duplicated this problem in a WinForm. I'm using System.GC.GetTotalMemory(true) and Red Gate ANTS Memory Profiler to measure the growth (about 600 bytes with the sample code).

From the searching I've done, it sounds like the leak comes from the creation of new types, not really from any objects that I'm holding references to. Some of the web pages I've found have mentioned something about AppDomain, but I don't understand. Can someone explain what's going on here and how to fix it?

Here's some sample code for leaking:

private void leak()
{
    CSharpCodeProvider codeProvider = new CSharpCodeProvider();
    CompilerParameters parameters = new CompilerParameters();
    parameters.GenerateInMemory = true;
    parameters.GenerateExecutable = false;

    parameters.ReferencedAssemblies.Add("system.dll");

    string sourceCode = "using System;\r\n";
    sourceCode += "public class HelloWord {\r\n";
    sourceCode += "  public HelloWord() {\r\n";
    sourceCode += "    Console.WriteLine(\"hello world\");\r\n";
    sourceCode += "  }\r\n";
    sourceCode += "}\r\n";

    CompilerResults results = codeProvider.CompileAssemblyFromSource(parameters, sourceCode);
    Assembly assembly = null;
    if (!results.Errors.HasErrors)
    {
        assembly = results.CompiledAssembly;
    }
}

Update 1: This question may be related: Dynamically loading and unloading a a dll generated using CSharpCodeProvider

Update 2: Trying to understand application domains more, I found this: What is an application domain - an explanation for .Net beginners

Update 3: To clarify, I'm looking for a solution that provides the same functionality as the code above (compiling and providing access to generated code) without leaking memory. It looks like the solution will involve creating a new AppDomain and marshaling.

Community
  • 1
  • 1
Nogwater
  • 2,777
  • 3
  • 28
  • 28
  • Very cool question. I'll have an example of how to do this using another AppDomain by the end of today (I'm currently eating lunch, then back to work...). – Charles Dec 04 '09 at 18:55
  • What are you planning to do with the resultant assembly? Is it just for a one time execution or are you going to hold on it? – madaboutcode Dec 05 '09 at 15:54
  • 1
    @LightX I'm going to hold on to it for a while and invoke members from it as needed, but then when a new version of the source code is available, I'll want to dump it and create a new assembly based on the new code. Without the AppDomain fix, this cycle of repeatedly creating assemblies (even though I stop referencing the old versions) causes memory usage to grow. – Nogwater Dec 05 '09 at 16:17
  • There is no "leaks" with this function call. Just allow it to be garbage collected. To do this, you have to unload entire domain into which yiu have loaded the generated assembly – Alan Turing Sep 30 '11 at 03:14

4 Answers4

35

I think I have a working solution. Thanks to everyone for pointing me in the right direction (I hope).

Assemblies can't be unloaded directly, but AppDomains can. I created a helper library that gets loaded in a new AppDomain and is able to compile a new assembly from code. Here's what the class in that helper library looks like:

public class CompilerRunner : MarshalByRefObject
{
    private Assembly assembly = null;

    public void PrintDomain()
    {
        Console.WriteLine("Object is executing in AppDomain \"{0}\"",
            AppDomain.CurrentDomain.FriendlyName);
    }

    public bool Compile(string code)
    {
        CSharpCodeProvider codeProvider = new CSharpCodeProvider();
        CompilerParameters parameters = new CompilerParameters();
        parameters.GenerateInMemory = true;
        parameters.GenerateExecutable = false;
        parameters.ReferencedAssemblies.Add("system.dll");

        CompilerResults results = codeProvider.CompileAssemblyFromSource(parameters, code);
        if (!results.Errors.HasErrors)
        {
            this.assembly = results.CompiledAssembly;
        }
        else
        {
            this.assembly = null;
        }

        return this.assembly != null;
    }

    public object Run(string typeName, string methodName, object[] args)
    {
        Type type = this.assembly.GetType(typeName);
        return type.InvokeMember(methodName, BindingFlags.InvokeMethod, null, assembly, args);
    }

}

It's very basic, but was enough for testing. PrintDomain is there to verify that it does live in my new AppDomain. Compile takes some source code and tries to create an assembly. Run lets us test executing static methods from the given source code.

Here's how I use the helper library:

static void CreateCompileAndRun()
{
    AppDomain domain = AppDomain.CreateDomain("MyDomain");

    CompilerRunner cr = (CompilerRunner)domain.CreateInstanceFromAndUnwrap("CompilerRunner.dll", "AppDomainCompiler.CompilerRunner");            
    cr.Compile("public class Hello { public static string Say() { return \"hello\"; } }");            
    string result = (string)cr.Run("Hello", "Say", new object[0]);

    AppDomain.Unload(domain);
}

It basically creates the domain, creates an instance of my helper class (CompilerRunner), uses it to compile a new assembly (hidden), runs some code from that new assembly, and then unloads the domain to free up memory.

You'll notice the use of MarshalByRefObject and CreateInstanceFromAndUnwrap. These are important for ensuring that the helper library really does live in the new domain.

If anyone notices any problems or has suggestions for improving this, I'd love to hear them.

Nogwater
  • 2,777
  • 3
  • 28
  • 28
  • 2
    Something to note about crossing domain boundaries. If you do not derive the marshalled type from MarshalByRefObject, then marshalling will use copy-by-value semantics. This can result in some very slow execution, as communication across the domain boundary will be very chatty. If you can't have your types deriving from MarshalByRefObject, you might want to create a generic proxy object that you instantiate in the secondary AppDomain that DOES derive from MarshalByRefObject, which mediates communication. If you do implement MarshalByRefObject, beware of object lifetime, and implement ... – jrista Dec 05 '09 at 03:38
  • 1
    an override of *InitializeLifetimeService* such that it keeps the object alive long enough. If you want the object to persist forever, return null from InitializeLifetimeService. – jrista Dec 05 '09 at 03:40
  • @jrista Thanks for the pointer. Is the marshalled type in the above example the Hello class (the one accessed with this.assembly.GetType(typeName))? – Nogwater Dec 05 '09 at 16:27
13

Unloading an assembly is not supported. Some information on why can be found here. Some information on using an AppDomain can be found here.

Omar
  • 16,329
  • 10
  • 48
  • 66
toad
  • 1,351
  • 8
  • 14
  • 4
    Jeremy is correct, there is no way to force .NET to unload an assembly. You're going to have to use an appdomain (which is kind of annoying, but really not all that bad), so you can dump the whole thing when you're done. – David Hay Nov 25 '09 at 20:25
  • So, what I'm hearing is you can't unload an assembly, unless you load it in it's own AppDomain and then dump the whole thing. That sounds like a workable solution, so how do I do that to compile and use dynamic code? – Nogwater Nov 29 '09 at 01:03
8

You may also find this blog entry useful: Using AppDomain to Load and Unload Dynamic Assemblies. It provides some example code demonstrating how create an AppDomain, load a (dynamic) assembly into it, do some work in the new AppDomain then unload it.

Edit: fixed link as pointed out in comments below.

Dan Blanchard
  • 4,014
  • 1
  • 30
  • 26
  • This seems to be the direction that I'll need to know. Can I use this with CompileAssemblyFromSource? Can I create new AppDomains from a web application? – Nogwater Nov 25 '09 at 21:11
  • 3
    You could try and call the CompileAssemblyFromSource in a separate AppDomain, and then unload that domain when you're done. – Mikael Svenson Nov 30 '09 at 21:56
  • 1
    link above should be: http://blogs.claritycon.com/steveholstad/2007/06/28/using-appdomain-to-load-and-unload-dynamic-assemblies/ – Lee Smith Jul 12 '11 at 14:39
  • the blog entry has been deleted along with the blog – JBeurer Mar 10 '14 at 05:22
  • Fixed blog entry link again to http://blogs.claritycon.com/blog/2007/06/using-appdomain-to-load-and-unload-dynamic-assemblies/ – Dan Blanchard Mar 13 '14 at 13:19
2

Can you wait until .NET 4.0? With it you can use expression trees and the DLR to dynamically generate code without the code-gen memory loss issue.

Another option is to use .NET 3.5 with a dynamic language like IronPython.

EDIT: Expression Tree Example

http://www.infoq.com/articles/expression-compiler

Jonathan Allen
  • 68,373
  • 70
  • 259
  • 447
  • I like your suggestions, but unfortunately they won't work for me on this project (we're not using .NET 4 yet, and can't use IronPython (only C#)). If you don't mind, could you flesh out your answer relating to expression trees. It might help someone else. Can they be used to take something stored in a string and turn it into working code that can be unloaded from memory? Thanks. – Nogwater Dec 03 '09 at 22:50
  • I added a link to an article on expression trees. – Jonathan Allen Dec 04 '09 at 08:44
  • 3
    If you dynamically create code with DLR, it will take up space when you run it. Would you be able to free this in .Net 4 without appdomains? The memory leak in the question is not due to the code-gen, but because of loading an assembly in the same appdomain (so not really a leak) which can't be freed. – Mikael Svenson Dec 05 '09 at 10:06
  • 2
    If you use "DynamicMethod", the dynamically generated code is associated with an object that can be garbaged collected. http://msdn.microsoft.com/en-us/magazine/cc163491.aspx – Jonathan Allen Dec 07 '09 at 06:48