.NET Core 3.0 introduced collectible AssemblyLoadContext
, which allows to call Unload()
method to unload assemblies loaded inside the context.
As per documentation (https://learn.microsoft.com/en-us/dotnet/standard/assembly/unloadability#troubleshoot-unloadability-issues), unloading is asynchronous and any reference to the context or objects from it will prevent context from unloading.
I was wondering what if I loose my reference to AssemblyLoadContext
, will this cause a leak (since I have no more context to call Unload()
on). The test proved that this won't cause a leak and unused assembly will be unloaded even without Unload()
called explicitly:
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using NUnit.Framework;
namespace Tests.Core
{
[TestFixture]
public class CollectibleAssemblyLoadContextTests
{
private const string AssemblyName = "Test___DynamicAssembly";
[Test]
[TestCase(/*unload*/ true, /*GC sessions*/ 1)]
[TestCase(/*unload*/ false, /*GC sessions*/ 2)]
public void ShouldExecuteAndUnload(bool unload, int expectedGcSessions)
{
string actual = Execute(10, unload);
Assert.AreEqual("executed 10", actual);
int gcSessions = 0;
while (!IsUnloaded())
{
GC.Collect();
gcSessions++;
}
Assert.AreEqual(expectedGcSessions, gcSessions);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private bool IsUnloaded()
{
return !AppDomain.CurrentDomain.GetAssemblies()
.Select(x => x.GetName().Name)
.Contains(AssemblyName);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private string Execute(int number, bool unload)
{
var source = @"
public static class Process
{
public static string Execute(int i)
{
return $""executed {i}"";
}
}";
var compilation = CSharpCompilation.Create(AssemblyName, new[] {CSharpSyntaxTree.ParseText(source)},
new []{MetadataReference.CreateFromFile(typeof(object).Assembly.Location)},
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
using var ms = new MemoryStream();
compilation.Emit(ms);
ms.Seek(0, SeekOrigin.Begin);
var assemblyLoadContext = new AssemblyLoadContext("CollectibleContext", isCollectible: true);
Assembly assembly = assemblyLoadContext.LoadFromStream(ms);
if (unload)
assemblyLoadContext.Unload();
Type type = assembly.GetType("Process");
MethodInfo method = type.GetMethod("Execute");
return (string)method.Invoke(null, new object[] {number});
}
}
}
What this test also shows is that using Unload()
get the context unloaded after 1 GC session, whether without Unload()
it takes 2 sessions to unload. But can be just coincidence and not always reproducible.
So, given that
Unload()
just after loading all the assemblies you need to schedule unload when it won't be in use).Unload()
collectible context is unloaded once it is not in use anymore.What is the purpose of this Unload()
method and what is the difference between using Unload()
and simply relying on GC?
I had the same impression and the tests I did showed the same. It looked like it doesn't make sense to call Unload()
explicitly. Then I found https://github.com/dotnet/samples/blob/master/core/tutorials/Unloading and realized that if you comment Unload()
there, the lib won't be unloaded. This is the line https://github.com/dotnet/samples/blob/master/core/tutorials/Unloading/Host/Program.cs#L74.
Eventually it is like Lasse V. Karlsen says. If you call Unload()
explicitly, you can expect that your library will be unloaded faster.