Использование AppDomain для загрузки/выгрузки внешних сборок

Мой сценарий выглядит следующим образом:

  • Создать новый 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()

Может кто-нибудь пролить свет на эту проблему?

Ответы

Ответ 1

Это не полный ответ: я только заметил, что вы используете строку как полезную нагрузку. Строки не полезны для этого, поскольку литеральные строки интернированы. Интернированные строки разделяются между AppDomains, поэтому часть не выгружается при выгрузке вашего AppDomain. Вместо этого попробуйте использовать байт [].

Ответ 3

Отвечая на мой собственный вопрос - не знаю, есть ли лучший способ сделать это на 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);
    }
}

Ответ 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, которая должна быть уже создана и добавлена ​​как в основное приложение, так и в ваше удаленное приложение. Это в основном интерфейс и загрузчик factory. Следующий код взят с сайта:

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

Надеюсь, что это имеет смысл и помогает...

Ответ 5

.Net использует не детерминированную финализацию. Если вы хотите увидеть, падает ли память, вы должны сделать...

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

... после разгрузки. Кроме того, если у вас нет необходимости принудительно собирать коллекцию (скорее всего, это не так), вы должны позволить системе собирать ее самостоятельно. Обычно, если вы чувствуете необходимость принудительного сбора в производственном коде, возникает утечка ресурса, обычно вызванная тем, что вы не вызываете Dispose на объекты IDisposable или не освобождаете неуправляемые объекты

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

Ответ 6

Каждая сборка также загружается в основной домен. Поскольку вы используете экземпляр Assembly, ваш основной домен загружает эту сборку, чтобы иметь возможность анализировать все типы в ней.

Если вы хотите предотвратить загрузку сборки в обоих доменах - используйте метод AppDomain.CreateInstance.

Ответ 7

Собственно, комбинация из вышеперечисленных ответов указала мне (надеюсь) правильный ответ: Теперь мой код выглядит следующим образом:

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, которого я не знаю, - библиотека загружается динамически.

Спасибо за помощь, Болек.