29

J'ai du code C# qui utilise CSharpCodeProvider.CompileAssemblyFromSource pour créer un assembly en mémoire. Une fois l'assemblage collecté, mon application utilise plus de mémoire qu'avant la création de l'assembly. Mon code est dans une application Web ASP.NET, mais j'ai dupliqué ce problème dans un formulaire WinForm. J'utilise System.GC.GetTotalMemory (true) et Red Hat ANTS Memory Profiler pour mesurer la croissance (environ 600 octets avec l'exemple de code). De la recherche que j'ai faite, on dirait que la fuite vient de la création de nouveaux types, pas vraiment d'aucun des objets auxquels je tiens des références. Certaines des pages Web que j'ai trouvées ont mentionné quelque chose sur AppDomain, mais je ne comprends pas. Quelqu'un peut-il expliquer ce qui se passe ici et comment y remédier?Comment puis-je empêcher CompileAssemblyFromSource de fuir la mémoire?

Voici quelques exemples de code pour une fuite:

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

Mise à jour 1: Cette question peut être liée: Dynamically loading and unloading a a dll generated using CSharpCodeProvider

Mise à jour 2: Essayer de comprendre les domaines d'application plus, je trouve ceci: What is an application domain - an explanation for .Net beginners

Mise à jour 3: Pour clarifier, je suis looki ng pour une solution qui fournit la même fonctionnalité que le code ci-dessus (compilation et fourniture d'accès au code généré) sans perte de mémoire. Il semble que la solution impliquera la création d'un nouvel AppDomain et d'un marshaling.

+0

question très cool. Je vais avoir un exemple de la façon de le faire en utilisant un autre AppDomain d'ici la fin de la journée (je déjeune actuellement, puis je retourne au travail ...). – Charles

+0

Que comptez-vous faire avec l'assemblage résultant? Est-ce juste pour une exécution ponctuelle ou allez-vous vous en occuper? – madaboutcode

+1

@LightX Je vais m'y accrocher pendant un moment et en invoquer des membres si nécessaire, mais quand une nouvelle version du code source sera disponible, je vais vouloir la vider et créer un nouvel assemblage basé sur le nouveau code. Sans le correctif AppDomain, ce cycle de création répétée d'assemblys (même si j'arrête de référencer les anciennes versions) entraîne une augmentation de l'utilisation de la mémoire. – Nogwater

Répondre

13

Le déchargement d'un ensemble n'est pas pris en charge. Quelques informations sur pourquoi peuvent être trouvés here. Vous trouverez des informations sur l'utilisation d'un AppDomain dans here.

+4

Jeremy a raison, il n'y a aucun moyen de forcer le .NET à décharger un assembly. Vous allez devoir utiliser un domaine d'application (ce qui est plutôt ennuyeux, mais vraiment pas si mal), de sorte que vous pouvez vider le tout une fois que vous avez terminé. –

+0

Donc, ce que j'entends c'est que vous ne pouvez pas décharger un assembly, sauf si vous le chargez dans son propre AppDomain et que vous videz le tout. Cela ressemble à une solution réalisable, alors comment faire pour compiler et utiliser du code dynamique? – Nogwater

7

Vous trouverez également cet article de blog utile: Using AppDomain to Load and Unload Dynamic Assemblies. Il fournit un exemple de code montrant comment créer un AppDomain, charger un assemblage (dynamique), travailler dans le nouveau AppDomain puis le décharger.

Editer: lien fixe comme indiqué dans les commentaires ci-dessous.

+0

Cela semble être la direction que je vais devoir connaître. Puis-je l'utiliser avec CompileAssemblyFromSource? Puis-je créer de nouveaux AppDomains à partir d'une application Web? – Nogwater

+3

Vous pouvez essayer d'appeler CompileAssemblyFromSource dans un AppDomain distinct, puis décharger ce domaine lorsque vous avez terminé. –

+1

lien ci-dessus devrait être: http://blogs.claritycon.com/steveholstad/2007/06/28/using-appdomain-to-load-and-unload-dynamic-assemblies/ –

30

Je pense que j'ai une solution de travail. Merci à tous pour m'avoir indiqué dans la bonne direction (j'espère).

Les assemblys ne peuvent pas être déchargés directement, mais AppDomains peuvent le faire. J'ai créé une bibliothèque d'assistance qui est chargée dans un nouvel AppDomain et est capable de compiler un nouvel assembly à partir du code. Voici à quoi ressemble la classe de cette bibliothèque auxiliaire:

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

} 

C'est très basique, mais c'était suffisant pour tester. PrintDomain est là pour vérifier qu'il ne vit pas dans mon nouvel AppDomain. Compile prend du code source et essaie de créer un assembly. Run nous permet de tester l'exécution de méthodes statiques à partir du code source donné.

Voilà comment j'utiliser la bibliothèque d'aide:

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

Il crée essentiellement le domaine, crée une instance de ma classe d'aide (CompilerRunner), utilise pour compiler un nouvel ensemble (caché), exécute un code à partir de ce nouvel assemblage, puis décharger le domaine pour libérer de la mémoire.

Vous remarquerez l'utilisation de MarshalByRefObject et CreateInstanceFromAndUnwrap.Ils sont importants pour s'assurer que la bibliothèque d'aide vit réellement dans le nouveau domaine.

Si quelqu'un remarque des problèmes ou a des suggestions pour améliorer cela, j'aimerais les entendre.

+2

Quelque chose à noter à propos du franchissement des frontières de domaine. Si vous ne dérivez pas le type marshalled de MarshalByRefObject, alors le marshaling utilisera la sémantique copy-by-value. Cela peut entraîner une exécution très lente, car la communication à travers les limites du domaine sera très bavarde. Si vos types ne peuvent pas provenir de MarshalByRefObject, vous pouvez créer un objet proxy générique que vous instanciez dans le domaine AppDomain secondaire qui dérive de MarshalByRefObject, qui sert de médiateur à la communication. Si vous implémentez MarshalByRefObject, méfiez-vous de la durée de vie de l'objet et implémentez ... – jrista

+1

un remplacement de * InitializeLifetimeService * de manière à maintenir l'objet en vie assez longtemps. Si vous souhaitez que l'objet reste indéfiniment, renvoyez null à partir de InitializeLifetimeService. – jrista

+0

@jrista Merci pour le pointeur. Le type marshalled dans l'exemple ci-dessus est-il la classe Hello (celle accessible avec this.assembly.GetType (typeName))? – Nogwater

2

Pouvez-vous attendre .NET 4.0? Avec lui, vous pouvez utiliser les arbres d'expression et le DLR pour générer dynamiquement du code sans le problème de perte de mémoire code-gen.

Une autre option consiste à utiliser .NET 3.5 avec un langage dynamique comme IronPython.

EDIT: Expression Arbre Exemple

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

+0

J'aime vos suggestions, mais malheureusement, elles ne fonctionneront pas pour moi sur ce projet (nous n'utilisons pas encore .NET 4 et ne pouvons pas utiliser IronPython (seulement C#)). Si cela ne vous dérange pas, pourriez-vous étoffer votre réponse concernant les arbres d'expression. Cela pourrait aider quelqu'un d'autre. Peuvent-ils être utilisés pour prendre quelque chose stocké dans une chaîne et le transformer en code de travail qui peut être déchargé de la mémoire? Merci. – Nogwater

+0

J'ai ajouté un lien vers un article sur les arbres d'expression. –

+3

Si vous créez dynamiquement du code avec DLR, cela prendra de l'espace lorsque vous l'exécuterez. Seriez-vous capable de libérer cela dans .Net 4 sans appdomains? La fuite de mémoire dans la question n'est pas due au code-gen, mais à cause du chargement d'un assembly dans le même domaine d'application (donc pas vraiment une fuite) qui ne peut pas être libéré. –

Questions connexes