Как я могу предотвратить утечку памяти в CompileAssemblyFromSource?
У меня есть код на C#, который использует CSharpCodeProvider.CompileAssemblyFromSource для создания сборки в памяти. После сборки мусора мое приложение использует больше памяти, чем до создания сборки. Мой код находится в веб-приложении ASP.NET, но я продублировал эту проблему в WinForm. Я использую System.GC.GetTotalMemory(true) и профилировщик памяти ANTS Red Gate для измерения роста (около 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: чтобы уточнить, я ищу решение, которое обеспечивает те же функциональные возможности, что и код выше (компиляция и предоставление доступа к сгенерированному коду) без утечки памяти. Похоже, что решение будет включать создание нового домена приложений и маршалинг.
4 ответа
Я думаю, что у меня есть рабочее решение. Спасибо всем за то, что указали мне правильное направление (я надеюсь).
Сборки не могут быть выгружены напрямую, но 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. Компиляция берет некоторый исходный код и пытается создать сборку. 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. Это важно для гарантии того, что вспомогательная библиотека действительно живет в новом домене.
Если кто-то заметит какие-либо проблемы или предложит улучшить их, я бы хотел их услышать.
Вы также можете найти эту запись в блоге полезной: Использование AppDomain для загрузки и выгрузки динамических сборок. В нем приведен пример кода, демонстрирующего, как создать домен приложения, загрузить в него (динамическую) сборку, выполнить некоторую работу в новом домене приложения, а затем выгрузить его.
Изменить: исправлена ссылка, как указано в комментариях ниже.
Можете ли вы подождать до.NET 4.0? С его помощью вы можете использовать деревья выражений и DLR для динамического генерирования кода без проблем потери памяти в коде.
Другой вариант - использовать.NET 3.5 с динамическим языком, таким как IronPython.
РЕДАКТИРОВАТЬ: Пример дерева выражений