.NET Core でアセンブリのアンローダビリティを使用およびデバッグする方法How to use and debug assembly unloadability in .NET Core

.NET Core 3.0 以降では、一連のアセンブリをロードし、後でアンロードする機能がサポートされます。Starting with .NET Core 3.0, the ability to load and later unload a set of assemblies is supported. .NET Framework では、この目的でカスタム アプリ ドメインが使用されていましたが、.NET Core では、既定のアプリ ドメインは 1 つしかサポートされません。In .NET Framework, custom app domains were used for this purpose, but .NET Core only supports a single default app domain.

このため、.NET Core 3.0 以降のバージョンでは、アンロードをサポートするために AssemblyLoadContext が用意されています。.NET Core 3.0 and later versions support unloadability through AssemblyLoadContext. 一連のアセンブリを収集可能な AssemblyLoadContext にロードし、その中のメソッドを実行するか、またはリフレクションを使用して調べた後、最後に AssemblyLoadContext をアンロードすることができます。You can load a set of assemblies into a collectible AssemblyLoadContext, execute methods in them or just inspect them using reflection, and finally unload the AssemblyLoadContext. これにより、AssemblyLoadContext にロードされたアセンブリがアンロードされます。That unloads the assemblies loaded into the AssemblyLoadContext.

AssemblyLoadContext を使用したアンロードと AppDomain を使用したアンロードには、1 つの注目すべき違いがあります。There's one noteworthy difference between the unloading using AssemblyLoadContext and using AppDomains. AppDomain では、アンロードが強制されます。With AppDomains, the unloading is forced. アンロード時には、ターゲット AppDomain で実行中のすべてのスレッドが中止されたり、ターゲット AppDomain で作成されたマネージド COM オブジェクトが破棄されたりといったことが行われます。At unload time, all threads running in the target AppDomain are aborted, managed COM objects created in the target AppDomain are destroyed, and so on. AssemblyLoadContext では、アンロードは "協調的" です。With AssemblyLoadContext, the unload is "cooperative". AssemblyLoadContext.Unload メソッドを呼び出すと、アンロードが単に開始されるだけです。Calling the AssemblyLoadContext.Unload method just initiates the unloading. アンロードは次の場合に完了します。The unloading finishes after:

  • コール スタックに、AssemblyLoadContext にロードされたアセンブリ内のメソッドを含むスレッドがなくなった。No threads have methods from the assemblies loaded into the AssemblyLoadContext on their call stacks.
  • AssemblyLoadContext にロードされたアセンブリ内の型、それらの型のインスタンス、およびアセンブリ自体が、次の方法により参照されなくなった。None of the types from the assemblies loaded into the AssemblyLoadContext, instances of those types, and the assemblies themselves are referenced by:

収集可能な AssemblyLoadContext の使用Use collectible AssemblyLoadContext

このセクションは、収集可能な AssemblyLoadContext に NET Core アプリケーションをロードして、そのエントリ ポイントを実行した後、NET Core アプリケーションをアンロードするための簡単な方法を詳細に説明したステップバイステップのチュートリアルです。This section contains a detailed step-by-step tutorial that shows a simple way to load a .NET Core application into a collectible AssemblyLoadContext, execute its entry point, and then unload it. 完全なサンプルについては、https://github.com/dotnet/samples/tree/master/core/tutorials/Unloading を参照してください。You can find a complete sample at https://github.com/dotnet/samples/tree/master/core/tutorials/Unloading.

収集可能な AssemblyLoadContext を作成するCreate a collectible AssemblyLoadContext

AssemblyLoadContext からクラスを派生させ、その AssemblyLoadContext.Load メソッドをオーバーライドする必要があります。You need to derive your class from the AssemblyLoadContext and override its AssemblyLoadContext.Load method. このメソッドは、その AssemblyLoadContext に読み込まれたアセンブリと依存関係にあるすべてのアセンブリへの参照を解決します。That method resolves references to all assemblies that are dependencies of assemblies loaded into that AssemblyLoadContext.

次のコードは、最も簡単なカスタム AssemblyLoadContext の例を示します。The following code is an example of the simplest custom AssemblyLoadContext:

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

    protected override Assembly Load(AssemblyName name)
    {
        return null;
    }
}

ご覧のとおり、Load メソッドから null が返されます。As you can see, the Load method returns null. つまり、すべての依存関係アセンブリが既定のコンテキストに読み込まれ、新しいコンテキストには、そこに明示的に読み込まれたアセンブリのみが含まれます。That means that all the dependency assemblies are loaded into the default context, and the new context contains only the assemblies explicitly loaded into it.

一部またはすべての依存関係も AssemblyLoadContext に読み込む必要がある場合は、Load メソッドで AssemblyDependencyResolver を使用することができます。If you want to load some or all of the dependencies into the AssemblyLoadContext too, you can use the AssemblyDependencyResolver in the Load method. AssemblyDependencyResolver によりアセンブリ名が絶対アセンブリ ファイル パスに解決されます。The AssemblyDependencyResolver resolves the assembly names to absolute assembly file paths. リゾルバーでは、コンテキストに読み込まれたメイン アセンブリのディレクトリ内の .deps.json ファイルとアセンブリ ファイルが使用されます。The resolver uses the .deps.json file and assembly files in the directory of the main assembly loaded into the context.

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

namespace complex
{
    class TestAssemblyLoadContext : AssemblyLoadContext
    {
        private AssemblyDependencyResolver _resolver;

        public TestAssemblyLoadContext(string mainAssemblyToLoadPath) : base(isCollectible: true)
        {
            _resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath);
        }

        protected override Assembly Load(AssemblyName name)
        {
            string assemblyPath = _resolver.ResolveAssemblyToPath(name);
            if (assemblyPath != null)
            {
                return LoadFromAssemblyPath(assemblyPath);
            }

            return null;
        }
    }
}

カスタムの収集可能な AssemblyLoadContext を使用するUse a custom collectible AssemblyLoadContext

このセクションでは、より単純な TestAssemblyLoadContext が使用されていることを前提とします。This section assumes the simpler version of the TestAssemblyLoadContext is being used.

次のように、カスタム AssemblyLoadContext のインスタンスを作成して、アセンブリをそこに読み込むことができます。You can create an instance of the custom AssemblyLoadContext and load an assembly into it as follows:

var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);

読み込まれたアセンブリによって参照される各アセンブリについて、TestAssemblyLoadContext.Load メソッドを呼び出して、TestAssemblyLoadContext がどこからアセンブリを取得すればよいかを判断できるようにします。For each of the assemblies referenced by the loaded assembly, the TestAssemblyLoadContext.Load method is called so that the TestAssemblyLoadContext can decide where to get the assembly from. この例では、null が返されます。これは、ランタイムでアセンブリを読み込むために既定で使用される場所から既定のコンテキストに読み込む必要があることを示します。In our case, it returns null to indicate that it should be loaded into the default context from locations that the runtime uses to load assemblies by default.

アセンブリが読み込まれたので、そこからメソッドを実行できます。Now that an assembly was loaded, you can execute a method from it. Main メソッドを実行します。Run the Main method:

var args = new object[1] {new string[] {"Hello"}};
int result = (int) a.EntryPoint.Invoke(null, args);

Main メソッドが返ると、カスタム AssemblyLoadContextUnload メソッドを呼び出すか、AssemblyLoadContext に対する参照を削除することにより、アンロードを開始できます。After the Main method returns, you can initiate unloading by either calling the Unload method on the custom AssemblyLoadContext or getting rid of the reference you have to the AssemblyLoadContext:

alc.Unload();

これだけでテスト アセンブリをアンロードできます。This is sufficient to unload the test assembly. では、実際に、これらすべてをインライン化できない個別のメソッドに配置して、TestAssemblyLoadContextAssembly、および MethodInfo (Assembly.EntryPoint) が、スタック スロット参照 (実数または JIT 対応のローカル) でキープ アライブできないようにしましょう。Let's actually put all of this into a separate non-inlineable method to ensure that the TestAssemblyLoadContext, Assembly, and MethodInfo (the Assembly.EntryPoint) can't be kept alive by stack slot references (real- or JIT-introduced locals). これにより、TestAssemblyLoadContext をキープ アライブし、アンロードを妨ぐことができます。That could keep the TestAssemblyLoadContext alive and prevent the unload.

また、AssemblyLoadContext への弱い参照が返されるので、後でこれを使用して、アンロードの完了を検出できます。Also, return a weak reference to the AssemblyLoadContext so that you can use it later to detect unload completion.

[MethodImpl(MethodImplOptions.NoInlining)]
static int ExecuteAndUnload(string assemblyPath, out WeakReference alcWeakRef)
{
    var alc = new TestAssemblyLoadContext();
    Assembly a = alc.LoadFromAssemblyPath(assemblyPath);

    alcWeakRef = new WeakReference(alc, trackResurrection: true);

    var args = new object[1] {new string[] {"Hello"}};
    int result = (int) a.EntryPoint.Invoke(null, args);

    alc.Unload();

    return result;
}

これで、この関数を実行して、アセンブリのロード、実行、アンロードを行うことができるようになりました。Now you can run this function to load, execute, and unload the assembly.

WeakReference testAlcWeakRef;
int result = ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);

ただし、アンロードは即座には完了しません。However, the unload doesn't complete immediately. 前述のとおり、これはガベージ コレクターに依存して、テスト アセンブリからすべてのオブジェクトを収集します。As previously mentioned, it relies on the garbage collector to collect all the objects from the test assembly. 多くの場合、アンロードは完了するまで待機する必要はありません。In many cases, it isn't necessary to wait for the unload completion. しかしながら、アンロードが完了したことを認識できると便利な場合があります。However, there are cases where it's useful to know that the unload has finished. たとえば、ディスクからカスタム AssemblyLoadContext にロードされたアセンブリ ファイルを削除したい場合などです。For example, you may want to delete the assembly file that was loaded into the custom AssemblyLoadContext from disk. このような場合、以下のコード スニペットを使用できます。In such a case, the following code snippet can be used. これは、ガベージ コレクションをトリガーし、カスタム AssemblyLoadContext への弱い参照が、ターゲット オブジェクトが収集されたことを示す null に設定されるまで、ループ内の保留中のファイナライザーを待機します。It triggers garbage collection and waits for pending finalizers in a loop until the weak reference to the custom AssemblyLoadContext is set to null, indicating the target object was collected. ほとんどの場合、ループは 1 回だけ通る必要があります。In most cases, just one pass through the loop is required. しかしながら、より複雑な場合、つまり AssemblyLoadContext で実行されるコードによって作成されるオブジェクトにファイナライザーが含まれている場合は、ループを複数回通ることが必要になる可能性があります。However, for more complex cases where objects created by the code running in the AssemblyLoadContext have finalizers, more passes may be needed.

for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

アンロード イベントThe Unloading event

アンロードの開始時に何らかのクリーンアップを実行するために、カスタム AssemblyLoadContext にロードされたコードが必要になることがあります。In some cases, it may be necessary for the code loaded into a custom AssemblyLoadContext to perform some cleanup when the unloading is initiated. たとえば、スレッドの停止や厳密な GC ハンドルのクリーンアップが必要になる場合があります。For example, it may need to stop threads or clean up strong GC handles. このような場合、Unloading イベントを使用できます。The Unloading event can be used in such cases. 必要なクリーンアップを実行するハンドラーをこのイベントにフックすることができます。A handler that performs the necessary cleanup can be hooked to this event.

アンローダビリティ問題のトラブルシューティングTroubleshoot unloadability issues

アンロードの協調的な性質のために、収集可能な AssemblyLoadContext の内容をキープ アライブしてアンロードを妨げている可能性がある参照のことを忘れてしまいがちです。Due to the cooperative nature of the unloading, it's easy to forget about references that may be keeping the stuff in a collectible AssemblyLoadContext alive and preventing unload. 以下に、参照を保持できるエンティティの概要を示します (一部は明確ではありません)。Here is a summary of entities (some of them non-obvious) that can hold the references:

  • 収集可能な AssemblyLoadContext の外部から保持される標準参照は、スタック スロットまたはプロセッサ レジスタ (ユーザー コードで明示的に、または Just-In-Time (JIT) コンパイラで暗黙的に作成されるメソッド ローカル)、静的変数、または厳密な (固定の) GC ハンドルに格納され、次のものを推移的に指します。Regular references held from outside of the collectible AssemblyLoadContext that are stored in a stack slot or a processor register (method locals, either explicitly created by the user code or implicitly by the just-in-time (JIT) compiler), a static variable, or a strong (pinning) GC handle, and transitively pointing to:
    • 収集可能な AssemblyLoadContext に読み込まれたアセンブリ。An assembly loaded into the collectible AssemblyLoadContext.
    • このようなアセンブリ内の型。A type from such an assembly.
    • このようなアセンブリ内の型のインスタンス。An instance of a type from such an assembly.
  • 収集可能な AssemblyLoadContext に読み込まれたアセンブリからコードを実行するスレッド。Threads running code from an assembly loaded into the collectible AssemblyLoadContext.
  • 収集可能な AssemblyLoadContext 内部で作成されたカスタムの収集不可能な AssemblyLoadContext 型のインスタンス。Instances of custom, non-collectible AssemblyLoadContext types created inside of the collectible AssemblyLoadContext.
  • コールバックがカスタムの AssemblyLoadContext 内のメソッドに設定された保留中の RegisteredWaitHandle インスタンス。Pending RegisteredWaitHandle instances with callbacks set to methods in the custom AssemblyLoadContext.

ヒント

スタック スロットまたはプロセッサ レジスタに格納されていて、AssemblyLoadContext のアンロードを妨げる可能性があるオブジェクト参照は、次の状況で発生する可能性があります。Object references that are stored in stack slots or processor registers and that could prevent unloading of an AssemblyLoadContext can occur in the following situations:

  • ユーザーが作成したローカル変数がなくても、関数呼び出しの結果が別の関数に直接渡される場合。When function call results are passed directly to another function, even though there is no user-created local variable.
  • JIT コンパイラが、メソッドのある時点で使用可能だったオブジェクトへの参照を保持している場合。When the JIT compiler keeps a reference to an object that was available at some point in a method.

アンロードに関する問題のデバッグDebug unloading issues

アンロードに関する問題のデバッグは、面倒な場合があります。Debugging issues with unloading can be tedious. 何が AssemblyLoadContext を保持しているかはわからないが、アンロードが失敗する状況に陥ることがあります。You can get into situations where you don't know what can be holding an AssemblyLoadContext alive, but the unload fails. 最も役立つツールは、WinDbg (Unix では LLDB) と SOS プラグインです。The best weapon to help with that is WinDbg (LLDB on Unix) with the SOS plugin. 特定の AssemblyLoadContext に属する LoaderAllocator をキープ アライブしているのが何かを突き止める必要があります。You need to find what's keeping a LoaderAllocator belonging to the specific AssemblyLoadContext alive. SOS プラグインを使用すると、GC ヒープ オブジェクト、その階層、およびルートを調べることができます。The SOS plugin allows you to look at GC heap objects, their hierarchies, and roots.

プラグインをデバッガーに読み込むには、次のコマンドをデバッガーのコマンド ラインに入力します。To load the plugin into the debugger, enter the following command in the debugger command line:

WinDbg の場合 (WinDbg では、.NET Core アプリケーションを中断すると、自動的に実行されるようです):In WinDbg (it seems WinDbg does that automatically when breaking into .NET Core application):

.loadby sos coreclr

LLDB の場合:In LLDB:

plugin load /path/to/libsosplugin.so

アンロードの問題が発生するプログラム例をデバッグしましょう。Let's debug an example program that has problems with unloading. ソース コードの内容は次のとおりです。Source code is included below. WinDbg でこのプログラムを実行すると、このデバッガーでは、アンロードが成功したかどうかを確認しようとした直後で中断されます。When you run it under WinDbg, the program breaks into the debugger right after attempting to check for the unload success. この後、原因の究明を開始できます。You can then start looking for the culprits.

ヒント

UNIX で LLDB を使用してデバッグする場合は、次の例の SOS コマンドの前に ! がないようにします。If you debug using LLDB on Unix, the SOS commands in the following examples don't have the ! in front of them.

!dumpheap -type LoaderAllocator

このコマンドは、GC ヒープ内にあり、型名に LoaderAllocator を含むすべてのオブジェクトをダンプします。This command dumps all objects with a type name containing LoaderAllocator that are in the GC heap. たとえば次のようになります。Here is an example:

         Address               MT     Size
000002b78000ce40 00007ffadc93a288       48
000002b78000ceb0 00007ffadc93a218       24

Statistics:
              MT    Count    TotalSize Class Name
00007ffadc93a218        1           24 System.Reflection.LoaderAllocatorScout
00007ffadc93a288        1           48 System.Reflection.LoaderAllocator
Total 2 objects

次の例の "Statistics:" パーツで、System.Reflection.LoaderAllocator に属する MT (MethodTable) を確認します。これが、注目すべきオブジェクトです。In the "Statistics:" part below, check the MT (MethodTable) belonging to the System.Reflection.LoaderAllocator, which is the object we care about. 次に、最初にリストでこのエントリと MT が一致するエントリを見つけて、オブジェクト自体のアドレスを取得します。Then, in the list at the beginning, find the entry with MT matching that one and get the address of the object itself. この例では、"000002b78000ce40" です。In our case, it is "000002b78000ce40".

LoaderAllocator オブジェクトのアドレスがわかったので、別のコマンドを使用して GC ルートを見つけることができます。Now that we know the address of the LoaderAllocator object, we can use another command to find its GC roots:

!gcroot -all 0x000002b78000ce40

このコマンドは、LoaderAllocator インスタンスが発生するオブジェクト参照のチェーンをダンプします。This command dumps the chain of object references that lead to the LoaderAllocator instance. リストはルートから始まります。ルートは、LoaderAllocator をキープ アライブしているエンティティであるため、問題の核心になります。The list starts with the root, which is the entity that keeps our LoaderAllocator alive and thus is the core of the problem. ルートは、スタック スロット、プロセッサ レジスタ、GC ハンドル、または静的変数のいずれかです。The root can be a stack slot, a processor register, a GC handle, or a static variable.

gcroot コマンドの出力例を次に示します。Here is an example of the output of the gcroot command:

Thread 4ac:
    000000cf9499dd20 00007ffa7d0236bc example.Program.Main(System.String[]) [E:\unloadability\example\Program.cs @ 70]
        rbp-20: 000000cf9499dd90
            ->  000002b78000d328 System.Reflection.RuntimeMethodInfo
            ->  000002b78000d1f8 System.RuntimeType+RuntimeTypeCache
            ->  000002b78000d1d0 System.RuntimeType
            ->  000002b78000ce40 System.Reflection.LoaderAllocator

HandleTable:
    000002b7f8a81198 (strong handle)
    -> 000002b78000d948 test.Test
    -> 000002b78000ce40 System.Reflection.LoaderAllocator

    000002b7f8a815f8 (pinned handle)
    -> 000002b790001038 System.Object[]
    -> 000002b78000d390 example.TestInfo
    -> 000002b78000d328 System.Reflection.RuntimeMethodInfo
    -> 000002b78000d1f8 System.RuntimeType+RuntimeTypeCache
    -> 000002b78000d1d0 System.RuntimeType
    -> 000002b78000ce40 System.Reflection.LoaderAllocator

Found 3 roots.

次の手順は、修正するために、ルートがどこにあるかを特定することです。The next step is to figure out where the root is located so you can fix it. 最も簡単なケースは、ルートがスタック スロットまたはプロセッサ レジスタである場合です。The easiest case is when the root is a stack slot or a processor register. この場合、gcroot には、フレームにルートとその関数を実行するスレッドを含む関数の名前が表示されます。In that case, the gcroot shows the name of the function whose frame contains the root and the thread executing that function. 難解なケースは、ルートが静的変数または GC ハンドルの場合です。The difficult case is when the root is a static variable or a GC handle.

前述の例では、1 番目のルートは、アドレス rbp-20 にある関数 example.Program.Main(System.String[]) のフレームに格納されている System.Reflection.RuntimeMethodInfo 型のローカルです (rbp はプロセッサ レジスタ rbp で、-20 は、そのレジスタからの 16 進のオフセットです)。In the previous example, the first root is a local of type System.Reflection.RuntimeMethodInfo stored in the frame of the function example.Program.Main(System.String[]) at address rbp-20 (rbp is the processor register rbp and -20 is a hexadecimal offset from that register).

2 番目のルートは通常の (強い) GCHandle で、test.Test クラスのインスタンスへの参照を保持します。The second root is a normal (strong) GCHandle that holds a reference to an instance of the test.Test class.

3 番目のルートは、固定の GCHandle です。The third root is a pinned GCHandle. これは実際には静的変数ですが、残念ながら、これを確認する方法はありません。This one is actually a static variable, but unfortunately, there is no way to tell. 参照型の静的変数は、内部のランタイム構造体内のマネージド オブジェクト配列に格納されます。Statics for reference types are stored in a managed object array in internal runtime structures.

AssemblyLoadContext のアンロードが妨げられる可能性のあるもう 1 つのケースは、スタック上の AssemblyLoadContext に読み込まれたアセンブリ内のメソッドのフレームがスレッドに含まれている場合です。Another case that can prevent unloading of an AssemblyLoadContext is when a thread has a frame of a method from an assembly loaded into the AssemblyLoadContext on its stack. すべてのスレッドのマネージド コール スタックをダンプすると、これを確認できます。You can check that by dumping managed call stacks of all threads:

~*e !clrstack

コマンドは、"すべてのスレッドに !clrstack コマンドを適用する" ことを意味します。The command means "apply to all threads the !clrstack command". このコマンドの出力例を次に示します。The following is the output of that command for the example. 残念ながら、UNIX の LLDB には、すべてのスレッドにコマンドを適用する方法がないため、手動でスレッドを切り替えて、clrstack コマンドを繰り返す必要があります。Unfortunately, LLDB on Unix doesn't have any way to apply a command to all threads, so you must manually switch threads and repeat the clrstack command. デバッガーで "マネージド スタックを調べることができません" と表示されたすべてのスレッドを無視します。Ignore all threads where the debugger says "Unable to walk the managed stack".

OS Thread Id: 0x6ba8 (0)
        Child SP               IP Call Site
0000001fc697d5c8 00007ffb50d9de12 [HelperMethodFrame: 0000001fc697d5c8] System.Diagnostics.Debugger.BreakInternal()
0000001fc697d6d0 00007ffa864765fa System.Diagnostics.Debugger.Break()
0000001fc697d700 00007ffa864736bc example.Program.Main(System.String[]) [E:\unloadability\example\Program.cs @ 70]
0000001fc697d998 00007ffae5fdc1e3 [GCFrame: 0000001fc697d998]
0000001fc697df28 00007ffae5fdc1e3 [GCFrame: 0000001fc697df28]
OS Thread Id: 0x2ae4 (1)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x61a4 (2)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x7fdc (3)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x5390 (4)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x5ec8 (5)
        Child SP               IP Call Site
0000001fc70ff6e0 00007ffb5437f6e4 [DebuggerU2MCatchHandlerFrame: 0000001fc70ff6e0]
OS Thread Id: 0x4624 (6)
        Child SP               IP Call Site
GetFrameContext failed: 1
0000000000000000 0000000000000000
OS Thread Id: 0x60bc (7)
        Child SP               IP Call Site
0000001fc727f158 00007ffb5437fce4 [HelperMethodFrame: 0000001fc727f158] System.Threading.Thread.SleepInternal(Int32)
0000001fc727f260 00007ffb37ea7c2b System.Threading.Thread.Sleep(Int32)
0000001fc727f290 00007ffa865005b3 test.Program.ThreadProc() [E:\unloadability\test\Program.cs @ 17]
0000001fc727f2c0 00007ffb37ea6a5b System.Threading.Thread.ThreadMain_ThreadStart()
0000001fc727f2f0 00007ffadbc4cbe3 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
0000001fc727f568 00007ffae5fdc1e3 [GCFrame: 0000001fc727f568]
0000001fc727f7f0 00007ffae5fdc1e3 [DebuggerU2MCatchHandlerFrame: 0000001fc727f7f0]

ご覧のとおり、最後のスレッドには、test.Program.ThreadProc() が含まれています。As you can see, the last thread has test.Program.ThreadProc(). これは、AssemblyLoadContext に読み込まれたアセンブリ内の関数であるため、これが AssemblyLoadContext をキープ アライブしています。This is a function from the assembly loaded into the AssemblyLoadContext, and so it keeps the AssemblyLoadContext alive.

アンローダビリティの問題を発生するソース例Example source with unloadability issues

前述のデバッグ例では、次のコードが使用されています。The following code is used in the previous debugging example.

メインのテスト プログラムMain testing program

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

namespace example
{
    class TestAssemblyLoadContext : AssemblyLoadContext
    {
        public TestAssemblyLoadContext() : base(true)
        {
        }
        protected override Assembly Load(AssemblyName name)
        {
            return null;
        }
    }

    class TestInfo
    {
        public TestInfo(MethodInfo mi)
        {
            entryPoint = mi;
        }
        MethodInfo entryPoint;
    }

    class Program
    {
        static TestInfo entryPoint;

        [MethodImpl(MethodImplOptions.NoInlining)]
        static int ExecuteAndUnload(string assemblyPath, out WeakReference testAlcWeakRef, out MethodInfo testEntryPoint)
        {
            var alc = new TestAssemblyLoadContext();
            testAlcWeakRef = new WeakReference(alc);

            Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
            if (a == null)
            {
                testEntryPoint = null;
                Console.WriteLine("Loading the test assembly failed");
                return -1;
            }

            var args = new object[1] {new string[] {"Hello"}};

            // Issue preventing unloading #1 - we keep MethodInfo of a method for an assembly loaded into the TestAssemblyLoadContext in a static variable
            entryPoint = new TestInfo(a.EntryPoint);
            testEntryPoint = a.EntryPoint;

            int result = (int)a.EntryPoint.Invoke(null, args);
            alc.Unload();

            return result;
        }

        static void Main(string[] args)
        {
            WeakReference testAlcWeakRef;
            // Issue preventing unloading #2 - we keep MethodInfo of a method for an assembly loaded into the TestAssemblyLoadContext in a local variable
            MethodInfo testEntryPoint;
            int result = ExecuteAndUnload(@"absolute/path/to/test.dll", out testAlcWeakRef, out testEntryPoint);

            for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }

            System.Diagnostics.Debugger.Break();

            Console.WriteLine($"Test completed, result={result}, entryPoint: {testEntryPoint} unload success: {!testAlcWeakRef.IsAlive}");
        }
    }
}

TestAssemblyLoadContext に読み込まれたプログラムProgram loaded into the TestAssemblyLoadContext

次のコードは、メインのテスト プログラムで ExecuteAndUnload メソッドに渡された test.dll を表しています。The following code represents the test.dll passed to the ExecuteAndUnload method in the main testing program.

using System;
using System.Runtime.InteropServices;
using System.Threading;

namespace test
{
    class Test
    {
        string message = "Hello";
    }

    class Program
    {
        public static void ThreadProc()
        {
            // Issue preventing unloading #4 - a thread running method inside of the TestAssemblyLoadContext at the unload time
            Thread.Sleep(Timeout.Infinite);
        }

        static GCHandle handle;
        static int Main(string[] args)
        {
            // Issue preventing unloading #3 - normal GC handle
            handle = GCHandle.Alloc(new Test());
            Thread t = new Thread(new ThreadStart(ThreadProc));
            t.IsBackground = true;
            t.Start();
            Console.WriteLine($"Hello from the test: args[0] = {args[0]}");

            return 1;
        }
    }
}