使用AppDomain加载/卸载外部程序集

13

我的场景如下:

  • 创建新的AppDomain
  • 加载一些程序集到其中
  • 对已加载的dll进行一些操作
  • 卸载AppDomain以释放内存和已加载的库

以下是我正在尝试使用的代码:

    class Program
{
    static void Main(string[] args)
    {
        Evidence e = new Evidence(AppDomain.CurrentDomain.Evidence);
        AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
        Console.WriteLine("Creating new AppDomain");
        AppDomain newDomain = AppDomain.CreateDomain("newDomain", e, setup);
        string fullName = Assembly.GetExecutingAssembly().FullName;
        Type loaderType = typeof(AssemblyLoader);
        var loader = (AssemblyLoader)newDomain.CreateInstanceFrom(loaderType.Assembly.Location, loaderType.FullName).Unwrap();
        Console.WriteLine("Loading assembly");
        Assembly asm = loader.LoadAssembly("library.dll");
        Console.WriteLine("Creating instance of Class1");
        object instance = Activator.CreateInstance(asm.GetTypes()[0]);
        Console.WriteLine("Created object is of type {0}", instance.GetType());
        Console.ReadLine();
        Console.WriteLine("Unloading AppDomain");
        instance = null;
        AppDomain.Unload(newDomain);
        Console.WriteLine("New Domain unloaded");
        Console.ReadLine();
    }

    public class AssemblyLoader : MarshalByRefObject
    {
        public Assembly LoadAssembly(string path)
        {
            return Assembly.LoadFile(path);
        }
    }
}

library.dll只包含一个单一的虚构类,拥有一个庞大的字符串表(用于更容易地追踪内存消耗)。

现在问题在于,内存实际上并没有被释放。更让人惊讶的是,在 AppDomain.Unload() 后,内存使用量实际上还会增加。

有人能解决这个问题吗?

8个回答

7

这不是一个完整的答案:我注意到你使用字符串作为有效载荷。字符串对于此并不有用,因为字面字符串被内部化了。内部化的字符串在AppDomains之间共享,因此当您卸载AppDomain时,该部分不会被卸载。尝试改用byte[]。


你能再详细解释一下吗?你指的是OP帖子中的哪一行代码? - NGambit
我猜你指的是返回Assembly.LoadFile(path);并建议改为byte[] bytes = File.ReadAllBytes(path); return assembly = Assembly.Load(bytes); 我理解得对吗? - NGambit

6

回答自己的问题 - 不知道在StackOverflow上是否有更好的方法... 如果有的话,我会感激您提供指导... 无论如何,通过搜索互联网,我找到了另一个解决方案,希望这个更好。 下面是代码,如果有人发现任何弱点,请回应。

class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();
        for(int i=0;i<10;i++)
        {
            AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
            appDomain.DoCallBack(loadAssembly);
            appDomain.DomainUnload += appDomain_DomainUnload;

            AppDomain.Unload(appDomain);        
        }

        AppDomain appDomain2 = AppDomain.CreateDomain("MyTemp2");
        appDomain2.DoCallBack(loadAssembly);
        appDomain2.DomainUnload += appDomain_DomainUnload;

        AppDomain.Unload(appDomain2);

        GC.Collect();
        GC.WaitForPendingFinalizers();  
        Console.ReadLine();
    }

    private static void loadAssembly()
    {
        string fullPath = @"E:\tmp\sandbox\AppDomains\AppDomains1\AppDomains1\bin\Debug\BigLib.dll";
        var assembly = Assembly.LoadFrom(fullPath);
        var instance = Activator.CreateInstance(assembly.GetTypes()[0]);
        Console.WriteLine("Creating instance of {0}", instance.GetType());
        Thread.Sleep(2000);
        instance = null;
    }

    private static void appDomain_DomainUnload(object sender, EventArgs e)
    {
        AppDomain ap = sender as AppDomain;
        Console.WriteLine("Unloading {0} AppDomain", ap.FriendlyName);
    }
}

6

1
你的链接已失效,我在这里看到了你的代码:https://github.com/UAV/PlugInLoader 但是没有README。 - DaFi4

4
这是一个晚回答,但对于未来查看此问题的任何人都将是有价值的。我需要以动态代码编译/执行方式实现类似的功能。最好的方法是在主AppDomain之外的远程域中执行所有方法,否则应用程序内存将不断增加。您可以通过远程接口和代理解决此问题。 因此,您将通过接口公开方法,在主AppDomain中获取其实例,然后在远程域中远程执行这些方法,卸载新创建的域(远程域),将其置空,然后强制GC收集未使用的对象。我花了很长时间调试我的代码,直到我意识到我必须强制GC这样做,它才能正常工作。我的大部分实现都来自:http://www.west-wind.com/presentations/dynamicCode/DynamicCode.htm
      //pseudo code
      object ExecuteCodeDynamically(string code)
       {
        Create AppDomain my_app
         src_code = "using System; using System.Reflection; using RemoteLoader;
        namespace MyNameSpace{
       public class MyClass:MarshalByRefObject, IRemoteIterface
      {
      public object Invoke(string local_method, object[] parameters)
        {
       return this.GetType().InvokeMember(local_method, BindingFlags.InvokeMethod, null, this,    parameters);
     }
     public object ExecuteDynamicCode(params object[] parameters)
     {
    " + code + } } } ";// this whole big string is the remote application

     //compile this code which is src_code
     //output it as a DLL on the disk rather than in memory with the name e.g.: DynamicHelper.dll. This can be done by playing with the CompileParameters
     // create the factory class in the secondary app-domain
               RemoteLoader.RemoteLoaderFactory factory =
                  (RemoteLoader.RemoteLoaderFactory)loAppDomain.CreateInstance("RemoteLoader",
                  "RemoteLoader.RemoteLoaderFactory").Unwrap();

            // with the help of this factory, we can now create a real instance
            object loObject = factory.CreateInstance("DynamicHelper.dll", "MyNamespace.MyClass", null);

            // *** Cast the object to the remote interface to avoid loading type info
            RemoteLoader.IRemoteInterface loRemote = (RemoteLoader.IRemoteInterface)loObject;

            if (loObject == null)
            {
                System.Windows.Forms.MessageBox.Show("Couldn't load class.");
                return null;
            }

            object[] loCodeParms = new object[1];
            loCodeParms[0] = "bla bla bla";

            try
            {
                // *** Indirectly call the remote interface
                object result = loRemote.Invoke("ExecuteDynamicCode", loCodeParms);// this is the object to return                

            }
            catch (Exception loError)
            {
                System.Windows.Forms.MessageBox.Show(loError.Message, "Compiler Demo",
                    System.Windows.Forms.MessageBoxButtons.OK,
                    System.Windows.Forms.MessageBoxIcon.Information);
                return null;
            }

            loRemote = null;
            try { AppDomain.Unload(my_app); }
            catch (CannotUnloadAppDomainException ex)
            { String str = ex.Message; }
            loAppDomain = null;
            GC.Collect();//this will do the trick and free the memory
            GC.WaitForPendingFinalizers();
            System.IO.File.Delete("ConductorDynamicHelper.dll");
            return result;

}

请注意,RemoteLoader是另一个DLL,应该已经创建并添加到您的主应用程序和远程应用程序中。它基本上是一个接口和工厂加载器。以下代码摘自上述网站:
      /// <summary>
    /// Interface that can be run over the remote AppDomain boundary.
   /// </summary>
     public interface IRemoteInterface
     {
    object Invoke(string lcMethod,object[] Parameters);
     }


     naemspace RemoteLoader{
   /// <summary>
   /// Factory class to create objects exposing IRemoteInterface
  /// </summary>
  public class RemoteLoaderFactory : MarshalByRefObject
 {
   private const BindingFlags bfi = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance;

 public RemoteLoaderFactory() {}

 /// <summary> Factory method to create an instance of the type whose name is specified,
  /// using the named assembly file and the constructor that best matches the specified parameters.  </summary>
 /// <param name="assemblyFile"> The name of a file that contains an assembly where the type named typeName is sought. </param>
 /// <param name="typeName"> The name of the preferred type. </param>
 /// <param name="constructArgs"> An array of arguments that match in number, order, and type the parameters of the constructor to invoke, or null for default constructor. </param>
 /// <returns> The return value is the created object represented as ILiveInterface. </returns>
 public IRemoteInterface Create( string assemblyFile, string typeName, object[] constructArgs )
 {
  return (IRemoteInterface) Activator.CreateInstanceFrom(
  assemblyFile, typeName, false, bfi, null, constructArgs,
  null, null, null ).Unwrap();
   }
  }
  }

希望这有意义并能帮到你...

3

.Net使用非确定性终结。如果您想查看内存是否下降,应执行...

GC.Collect(); 
GC.WaitForPendingFinalizers();

在卸载后,.NET运行时会自动回收内存。除非你有强制回收的需求(这种情况不太可能),否则应该让系统自己回收。通常,如果你在生产代码中感觉需要强制回收,那么很可能是由于没有调用IDisposable对象的Dispose方法或未释放非托管对象导致的资源泄漏。

using (var imdisposable = new IDisposable())
{
}
//
var imdisposable = new IDisposable();
imdisposable.Dispose();
//
Marshal.Release(intPtr); 
//
Marshal.ReleaseComObject(comObject);

1

实际上,上面的答案结合起来指引我(希望)得出了正确的答案: 我的代码现在如下:

AppDomain newDomain = AppDomain.CreateDomain("newDomain", e, setup);
string fullName = Assembly.GetExecutingAssembly().FullName;
Type loaderType = typeof(AssemblyLoader);
FileStream fs = new FileStream(@"library.dll", FileMode.Open);
byte[] buffer = new byte[(int)fs.Length];
fs.Read(buffer, 0, buffer.Length);
fs.Close();

Assembly domainLoaded = newDomain.Load(buffer);
object loaded = Activator.CreateInstance(domainLoaded.GetTypes()[1]);
AppDomain.Unload(newDomain);
GC.Collect();
GC.WaitForPendingFinalizers();

我无法使用AppDomain.CreateInstance,因为它需要Assembly.FullName,而我不知道 - 库是动态加载的。

感谢您的帮助, Bolek。


1
但这个能行吗?在我看来,似乎你仍然将程序集拉回到了主应用程序域中... - Lasse V. Karlsen
2
实际上,Lasse,你是对的。它不起作用。一些内存被释放了,但是已加载的dll文件句柄并没有...仍在寻找答案... - Bolek Tekielski


1
每个程序集也被加载到主域中。因为你使用了程序集实例,所以主域加载该程序集以便能够分析其中的所有类型。
如果你想防止在两个域中都加载程序集,请使用 AppDomain.CreateInstance 方法。

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