如何防止 CompileAssemblyFromSource 泄漏内存?

Posted

技术标签:

【中文标题】如何防止 CompileAssemblyFromSource 泄漏内存?【英文标题】:How can I prevent CompileAssemblyFromSource from leaking memory? 【发布时间】:2010-12-20 10:33:30 【问题描述】:

我有一些 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:这个问题可能相关:Dynamically loading and unloading a a dll generated using CSharpCodeProvider

更新 2: 试图更多地了解应用程序域,我发现了这个:What is an application domain - an explanation for .Net beginners

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

【问题讨论】:

非常酷的问题。我将在今天结束之前提供一个如何使用另一个 AppDomain 执行此操作的示例(我目前正在吃午饭,然后回去工作......)。 您打算如何处理生成的程序集?是一次性执行还是要坚持执行? @LightX 我会坚持一段时间并根据需要从中调用成员,但是当有新版本的源代码可用时,我会想转储它并根据新代码创建一个新程序集。如果没有 AppDomain 修复程序,这种重复创建程序集的循环(即使我停止引用旧版本)会导致内存使用量增加。 这个函数调用没有“泄漏”。只允许它被垃圾收集。为此,您必须卸载 yiu 已将生成的程序集加载到其中的整个域 【参考方案1】:

您可能还会发现此博客条目很有用:Using AppDomain to Load and Unload Dynamic Assemblies. 它提供了一些示例代码,演示了如何创建 AppDomain、将(动态)程序集加载到其中、在新的 AppDomain 中做一些工作然后卸载它。

编辑:固定链接,如下面的 cmets 所示。

【讨论】:

这似乎是我需要知道的方向。我可以将它与 CompileAssemblyFromSource 一起使用吗?我可以从 Web 应用程序创建新的 AppDomain 吗? 您可以尝试在单独的 AppDomain 中调用 CompileAssemblyFromSource,然后在完成后卸载该域。 上面的链接应该是:blogs.claritycon.com/steveholstad/2007/06/28/… 该博客条目已连同博客一起被删除 将博客条目链接再次固定为blogs.claritycon.com/blog/2007/06/…【参考方案2】:

不支持卸载程序集。有关原因的一些信息可以在here 找到。 可以在here 找到有关使用 AppDomain 的一些信息。

【讨论】:

Jeremy 是正确的,没有办法强制 .NET 卸载程序集。您将不得不使用 appdomain(这有点烦人,但实际上并没有那么糟糕),因此您可以在完成后转储整个内容。 所以,我听说你不能卸载程序集,除非你将它加载到它自己的 AppDomain 中,然后转储整个程序。这听起来像是一个可行的解决方案,那么我该如何编译和使用动态代码呢?【参考方案3】:

你能等到 .NET 4.0 吗?有了它,您可以使用表达式树和 DLR 动态生成代码,而不会出现代码生成内存丢失问题。

另一种选择是将 .NET 3.5 与 IronPython 等动态语言结合使用。

编辑:表达式树示例

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

【讨论】:

我喜欢你的建议,但不幸的是,它们在这个项目上对我不起作用(我们还没有使用 .NET 4,也不能使用 IronPython(仅限 C#))。如果您不介意,您能否充实您与表达式树有关的答案。它可能会帮助别人。它们可以用来获取存储在字符串中的内容并将其转换为可以从内存中卸载的工作代码吗?谢谢。 我添加了一个关于表达式树的文章的链接。 如果使用 DLR 动态创建代码,运行时会占用空间。你能在没有 appdomains 的情况下在 .Net 4 中释放它吗?问题中的内存泄漏不是由于代码生成,而是由于在同一应用程序域中加载了无法释放的程序集(所以不是真正的泄漏)。 如果你使用“DynamicMethod”,动态生成的代码与一个可以被垃圾回收的对象相关联。 msdn.microsoft.com/en-us/magazine/cc163491.aspx【参考方案4】:

我想我有一个可行的解决方案。感谢大家为我指明正确的方向(我希望如此)。

程序集不能直接卸载,但 AppDomains 可以。我创建了一个辅助库,它被加载到一个新的 AppDomain 中,并且能够从代码编译一个新的程序集。这是该帮助程序库中的类的样子:

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 中。编译需要一些源代码并尝试创建一个程序集。 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 的使用。这些对于确保帮助程序库确实存在于新域中非常重要。

如果有人发现任何问题或有改进建议,我很乐意听取他们的意见。

【讨论】:

关于跨域边界的注意事项。如果您不从 MarshalByRefObject 派生编组类型,则编组将使用按值复制的语义。这可能会导致一些非常慢的执行,因为跨域边界的通信将非常健谈。如果您不能让您的类型派生自 MarshalByRefObject,您可能希望创建一个通用代理对象,您在从 MarshalByRefObject 派生的辅助 AppDomain 中实例化该对象,该对象介导通信。如果你确实实现了 MarshalByRefObject,请注意对象的生命周期,并实现 ... InitializeLifetimeService 的覆盖,以使对象保持足够长的时间。如果您希望对象永远存在,请从 InitializeLifetimeService 返回 null。 @jrista 感谢您的指点。上例中的编组类型是 Hello 类(使用 this.assembly.GetType(typeName) 访问的类)吗?

以上是关于如何防止 CompileAssemblyFromSource 泄漏内存?的主要内容,如果未能解决你的问题,请参考以下文章

如何防止超卖

在winform当中提交数据,如何防止重复提交?

PostgreSQL如何防止表太大?

如何彻底防止SQL注入?

如何防止VBS的错误提示

https如何防止会话劫持