To determine the cause of an assembly load context leak, in the leak report, read PathToRoot from the first item toward the root:
AssemblyName is one of your own assemblies identifies which object leaked. This is your starting point, not necessarily the reference to remove.Root.Kind, together with the last path items before the root, to decide what to remove:
StaticField: Root.FieldName names the field that retains the chain. Clear or reset that field.Handle: A GCHandle retains the object. The path often doesn’t contain the code that allocated the handle (as in the previous example, where MyGame.AssetIndex is 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 with -record-managed-allocation-callstacks 1 and read Root.StackTrace, which gives the call stack where the handle was allocated. Free that specific handle.Stack, Finalizer, or Other: 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.If the type name and root kind still don’t locate the code, record allocation call stacks to add a StackTrace to each path item and to the handle root.
When the type name and the root kind aren’t enough to locate the code that creates the reference, restart the Editor with the -record-managed-allocation-callstacks command-line argument. The argument adds a StackTrace member to the report, and takes the following values:
1: Records call stacks for GCHandle allocations.2: Records call stacks for GCHandle allocations and for all managed object allocations.
Important: Level 2 records a call stack for every managed allocation, which slows the Editor down and increases the memory it uses. Use it to diagnose a specific leak, then restart the Editor without the argument.
A strong or pinning GCHandle 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 GCHandleType.Weak doesn’t prevent unloading.
In the report, Root.Kind is Handle, and Root.Flags contains Pinning for a pinned handle.
To free the handle before the Editor unloads your code, follow these steps:
partial.[OnCodeUnloading], which the Editor calls before it unloads your assemblies.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();
}
}
}
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 Task.Run or to a Thread constructor also captures the enclosing instance when the lambda reads an instance member, so one long-running task can hold the whole object graph.
In the report, Root.Kind is Stack, and PathToRoot often includes compiler-generated types such as AsyncStateMachineBox.
To stop the background work before the Editor unloads your code, follow these steps:
CancellationToken to the work, and poll the token in the body of the work.partial.[OnCodeUnloading].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.
}
}
}
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:
In the report, PathToRoot ends in types that don’t belong to your assemblies, such as System.EventHandler followed by System.AppDomain. For a static field, Root.Kind is StaticField and Root.FieldName names the field.
To remove the reference before the Editor unloads your code, follow these steps:
partial.[OnCodeUnloading].Where your code subscribes from a method marked with [OnCodeLoaded], unsubscribe from the matching [OnCodeUnloading] method to keep the two callbacks symmetrical.
The following example unsubscribes from an event on AppDomain, which belongs to the base class library and stays loaded across reloads:
using 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);
}
}