Как я могу предотвратить утечку памяти в CompileAssemblyFromSource?

У меня есть код C#, который использует CSharpCodeProvider.CompileAssemblyFromSource для создания сборки в памяти. После сборки мусора мое приложение использует больше памяти, чем до создания сборки. Мой код находится в веб-приложении ASP.NET, но я продублировал эту проблему в WinForm. Я использую System.GC.GetTotalMemory(true) и профилировщик памяти Red Gate ANTS для измерения роста (около 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: этот вопрос может быть связан с: Динамическая загрузка и выгрузка DLL, сгенерированной с помощью CSharpCodeProvider

Обновление 2. Пытаясь лучше понять домены приложений, я обнаружил следующее: Что такое домен приложения — пояснение для новичков в .Net

Обновление 3. Чтобы уточнить, я ищу решение, которое обеспечивает ту же функциональность, что и приведенный выше код (компиляция и предоставление доступа к сгенерированному коду) без утечки памяти. Похоже, решение будет включать создание нового AppDomain и маршалинг.


person Nogwater    schedule 25.11.2009    source источник
comment
Очень классный вопрос. К концу сегодняшнего дня у меня будет пример того, как это сделать с помощью другого AppDomain (сейчас я обедаю, а затем возвращаюсь к работе...).   -  person Charles    schedule 04.12.2009
comment
Что вы планируете делать с полученной сборкой? Это только на разовую казнь или вы собираетесь задержаться?   -  person madaboutcode    schedule 05.12.2009
comment
@LightX Я собираюсь удерживать его некоторое время и вызывать из него членов по мере необходимости, но затем, когда будет доступна новая версия исходного кода, я захочу сбросить ее и создать новую сборку на основе новый код. Без исправления AppDomain этот цикл многократного создания сборок (даже несмотря на то, что я перестаю ссылаться на старые версии) приводит к увеличению использования памяти.   -  person Nogwater    schedule 05.12.2009
comment
При вызове этой функции нет утечек. Просто позвольте ему быть собранным мусором. Для этого надо выгрузить весь домен, в который yiu загрузил сгенерированную сборку   -  person Artur Mustafin    schedule 30.09.2011


Ответы (4)


Выгрузка сборки не поддерживается. Некоторую информацию о том, почему можно найти здесь. Некоторую информацию об использовании AppDomain можно найти здесь.

person toad    schedule 25.11.2009
comment
Джереми прав, невозможно заставить .NET выгрузить сборку. Вам придется использовать домен приложения (что немного раздражает, но на самом деле не так уж и плохо), так что вы можете сбросить все это, когда закончите. - person David Hay; 25.11.2009
comment
Итак, я слышал, что вы не можете выгрузить сборку, если только вы не загрузите ее в свой собственный домен приложения, а затем не сбросите все это. Это звучит как работающее решение, но как мне это сделать, чтобы скомпилировать и использовать динамический код? - person Nogwater; 29.11.2009

Думаю, у меня есть рабочее решение. Спасибо всем за то, что указали мне правильное направление (надеюсь).

Сборки нельзя выгружать напрямую, а AppDomains можно. Я создал вспомогательную библиотеку, которая загружается в новый домен приложения и может скомпилировать новую сборку из кода. Вот как выглядит класс в этой вспомогательной библиотеке:

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. Это важно для обеспечения того, чтобы вспомогательная библиотека действительно жила в новом домене.

Если кто-то заметит какие-либо проблемы или есть предложения по улучшению этого, я хотел бы услышать их.

person Nogwater    schedule 01.12.2009
comment
Кое-что, что нужно отметить о пересечении границ домена. Если маршалируемый тип не является производным от MarshalByRefObject, то маршалинг будет использовать семантику копирования по значению. Это может привести к очень медленному выполнению, так как связь через границу домена будет очень болтливой. Если вы не можете получить свои типы, производные от MarshalByRefObject, вы можете создать универсальный прокси-объект, который вы создаете во вторичном AppDomain, который ДЕЙСТВИТЕЛЬНО является производным от MarshalByRefObject, который опосредует связь. Если вы реализуете MarshalByRefObject, остерегайтесь времени жизни объекта и реализуйте... - person jrista; 05.12.2009
comment
переопределение InitializeLifetimeService таким образом, чтобы поддерживать объект в рабочем состоянии достаточно долго. Если вы хотите, чтобы объект сохранялся вечно, верните null из InitializeLifetimeService. - person jrista; 05.12.2009
comment
@jrista Спасибо за указатель. Является ли упорядоченный тип в приведенном выше примере классом Hello (тот, доступ к которому осуществляется с помощью this.assembly.GetType(typeName))? - person Nogwater; 05.12.2009

Вам также может быть полезна эта запись в блоге: Использование AppDomain для загрузки и выгрузки динамических сборок. В нем приведен пример кода, демонстрирующий, как создать AppDomain, загрузить в него (динамическую) сборку, выполнить некоторую работу в новом AppDomain, а затем выгрузить ее.

Изменить: исправлена ​​ссылка, как указано в комментариях ниже.

person Dan Blanchard    schedule 25.11.2009
comment
Кажется, это то направление, которое мне нужно знать. Могу ли я использовать это с CompileAssemblyFromSource? Могу ли я создавать новые домены приложений из веб-приложения? - person Nogwater; 26.11.2009
comment
Вы можете попробовать вызвать CompileAssemblyFromSource в отдельном AppDomain, а затем выгрузить этот домен, когда закончите. - person Mikael Svenson; 01.12.2009
comment
ссылка выше должна быть: blogs.claritycon.com/steveholstad/2007/06/28/ - person Lee Smith; 12.07.2011
comment
запись в блоге была удалена вместе с блогом - person JBeurer; 10.03.2014
comment
Снова исправлена ​​ссылка на запись в блоге на blogs.claritycon.com/blog/2007/06/ - person Dan Blanchard; 13.03.2014

Можете ли вы подождать до .NET 4.0? С его помощью вы можете использовать деревья выражений и DLR для динамической генерации кода без потери памяти при генерации кода.

Другой вариант — использовать .NET 3.5 с динамическим языком, таким как IronPython.

РЕДАКТИРОВАТЬ: Пример дерева выражений

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

person Jonathan Allen    schedule 03.12.2009
comment
Мне нравятся ваши предложения, но, к сожалению, они не будут работать для меня в этом проекте (мы еще не используем .NET 4 и не можем использовать IronPython (только C#)). Если вы не возражаете, не могли бы вы конкретизировать свой ответ, касающийся деревьев выражений. Это может помочь кому-то другому. Можно ли их использовать, чтобы взять что-то, хранящееся в строке, и превратить это в работающий код, который можно выгрузить из памяти? Спасибо. - person Nogwater; 04.12.2009
comment
Я добавил ссылку на статью о деревьях выражений. - person Jonathan Allen; 04.12.2009
comment
Если вы динамически создаете код с помощью DLR, он будет занимать место при запуске. Сможете ли вы освободить это в .Net 4 без доменов приложений? Утечка памяти в этом вопросе происходит не из-за генерации кода, а из-за загрузки сборки в том же домене приложения (так что на самом деле это не утечка), которую нельзя освободить. - person Mikael Svenson; 05.12.2009
comment
Если вы используете DynamicMethod, динамически сгенерированный код связывается с объектом, который может быть удален сборщиком мусора. msdn.microsoft.com/en-us/magazine/cc163491.aspx - person Jonathan Allen; 07.12.2009