Nathan Evans' Nemesis of the Moment

Memory leaks with an infinite-lifetime instance of MarshalByRefObject

Posted in .NET Framework, Software Design by Nathan B. Evans on April 17, 2011

Recently we discovered an issue with the way our product performs AppDomain sandboxing. We were leaking small amounts of memory, quite badly, from each sandbox and every sandbox operation ever created over the lifetime of the process. After much investigation using CLR Profiler, it transpired that our subclasses of MarshalByRefObject which were using “null” as the return from their InitializeLifetimeService() will cause a permanent reference to be held open by some fairly low level area in the .NET remoting stack, specifically System.Runtime.Remoting.ServerIdentity (which is marked internal). This was preventing any of our object(s) derived from MarshalByRefObject from being garbage collected, and thus the memory footprint would keep growing and growing. Fortunately we run our servers with rather huge page files so it never got to a point where customers were affected, but obviously it was something we needed to fix fairly urgently.

After playing around a lot with all the lifetime services stuff like the ISponsor interface to do sponsorship with nasty hacky timeout values etc (which would have had side-affects for our product, but which we were about to resign ourselves too!) we came across a much better alternative solution in the form of RemotingServices.Disconnect(). Hoorah. Is it just me or does the whole remoting story in .NET need a damn good overhaul? Cross-AppDomain communications deserves something better.

With this discovery, I came up with a useful class that improves MarshalByRefObject  by adding deterministic disposal of both itself and any such nested objects (which is more an implementation detail for us but I’m sure could be useful for anyone).

/// <summary>
/// Enables access to objects across application domain boundaries.
/// This type differs from <see cref="MarshalByRefObject"/> by ensuring that the
/// service lifetime is managed deterministically by the consumer.
/// </summary>
public abstract class CrossAppDomainObject : MarshalByRefObject, IDisposable {

    private bool _disposed; 

    /// <summary>
    /// Gets an enumeration of nested <see cref="MarshalByRefObject"/> objects.
    /// </summary>
    protected virtual IEnumerable<MarshalByRefObject> NestedMarshalByRefObjects {
        get { yield break; }
    }

    ~CrossAppDomainObject() {
        Dispose(false);
    }

    /// <summary>
    /// Disconnects the remoting channel(s) of this object and all nested objects.
    /// </summary>
    private void Disconnect() {
        RemotingServices.Disconnect(this);

        foreach (var tmp in NestedMarshalByRefObjects)
            RemotingServices.Disconnect(tmp);
    }

    public sealed override object InitializeLifetimeService() {
        //
        // Returning null designates an infinite non-expiring lease.
        // We must therefore ensure that RemotingServices.Disconnect() is called when
        // it's no longer needed otherwise there will be a memory leak.
        //
        return null;
    }

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

    protected virtual void Dispose(bool disposing) {
        if (_disposed)
            return;

        Disconnect();
        _disposed = true;
    }

}

It was then just a case of modifying a few of our classes to derive from this instead of MarshalByRefObject and then update a couple other locations in our codebase by ensuring that Dispose() was called during clean-up.