Diagnose and fix assembly load context leaks
Fix AssemblyLoadContext leaks by following the recommended practices for code reload and assembly load context management.
Read time 6 minutesLast updated 8 days ago
To determine the cause of an assembly load context leak, in the leak report, read from the first item toward the root:
PathToRoot-
The first item whoseis one of your own assemblies identifies which object leaked. This is your starting point, not necessarily the reference to remove.
AssemblyName -
The reference that keeps the chain alive is held at the root end of the chain. Inspect, together with the last path items before the root, to decide what to remove:
Root.Kind- :
StaticFieldnames the field that retains the chain. Clear or reset that field.Root.FieldName - : A
Handleretains the object. The path often doesn't contain the code that allocated the handle (as in the previous example, whereGCHandleis the only path item). In this case the type and root kind are not enough to find the reference, especially when several handles point to the same type. Restart the Editor withMyGame.AssetIndexand read-record-managed-allocation-callstacks 1, which gives the call stack where the handle was allocated. Free that specific handle.Root.StackTrace - ,
Stack, orFinalizer: The object is retained by a local on a thread stack, a pending finalizer, or another runtime root. Use the path items to identify the referencing type.Other
If the type name and root kind still don't locate the code, record allocation call stacks to add a to each path item and to the handle root.
StackTraceRecord allocation call stacks
When the type name and the root kind aren't enough to locate the code that creates the reference, restart the Editor with the command-line argument. The argument adds a member to the report, and takes the following values:
-record-managed-allocation-callstacksStackTrace- : Records call stacks for
1allocations.GCHandle - : Records call stacks for
2allocations and for all managed object allocations.GCHandle
Handles that are never freed
A strong or pinning keeps its target alive regardless of any other reference, so the context that owns the target's type can't unload. A handle that you allocate with doesn't prevent unloading.
GCHandleGCHandleType.WeakIn the report, is , and contains for a pinned handle.
Root.KindHandleRoot.FlagsPinningResolution
To free the handle before the Editor unloads your code, follow these steps:
- Declare the type that allocates the handle as .
partial - Add a static method to that type and mark it with , which the Editor calls before it unloads your assemblies.
[OnCodeUnloading] - Free the handle in that method.
The following example frees a handle that holds an asset index:
using System.Runtime.InteropServices;using Unity.Scripting.LifecycleManagement;public static partial class AssetIndexBridge{ private static GCHandle s_IndexHandle; public static void Register(AssetIndex index) { if (s_IndexHandle.IsAllocated) { s_IndexHandle.Free(); } s_IndexHandle = GCHandle.Alloc(index); } // Free the handle before the Editor unloads this assembly. [OnCodeUnloading] static void FreeIndexHandle() { if (s_IndexHandle.IsAllocated) { s_IndexHandle.Free(); } }}
Background tasks that outlive the reload
A thread that runs a method from your assemblies keeps that code alive, because the method sits on the call stack of the thread. A lambda that you pass to or to a constructor also captures the enclosing instance when the lambda reads an instance member, so one long-running task can hold the whole object graph.
Task.RunThreadIn the report, is , and often includes compiler-generated types such as .
Root.KindStackPathToRootAsyncStateMachineBoxResolution
To stop the background work before the Editor unloads your code, follow these steps:
- Pass a to the work, and poll the token in the body of the work.
CancellationToken - Declare the type that starts the work as .
partial - Add a static method to that type and mark it with .
[OnCodeUnloading] - Cancel the work and wait for it to finish in that method.
The following example stops an indexing task:
using System.Threading;using System.Threading.Tasks;using Unity.Scripting.LifecycleManagement;public static partial class AssetIndexer{ private static CancellationTokenSource s_Cancellation; private static Task s_IndexTask; public static void StartIndexing() { // Stop any previous run before replacing the fields. StopIndexing(); s_Cancellation = new CancellationTokenSource(); // Copy the token into a local variable, so that the lambda captures // the token instead of the enclosing type. CancellationToken token = s_Cancellation.Token; s_IndexTask = Task.Run(() => BuildIndex(token)); } // Stop the task and wait for it to return, so that no thread runs this // code when the Editor unloads the assembly. [OnCodeUnloading] static void StopIndexing() { if (s_IndexTask == null) { return; } s_Cancellation.Cancel(); s_IndexTask.Wait(); s_Cancellation.Dispose(); s_IndexTask = null; s_Cancellation = null; } static void BuildIndex(CancellationToken token) { while (!token.IsCancellationRequested) { // Index one asset per iteration. } }}
References from code that isn't reloaded
The Editor reloads your scripts and packages, but the .NET base class library and Unity's own assemblies stay loaded across reloads. A reference from code that isn't reloaded to a type in your code prevents your context from unloading. This reference takes two common forms:
- An event on a long-lived object that your code subscribes to and never unsubscribes from. The delegate holds a reference to your method, and to the enclosing instance for an instance method.
- A static collection or field on a long-lived type that your code adds an entry to and never removes.
In the report, ends in types that don't belong to your assemblies, such as followed by . For a static field, is and names the field.
PathToRootSystem.EventHandlerSystem.AppDomainRoot.KindStaticFieldRoot.FieldNameResolution
To remove the reference before the Editor unloads your code, follow these steps:
- Declare the type that creates the reference as .
partial - Add a static method to that type and mark it with .
[OnCodeUnloading] - Unsubscribe from the event, or remove the entry from the collection, in that method.
Where your code subscribes from a method marked with , unsubscribe from the matching method to keep the two callbacks symmetrical.
[OnCodeLoaded][OnCodeUnloading]The following example unsubscribes from an event on , which belongs to the base class library and stays loaded across reloads:
AppDomainusing System;using Unity.Scripting.LifecycleManagement;using UnityEngine;public static partial class CrashReporter{ [OnCodeLoaded] static void Subscribe() { AppDomain.CurrentDomain.UnhandledException += LogException; } // Unsubscribe so that the event no longer references code in this assembly. [OnCodeUnloading] static void Unsubscribe() { AppDomain.CurrentDomain.UnhandledException -= LogException; } static void LogException(object sender, UnhandledExceptionEventArgs args) { Debug.LogError(args.ExceptionObject); }}