Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loading/unloading same assembly in a loop does not actually unload #11366

Closed
jkotas opened this issue Oct 30, 2018 · 4 comments
Closed

Loading/unloading same assembly in a loop does not actually unload #11366

jkotas opened this issue Oct 30, 2018 · 4 comments
Labels
Milestone

Comments

@jkotas
Copy link
Member

jkotas commented Oct 30, 2018

Repro:

using System;
using System.Reflection;
using System.Runtime.Loader;

class SimpleUnloadableAssemblyLoadContext : AssemblyLoadContext
{
    public SimpleUnloadableAssemblyLoadContext()
       : base(isCollectible: true)
    {
    }

    protected override Assembly Load(AssemblyName assemblyName) => null;
}

class Program
{
    private static int ExecuteAssembly(Assembly assembly, string[] args)
    {
        MethodInfo entry = assembly.EntryPoint;

        object result = entry.GetParameters().Length > 0 ?
                    entry.Invoke(null, new object[] { args }) :
                    entry.Invoke(null, null);

        return (result != null) ? (int)result : 0;
    }

    static void Main(string[] args)
    {
        for (;;) {
            var context = new SimpleUnloadableAssemblyLoadContext();
            Assembly assembly = context.LoadFromAssemblyPath(@"D:\repro\hello\bin\Debug\netcoreapp3.0\hello.dll");
            ExecuteAssembly(assembly, Array.Empty<string>());
            context.Unload();

            GC.Collect();

            Console.WriteLine($"Number of loaded assemblies: {AppDomain.CurrentDomain.GetAssemblies().Length}");
        }
    }
}

Update path to hello.dll (it is the stock hello world from dotnet new console).

Result: Number of assemblies and process memory loaded keep growing indefinitely. They never unload.

Tested on 3.0.0-preview1-27029-03 (CoreCLR: https://github.com/dotnet/coreclr/tree/4fbeb37c62427b51410217cd49cec84e03fdca34).

@jkotas
Copy link
Member Author

jkotas commented Oct 30, 2018

cc @janvorli

Found while trying to build an example for https://github.com/dotnet/corefx/issues/19773

@janvorli
Copy link
Member

This slightly modified sample works as expected. I need to look at the generated code difference, since it seems that your example should work too.

using System;
using System.Reflection;
using System.Runtime.Loader;
using System.Runtime.CompilerServices;

class SimpleUnloadableAssemblyLoadContext : AssemblyLoadContext
{
    public SimpleUnloadableAssemblyLoadContext()
       : base(isCollectible: true)
    {
    }

    protected override Assembly Load(AssemblyName assemblyName) => null;
}

class Program
{
    private static int ExecuteAssembly(Assembly assembly, string[] args)
    {
        MethodInfo entry = assembly.EntryPoint;

        object result = entry.GetParameters().Length > 0 ?
                    entry.Invoke(null, new object[] { args }) :
                    entry.Invoke(null, null);

        return (result != null) ? (int)result : 0;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static int ExecuteAndUnload(string[] args)
    {
        var context = new SimpleUnloadableAssemblyLoadContext();
        Assembly assembly = context.LoadFromAssemblyPath(@"D:\repro\hello\bin\Debug\netcoreapp3.0\hello.dll");
        int st = ExecuteAssembly(assembly, Array.Empty<string>());
        context.Unload();

        return st;
    }

    static void Main(string[] args)
    {
        for (;;) {
            ExecuteAndUnload(Array.Empty<string>());

            GC.Collect();

            Console.WriteLine($"Number of loaded assemblies: {AppDomain.CurrentDomain.GetAssemblies().Length}");
        }
    }
}

@janvorli
Copy link
Member

Ok, so the reason why it keeps the assemblies in your version is really simple after all. It is this call:
AppDomain.CurrentDomain.GetAssemblies().Length. JIT creates a local (Assembly[]) for the result of AppDomain.CurrentDomain.GetAssemblies(). So once you call that, all the assemblies are rooted through that. In the next loop, this local is updated with the new list of assemblies that obviously includes all the previous ones etc etc.

@jkotas
Copy link
Member Author

jkotas commented Oct 31, 2018

Ah, makes sense. Thanks for taking a look

@jkotas jkotas closed this as completed Oct 31, 2018
@msftgits msftgits transferred this issue from dotnet/coreclr Jan 31, 2020
@msftgits msftgits added this to the 3.0 milestone Jan 31, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 15, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

3 participants