4

我遇到了一个有趣的问题,我猜这真的是双重的。不过,我会尽量保持专注。我设置了一个环境,其中以编程方式编译程序集并将其加载到子应用程序域中。该子应用程序域程序集中的一个类被实例化(它实际上被封送回父域并在那里使用代理),并针对它执行方法。

以下内容位于附属程序集中:

namespace ScriptingSandbox
{
  public interface ISandbox
  {
    object Invoke(string method, object[] parameters);
    void Disconnect();
  }

  public class SandboxLoader : MarshalByRefObject, IDisposable
  {
    #region Properties

    public bool IsDisposed { get; private set; }
    public bool IsDisposing { get; private set; }

    #endregion

    #region Finalization/Dispose Methods

    ~SandboxLoader()
    {
      DoDispose();
    }

    public void Dispose()
    {
      DoDispose();
      GC.SuppressFinalize(this);
    }

    private void DoDispose()
    {
      if (IsDisposing) return;
      if (IsDisposed) return;

      IsDisposing = true;

      Disconnect();

      IsDisposed = true;
      IsDisposing = false;
    }

    #endregion

    [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
    public override object InitializeLifetimeService()
    {
      // We don't want this to ever expire.
      // We will disconnect it when we're done.
      return null;
    }

    public void Disconnect()
    {
      // Close all the remoting channels so that this can be garbage
      // collected later and we don't leak memory.
      RemotingServices.Disconnect(this);
    }

    public ISandbox Create(string assemblyFileName, string typeName, object[] arguments)
    {
      // Using CreateInstanceFromAndUnwrap and then casting to the interface so that types in the
      // child AppDomain won't be loaded into the parent AppDomain.
      BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance;

      object instance = AppDomain.CurrentDomain.CreateInstanceFromAndUnwrap(assemblyFileName, typeName, true, bindingFlags, null, arguments, null, null);

      ISandbox sandbox = instance as ISandbox;

      return sandbox;
    }
  }
}

从子应用程序域中解包出来的类应该实现上面的接口。上面代码中的 SandboxLoader 也运行在子应用域中,并起到创建目标类的作用。这一切都由下面的 ScriptingHost 类绑定,该类在主程序集的父域中运行。

namespace ScriptingDemo
{
  internal class ScriptingHost : IDisposable
  {
    #region Declarations

    private AppDomain _childAppDomain;
    private string _workingDirectory;

    #endregion

    #region Properties

    public bool IsDisposed { get; private set; }
    public bool IsDisposing { get; private set; }
    public string WorkingDirectory
    {
      get
      {
        if (string.IsNullOrEmpty(_workingDirectory))
        {
          _workingDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin");
        }

        return _workingDirectory;
      }
    }

    #endregion

    public ScriptingHost() { }

    #region Finalization/Dispose Methods

    ~ScriptingHost()
    {
      DoDispose(false);
    }

    public void Dispose()
    {
      DoDispose(true);
      GC.SuppressFinalize(this);
    }

    private void DoDispose(bool isFromDispose)
    {
      if (IsDisposing) return;
      if (IsDisposed) return;

      IsDisposing = true;

      if (isFromDispose)
      {
        UnloadChildAppDomain();
      }

      IsDisposed = true;
      IsDisposing = false;
    }

    private void UnloadChildAppDomain()
    {
      if (_childAppDomain == null) return;

      try
      {
        bool isFinalizing = _childAppDomain.IsFinalizingForUnload();

        if (!isFinalizing)
        {
          AppDomain.Unload(_childAppDomain);
        }
      }
      catch { }

      _childAppDomain = null;
    }

    #endregion

    #region Compile

    public List<string> Compile()
    {
      CreateDirectory(WorkingDirectory);

      CreateChildAppDomain(WorkingDirectory);

      CompilerParameters compilerParameters = GetCompilerParameters(WorkingDirectory);

      using (VBCodeProvider codeProvider = new VBCodeProvider())
      {
        string sourceFile = GetSourceFilePath();

        CompilerResults compilerResults = codeProvider.CompileAssemblyFromFile(compilerParameters, sourceFile);

        List<string> compilerErrors = GetCompilerErrors(compilerResults);

        return compilerErrors;
      }
    }

    private string GetSourceFilePath()
    {
      DirectoryInfo dir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);

     // This points a test VB.net file in the solution.
      string sourceFile = Path.Combine(dir.Parent.Parent.FullName, @"Classes\Scripting", "ScriptingDemo.vb");

      return sourceFile;
    }

    private void CreateDirectory(string path)
    {
      if (Directory.Exists(path))
      {
        Directory.Delete(path, true);
      }

      Directory.CreateDirectory(path);
    }

    private void CreateChildAppDomain(string workingDirectory)
    {
      AppDomainSetup appDomainSetup = new AppDomainSetup()
      {
        ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
        PrivateBinPath = "bin",
        LoaderOptimization = LoaderOptimization.MultiDomainHost,
        ApplicationTrust = AppDomain.CurrentDomain.ApplicationTrust
      };

      Evidence evidence = new Evidence(AppDomain.CurrentDomain.Evidence);

      _childAppDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), evidence, appDomainSetup);
      _childAppDomain.InitializeLifetimeService();
    }

    private CompilerParameters GetCompilerParameters(string workingDirectory)
    {
      CompilerParameters compilerParameters = new CompilerParameters()
      {
        GenerateExecutable = false,
        GenerateInMemory = false,
        IncludeDebugInformation = true,
        OutputAssembly = Path.Combine(workingDirectory, "GeneratedAssembly.dll")
      };

      // Add GAC/System Assemblies
      compilerParameters.ReferencedAssemblies.Add("System.dll");
      compilerParameters.ReferencedAssemblies.Add("System.Xml.dll");
      compilerParameters.ReferencedAssemblies.Add("System.Data.dll");
      compilerParameters.ReferencedAssemblies.Add("Microsoft.VisualBasic.dll");

      // Add Custom Assemblies
      compilerParameters.ReferencedAssemblies.Add(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ScriptingSandbox.dll"));
      compilerParameters.ReferencedAssemblies.Add(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ScriptingInterfaces.dll"));

      return compilerParameters;
    }

    private List<string> GetCompilerErrors(CompilerResults compilerResults)
    {
      List<string> errors = new List<string>();

      if (compilerResults == null) return errors;
      if (compilerResults.Errors == null) return errors;
      if (compilerResults.Errors.Count == 0) return errors;

      foreach (CompilerError error in compilerResults.Errors)
      {
        string errorText = string.Format("[{0}, {1}] :: {2}", error.Line, error.Column, error.ErrorText);

        errors.Add(errorText);
      }

      return errors;
    }

    #endregion

    #region Execute

    public object Execute(string method, object[] parameters)
    {
      using (SandboxLoader sandboxLoader = CreateSandboxLoader())
      {
        ISandbox sandbox = CreateSandbox(sandboxLoader);

        try
        {
          object result = sandbox.Invoke(method, parameters);

          return result;
        }
        finally
        {
          if (sandbox != null)
          {
            sandbox.Disconnect();
            sandbox = null;
          }
        }
      }
    }

    private SandboxLoader CreateSandboxLoader()
    {
      object sandboxLoader = _childAppDomain.CreateInstanceAndUnwrap("ScriptingSandbox", "ScriptingSandbox.SandboxLoader", true, BindingFlags.CreateInstance, null, null, null, null);

      return sandboxLoader as SandboxLoader;
    }

    private ISandbox CreateSandbox(SandboxLoader sandboxLoader)
    {
      string assemblyPath = Path.Combine(WorkingDirectory, "GeneratedAssembly.dll");

      ISandbox sandbox = sandboxLoader.Create(assemblyPath, "ScriptingDemoSource.SandboxClass", null);

      return sandbox;
    }

    #endregion
  }
}

作为参考,编译的 ScriptingDemo.vb 文件:

Imports System
Imports System.Collections
Imports System.Collections.Generic
Imports System.Globalization
Imports Microsoft.VisualBasic
Imports System.Data
Imports System.Text
Imports System.Text.RegularExpressions
Imports System.Xml
Imports System.Net
Imports System.ComponentModel
Imports System.Reflection
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Lifetime
Imports System.Security.Permissions
Imports ScriptingSandbox
Imports ScriptingInterfaces

Namespace ScriptingDemoSource

  Public Class SandboxClass
    Inherits MarshalByRefObject
    Implements ISandbox

    Public Sub Disconnect() Implements ISandbox.Disconnect
      RemotingServices.Disconnect(Me)
    End Sub

    Public Function Invoke(ByVal methodName As String, methodParameters As Object()) As Object Implements ScriptingSandbox.ISandbox.Invoke

      'Return Nothing

      Dim type As System.Type = Me.GetType()

      Dim returnValue As Object = type.InvokeMember(methodName, Reflection.BindingFlags.InvokeMethod + Reflection.BindingFlags.Default, Nothing, Me, methodParameters)

      type = Nothing

      Return returnValue

    End Function

    <SecurityPermissionAttribute(SecurityAction.Demand, Flags:=SecurityPermissionFlag.Infrastructure)> _
    Public Overrides Function InitializeLifetimeService() As Object
      Return Nothing
    End Function

    Function ExecuteWithNoParameters() As Object
      Return Nothing
    End Function

    Function ExecuteWithSimpleParameters(a As Integer, b As Integer) As Object
      Return a + b
    End Function

    Function ExecuteWithComplexParameters(o As ScriptingInterfaces.IMyInterface) As Object
      Return o.Execute()
    End Function

  End Class

End Namespace

我遇到的第一个问题是,即使在清理沙箱之后,内存仍然泄漏。这是通过保留沙箱实例而不在执行脚本中的方法后将其销毁来解决的。这向 ScriptingHost 类添加/更改了以下内容:

private ISandbox _sandbox;
private string _workingDirectory;

private void DoDispose(bool isFromDispose)
{
  if (IsDisposing) return;
  if (IsDisposed) return;

  IsDisposing = true;

  if (isFromDispose)
  {
    Cleanup();
  }

  IsDisposed = true;
  IsDisposing = false;
}

private void CleanupSandboxLoader()
{
  try
  {
    if (_sandboxLoader == null) return;
    _sandboxLoader.Disconnect();
    _sandboxLoader = null;
  }
  catch { }
}

private void CleanupSandbox()
{
  try
  {
    if (_sandbox == null) return;
    _sandbox.Disconnect();
  }
  catch { }
}

public void Cleanup()
{
  CleanupSandbox();
  CleanupSandboxLoader();
  UnloadChildAppDomain();
}

public object Execute(string method, object[] parameters)
{
  if (_sandboxLoader == null)
  {
    _sandboxLoader = CreateSandboxLoader();
  }

  if (_sandbox == null)
  {
    _sandbox = CreateSandbox(_sandboxLoader);
  }

  object result = _sandbox.Invoke(method, parameters);

  return result;
}

这确实没有解决根本问题(破坏沙箱和加载程序没有按预期释放内存)。不过,由于我对这种行为有更多的控制权,它确实让我可以继续讨论下一个问题。

使用 ScriptingHost 的代码如下所示:

private void Execute()
{
  try
  {
    List<MyClass> originals = CreateList();

    for (int i = 0; i < 4000; i++)
    {
      List<MyClass> copies = MyClass.MembersClone(originals);

      foreach (MyClass copy in copies)
      {
        object[] args = new object[] { copy };

        try
        {
          object results = _scriptingHost.Execute("ExecuteWithComplexParameters", args);
        }
        catch (Exception ex)
        {
          _logManager.LogException("executing the script", ex);
        }
        finally
        {
          copy.Disconnect();

          args.SetValue(null, 0);
          args = null;
        }
      }

      MyClass.ShallowCopy(copies, originals);

      MyClass.Cleanup(copies);
      copies = null;
    }

    MyClass.Cleanup(originals);
    originals = null;
  }
  catch (Exception ex)
  {
    _logManager.LogException("executing the script", ex);
  }

  MessageBox.Show("done");
}

private List<MyClass> CreateList()
{
  List<MyClass> myClasses = new List<MyClass>();

  for (int i = 0; i < 300; i++)
  {
    MyClass myClass = new MyClass();
    myClasses.Add(myClass);
  }

  return myClasses;
}

MyClass 的代码:

namespace ScriptingDemo
{
  internal sealed class MyClass : MarshalByRefObject, IMyInterface, IDisposable
  {
    #region Properties

    public int ID { get; set; }
    public string Name { get; set; }
    public bool IsDisposed { get; private set; }
    public bool IsDisposing { get; private set; }

    #endregion

    public MyClass() { }

    #region Finalization/Dispose Methods

    ~MyClass()
    {
      DoDispose();
    }

    public void Dispose()
    {
      DoDispose();
      GC.SuppressFinalize(this);
    }

    private void DoDispose()
    {
      if (IsDisposing) return;
      if (IsDisposed) return;

      IsDisposing = true;

      Disconnect();

      IsDisposed = true;
      IsDisposing = false;
    }

    #endregion

    [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
    public override object InitializeLifetimeService()
    {
      // We don't want this to ever expire.
      // We will disconnect it when we're done.
      return null;
    }

    public void Disconnect()
    {
      // Close all the remoting channels so that this can be garbage
      // collected later and we don't leak memory.
      RemotingServices.Disconnect(this);
    }

    public object Execute()
    {
      return "Hello, World!";
    }

    public MyClass MembersClone()
    {
      MyClass copy = new MyClass();

      copy.ShallowCopy(this);

      return copy;
    }

    public void ShallowCopy(MyClass source)
    {
      if (source == null) return;

      ID = source.ID;
      Name = source.Name;
    }

    #region Static Members

    public static void ShallowCopy(List<MyClass> sources, List<MyClass> targets)
    {
      if (sources == null) return;
      if (targets == null) return;

      int minCount = Math.Min(sources.Count, targets.Count);

      for (int i = 0; i < minCount; i++)
      {
        MyClass source = sources[i];
        MyClass target = targets[i];

        target.ShallowCopy(source);
      }
    }

    public static List<MyClass> MembersClone(List<MyClass> originals)
    {
      if (originals == null) return null;

      List<MyClass> copies = new List<MyClass>();

      foreach (MyClass original in originals)
      {
        MyClass copy = original.MembersClone();

        copies.Add(copy);
      }

      return copies;
    }

    public static void Disconnect(List<MyClass> myClasses)
    {
      if (myClasses == null) return;

      myClasses.ForEach(c => c.Disconnect());
    }

    public static void Cleanup(List<MyClass> myClasses)
    {
      if (myClasses == null) return;

      myClasses.ForEach(c => c.Dispose());

      myClasses.Clear();
      myClasses.TrimExcess();
      myClasses = null;
    }

    #endregion
  }
}

正如代码所示,随着迭代次数的增加和 GCHandles 的飙升,内存会慢慢泄漏。我玩过添加一个有限租约而不是将租约设置为永不过期,但这会导致内存的剧烈波动,最终会下降但不完全,最终仍然比当前解决方案消耗更多的内存(相差几十兆字节)。

我完全理解创建大量这样的类并在不久之后将它们删除是不可取的,但它模拟了一个更大的系统。我们可能会也可能不会解决这个问题,但对我来说,我想更好地理解为什么当前系统中的内存泄漏。

编辑:

我只是想指出内存泄漏似乎不是托管内存。使用各种分析工具,托管堆似乎倾向于保持在一个相当设定的范围内,而非托管内存似乎在增长。

编辑#2

重写代码以保留类列表而不是每次迭代都转储它们似乎可以缓解问题(我的假设是这可行,因为我们正在重用我们已经分配的所有内容),但我想保留这个如果只是为了学术活动而开放。根本问题仍未解决。

4

0 回答 0