我该如何防止CompileAssemblyFromSource泄漏内存?

32
我有一些C#代码,使用CSharpCodeProvider.CompileAssemblyFromSource在内存中创建一个程序集。在程序集被垃圾回收后,我的应用程序比创建程序集之前占用更多的内存。我的代码在ASP.NET Web应用程序中,但我已经在WinForm中复制了这个问题。我使用System.GC.GetTotalMemory(true)和Red Gate ANTS Memory Profiler来测量增长(使用示例代码大约增加了600字节)。
从我所做的搜索中看来,泄漏似乎来自于新类型的创建,而不是我持有引用的任何对象。我找到的一些网页提到了AppDomain的某些内容,但我不理解。请问有人能够解释一下这里发生了什么以及如何修复它吗?
以下是一些泄漏的示例代码:
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;
    }
}

更新 1:这个问题可能与以下问题有关:使用CSharpCodeProvider动态加载和卸载生成的dll

更新 2:为了更好地理解应用程序域,我找到了这篇文章:什么是应用程序域——面向.NET初学者的解释

更新 3:澄清一下,我正在寻找一种提供与上述代码相同功能(编译并提供对生成代码的访问),但不会泄漏内存的解决方案。看起来解决方案将涉及创建一个新的AppDomain并进行封送。


非常酷的问题。我会在今天结束之前提供一个使用另一个AppDomain的示例(我现在正在吃午饭,然后回到工作中...)。 - Charles
你打算如何处理生成的汇编代码?它只是用于一次性执行还是你会保留它? - madaboutcode
1
@LightX 我打算暂时保留它,并根据需要从中调用成员,但是当源代码的新版本可用时,我将想要卸载它并基于新代码创建一个新的程序集。如果没有AppDomain修复,即使我停止引用旧版本,这种反复创建程序集的循环也会导致内存使用量增加。 - Nogwater
这个函数调用没有“泄漏”。只需允许它被垃圾回收即可。为此,您必须卸载已加载生成的程序集的整个域。 - Alan Turing
4个回答

35

我认为我已经有了一个可行的解决方案。感谢所有指引我的人(希望是正确的方向)。

程序集不能直接卸载,但应用程序域可以。我创建了一个辅助库,并在新的应用程序域中加载,能够从代码编译一个新的程序集。这是那个辅助库中类的样子:

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);
    }

}

这很基础,但足以用于测试。PrintDomain用于验证它是否存在于我的新AppDomain中。Compile接受一些源代码并尝试创建一个程序集。Run让我们测试从给定的源代码执行静态方法。

下面是我如何使用这个辅助库:

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);
}

这段代码基本上是创建域,实例化我的辅助类(CompilerRunner),使用它来编译一个新的程序集(隐藏),从该新程序集中运行一些代码,然后取消加载该域以释放内存。

你会注意到使用了MarshalByRefObject和CreateInstanceFromAndUnwrap。这些对于确保辅助库真正存在于新域中非常重要。

如果有人注意到任何问题或有改进建议,请告诉我。


2
关于跨域边界的一些注意事项。如果您没有从MarshalByRefObject派生序列化类型,那么序列化将使用按值复制语义。这可能导致执行非常慢,因为跨域边界的通信将非常烦琐。如果您不能让您的类型从MarshalByRefObject派生,您可能需要创建一个泛型代理对象,在次要AppDomain中实例化该对象,该对象从MarshalByRefObject派生,并协调通信。如果您实现了MarshalByRefObject,请注意对象的生命周期,并实现... - jrista
1
重写 InitializeLifetimeService 方法,使其保持对象的存活时间足够长。如果您希望对象永久存在,请从 InitializeLifetimeService 返回 null。 - jrista
@jrista 感谢您提供的指针。在上述示例中,序列化类型是否为 Hello 类(即通过 this.assembly.GetType(typeName) 访问的类)? - Nogwater

13

不支持卸载程序集。关于此的一些信息可以在这里找到。 有关使用AppDomain的一些信息可以在这里找到。


4
Jeremy是正确的,没有办法强制.NET卸载程序集。你需要使用一个应用程序域(有点麻烦,但真的并不那么糟糕),这样在完成后就可以将整个东西卸载掉。 - David Hay
所以,我的理解是你不能卸载一个程序集,除非你在它自己的AppDomain中加载它,然后将整个东西卸载。这听起来像是一个可行的解决方案,那么我该如何编译和使用动态代码呢? - Nogwater

8

您可能会发现这篇博客文章很有用:使用AppDomain加载和卸载动态程序集。它提供了一些示例代码,演示如何创建一个AppDomain,将(动态)程序集加载到其中,在新的AppDomain中进行一些工作,然后卸载它。

编辑:根据下面的评论修复了链接。


这似乎是我需要了解的方向。我能在CompileAssemblyFromSource中使用它吗?我能从Web应用程序中创建新的AppDomains吗? - Nogwater
3
你可以尝试在一个单独的 AppDomain 中调用 CompileAssemblyFromSource 方法,完成后再卸载该域。 - Mikael Svenson
1
链接应为:http://blogs.claritycon.com/steveholstad/2007/06/28/using-appdomain-to-load-and-unload-dynamic-assemblies/。 - Lee Smith
博客文章已经被删除,连同博客一起删除了。 - JBeurer
修复了博客链接,链接地址为http://blogs.claritycon.com/blog/2007/06/using-appdomain-to-load-and-unload-dynamic-assemblies/。 - Dan Blanchard

2

你能等到.NET 4.0吗?使用它,您可以使用表达式树和DLR动态生成代码,而无需担心代码生成内存损失问题。

另一个选择是使用.NET 3.5和像IronPython这样的动态语言。

编辑:表达式树示例

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


我喜欢你的建议,但不幸的是它们在这个项目上对我没用(我们还没有使用.NET 4,也不能使用IronPython(只能使用C#))。如果您不介意,能否详细说明一下关于表达式树的答案。这可能会帮助其他人。它们是否可以用于将存储在字符串中的内容转换为可卸载内存的工作代码?谢谢。 - Nogwater
我添加了一个关于表达式树的文章链接。 - Jonathan Allen
3
如果您使用DLR动态创建代码,则在运行时会占用空间。您是否能够在不使用应用程序域的情况下在.Net 4中释放它?本问题中的内存泄漏不是由于代码生成,而是因为在同一应用程序域加载程序集(因此并非真正的泄漏),无法释放该程序集。 - Mikael Svenson
2
如果您使用“DynamicMethod”,则动态生成的代码与可以进行垃圾回收的对象相关联。http://msdn.microsoft.com/en-us/magazine/cc163491.aspx - Jonathan Allen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接