Documentation

Unity Engine


Unity Engine


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
PathToRoot
from the first item toward the root:
  • The first item whose
    AssemblyName
    is one of your own assemblies identifies which object leaked. This is your starting point, not necessarily the reference to remove.
  • The reference that keeps the chain alive is held at the root end of the chain. Inspect
    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.

Record 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
-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.

Handles that are never freed

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.

Resolution

To free the handle before the Editor unloads your code, follow these steps:
  1. Declare the type that allocates the handle as
    partial
    .
  2. Add a static method to that type and mark it with
    [OnCodeUnloading]
    , which the Editor calls before it unloads your assemblies.
  3. 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
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
.

Resolution

To stop the background work before the Editor unloads your code, follow these steps:
  1. Pass a
    CancellationToken
    to the work, and poll the token in the body of the work.
  2. Declare the type that starts the work as
    partial
    .
  3. Add a static method to that type and mark it with
    [OnCodeUnloading]
    .
  4. 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,
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.

Resolution

To remove the reference before the Editor unloads your code, follow these steps:
  1. Declare the type that creates the reference as
    partial
    .
  2. Add a static method to that type and mark it with
    [OnCodeUnloading]
    .
  3. Unsubscribe from the event, or remove the entry from the collection, in that method.
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); }}

Additional resources