在.Net中使用MarshalByRef时内存泄漏

时间:2014-11-07 15:25:44

标签: c# memory-leaks appdomain .net-remoting marshalbyrefobject

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

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

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

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

0 个答案:

没有答案