Skip to content

Commit b4dd93a

Browse files
committed
Add unloadability howto document
This document describes how to use unloadability in .NET Core 3.0 and how to debug issues with unloading. It also contains a link to a complete sample in dotnet/samples repo.
1 parent 9254ff5 commit b4dd93a

File tree

3 files changed

+250
-1
lines changed

3 files changed

+250
-1
lines changed

docs/standard/assembly/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,12 @@ Assemblies form the fundamental unit of deployment, version control, reuse, acti
4848
4949
## See also
5050

51-
- [.NET assembly file format](file-format.md)
51+
- [.NET assembly file format](file-format.md)
5252
- [Assemblies in the Common Language Runtime](../../framework/app-domains/assemblies-in-the-common-language-runtime.md)
5353
- [Friend Assemblies (C#)](../../csharp/programming-guide/concepts/assemblies-gac/friend-assemblies.md)
5454
- [Friend Assemblies (Visual Basic)](../../visual-basic/programming-guide/concepts/assemblies-gac/friend-assemblies.md)
5555
- [How to: Load and Unload Assemblies (C#)](../../csharp/programming-guide/concepts/assemblies-gac/how-to-load-and-unload-assemblies.md)
5656
- [How to: Load and Unload Assemblies (Visual Basic)](../../visual-basic/programming-guide/concepts/assemblies-gac/how-to-load-and-unload-assemblies.md)
57+
- [How to: Use and Debug Assembly Unloadability in .NET Core](unloadability-howto.md)
5758
- [How to: Determine If a File Is an Assembly (C#)](../../csharp/programming-guide/concepts/assemblies-gac/how-to-determine-if-a-file-is-an-assembly.md)
5859
- [How to: Determine If a File Is an Assembly (Visual Basic)](../../visual-basic/programming-guide/concepts/assemblies-gac/how-to-determine-if-a-file-is-an-assembly.md)
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
---
2+
title: "How to use and debug assembly unloadability in .NET Core"
3+
description: "Learn how to use collectible AssemblyLoadContext for loading and unloading managed assemblies and how to debug issues preventing the unloading success."
4+
author: "janvorli"
5+
ms.author: "janvorli"
6+
ms.date: "02/05/2019"
7+
---
8+
# How to use and debug assembly unloadability in .NET Core
9+
10+
Starting with .NET Core 3.0, the ability to load and later unload a set of assemblies is supported. In .NET Framework, custom app domains were used for this purpose, but .NET Core only supports a single default app domain.
11+
12+
.NET Core 3.0 and later versions support unloadability through <xref:System.Runtime.Loader.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`. That unloads the assemblies loaded into that `AssemblyLoadContext`.
13+
14+
There's one noteworthy difference between the unloading using `AssemblyLoadContext` and using AppDomains. With AppDomains, the unloading is forced. At the unload time, all threads running in the target AppDomain are aborted, managed COM objects created in the target AppDomain are destroyed, etc. With `AssemblyLoadContext`, the unload is "cooperative". Calling the <xref:System.Runtime.Loader.AssemblyLoadContext.Unload%2A?displayProperty=nameWithType> method just initiates the unloading. The unloading finishes after:
15+
16+
- No threads have methods from the assemblies loaded into the `AssemblyLoadContext` on their call stacks.
17+
- None of the types from the assemblies loaded into the `AssemblyLoadContext`, instances of those types and the assemblies themselves outside of the `AssemblyLoadContext` are referenced by:
18+
- References outside of the `AssemblyLoadContext`, except of weak references (<xref:System.WeakReference> or <xref:System.WeakReference%601>).
19+
- Strong GC handles (<xref:System.Runtime.InteropServices.GCHandleType>.<xref:System.Runtime.InteropServices.GCHandleType.Normal> or <xref:System.Runtime.InteropServices.GCHandleType>.<xref:System.Runtime.InteropServices.GCHandleType.Pinned>) from both inside and outside of the `AssemblyLoadContext`.
20+
21+
## Using collectible AssemblyLoadContext
22+
23+
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. You can find a complete sample at <https://github.com/dotnet/samples/tree/master/core/tutorials/Unloading>.
24+
25+
### Create a collectible AssemblyLoadContext
26+
27+
You need to derive your class from the <xref:System.Runtime.Loader.AssemblyLoadContext> and overload its <xref:System.Runtime.Loader.AssemblyLoadContext.Load%2A?displayProperty=nameWithType> method. That method resolves references to all assemblies that are dependencies of assemblies loaded into that `AssemblyLoadContext`.
28+
The following code is an example of the simplest custom `AssemblyLoadContext`:
29+
30+
[!code-csharp[Simple custom AssemblyLoadContext](~/samples/snippets/standard/assembly/unloading/simple_example.cs#1)]
31+
32+
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.
33+
34+
If you want to load some or all of the dependencies into the `AssemblyLoadContext` too, you can use the `AssemblyDependencyResolver` in the `Load` method. The `AssemblyDependencyResolver` resolves the assembly names to absolute assembly file paths using the `*.deps.json` file contained in the directory of the main assembly loaded into the context and using assembly files in that directory.
35+
36+
[!code-csharp[Advanced custom AssemblyLoadContext](~/samples/snippets/standard/assembly/unloading/complex_assemblyloadcontext.cs)]
37+
38+
### Use a custom collectible AssemblyLoadContext
39+
40+
This section assumes the simpler version of the `TestAssemblyLoadContext` is being used.
41+
42+
You can create an instance of the custom `AssemblyLoadContext` and load an assembly into it as follows:
43+
44+
[!code-csharp[Part 1](~/samples/snippets/standard/assembly/unloading/simple_example.cs#3)]
45+
46+
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. 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.
47+
48+
Now that an assembly was loaded, you can execute a method from it. Run the `Main` method:
49+
50+
[!code-csharp[Part 2](~/samples/snippets/standard/assembly/unloading/simple_example.cs#4)]
51+
52+
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`:
53+
54+
[!code-csharp[Part 3](~/samples/snippets/standard/assembly/unloading/simple_example.cs#5)]
55+
56+
This is sufficient to unload the test assembly. 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). That could keep the `TestAssemblyLoadContext` alive and prevent the unload.
57+
58+
Also, return a weak reference to the `AssemblyLoadContext` so that you can use it later to detect unload completion.
59+
60+
[!code-csharp[Part 4](~/samples/snippets/standard/assembly/unloading/simple_example.cs#2)]
61+
62+
Now you can run this function to load, execute, and unload the assembly.
63+
64+
[!code-csharp[Part 5](~/samples/snippets/standard/assembly/unloading/simple_example.cs#6)]
65+
66+
However, the unload doesn't complete immediately. As previously mentioned, it relies on the GC 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. 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. It triggers a GC 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. Note that in most cases, just one pass through the loop is required. However, for more complex cases where objects created by the code running in the `AssemblyLoadContext` have finalizers, more passes may be needed.
67+
68+
[!code-csharp[Part 6](~/samples/snippets/standard/assembly/unloading/simple_example.cs#7)]
69+
70+
### The Unloading event
71+
72+
In some cases, it may be necessary for the code loaded into a custom `AssemblyLoadContext` to perform some cleanup when the unloading is initiated. For example, it may need to stop threads, clean up some strong GC handles, etc. The `Unloading` event can be used in such cases. A handler that performs the necessary cleanup can be hooked to this event.
73+
74+
### Troubleshoot unloadability issues
75+
76+
Due to the cooperative nature of the unloading, it's easy to forget about references keeping the stuff in a collectible `AssemblyLoadContext` alive and preventing unload. Here is a summary of things (some of them non-obvious) that can hold the references:
77+
78+
- Regular references held from outside of the collectible `AssemblyLoadContext`, stored in a stack slot or a processor register (method locals, either explicitly created by the user code or implicitly by the JIT), a static variable or a strong / pinning GC handle, and transitively pointing to:
79+
- An assembly loaded into the collectible `AssemblyLoadContext`.
80+
- A type from such an assembly.
81+
- An instance of a type from such an assembly.
82+
- Threads running code from an assembly loaded into the collectible `AssemblyLoadContext`.
83+
- Instances of custom non-collectible `AssemblyLoadContext` types created inside of the collectible `AssemblyLoadContext`
84+
- Pending <xref:System.Threading.RegisteredWaitHandle> instances with callbacks set to methods in the custom `AssemblyLoadContext`
85+
86+
Hints to find stack slot / processor register rooting an object:
87+
88+
- Passing function call results directly to another function may create a root even though there is no user-created local variable.
89+
- If a reference to an object was available at any point in a method, the JIT might have decided to keep the reference in a stack slot / processor register for as long as it wants in the current function.
90+
91+
## Debug unloading issues
92+
93+
Debugging issues with unloading can be tedious. You can get into situations where you don't know what can be holding an `AssemblyLoadContext` alive, but the unload fails.
94+
The best weapon to help with that is WinDbg (LLDB on Unix) with the SOS plugin. You need to find what's keeping a `LoaderAllocator` belonging to the specific `AssemblyLoadContext` alive.
95+
This plugin allows you to look at GC heap objects, their hierarchies, and roots.
96+
To load the plugin into the debugger, enter the following command in the debugger command line:
97+
98+
In WinDbg (it seems WinDbg does that automatically when breaking into .NET Core application):
99+
100+
```console
101+
.loadby sos coreclr
102+
```
103+
104+
In LLDB:
105+
106+
```console
107+
plugin load /path/to/libsosplugin.so
108+
```
109+
110+
Let's try to debug an example program that has problems with unloading. Source code is included below. 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.
111+
112+
Note that if you debug using LLDB on Unix, the SOS commands in the following examples don't have the `!` in front of them.
113+
114+
```console
115+
!dumpheap -type LoaderAllocator
116+
```
117+
118+
This command dumps all objects with a type name containing `LoaderAllocator` that are in the GC heap. Here is an example:
119+
120+
```console
121+
Address MT Size
122+
000002b78000ce40 00007ffadc93a288 48
123+
000002b78000ceb0 00007ffadc93a218 24
124+
125+
Statistics:
126+
MT Count TotalSize Class Name
127+
00007ffadc93a218 1 24 System.Reflection.LoaderAllocatorScout
128+
00007ffadc93a288 1 48 System.Reflection.LoaderAllocator
129+
Total 2 objects
130+
```
131+
132+
In the "Statistics:" part below, check the `MT` (`MethodTable`) belonging to the `System.Reflection.LoaderAllocator`, which is the object we care about. Then in the list at the beginning, find the entry with `MT` matching that one and get the address of the object itself. In our case, it is "000002b78000ce40"
133+
134+
Now that we know the address of the `LoaderAllocator` object, we can use another command to find its GC roots
135+
136+
```console
137+
!gcroot -all 0x000002b78000ce40
138+
```
139+
140+
This command dumps the chain of object references that lead to the `LoaderAllocator` instance. The list starts with the root, which is the entity that keeps our `LoaderAllocator` alive and thus is the core of the problem you're debugging. The root can be a stack slot, a processor register, a GC handle, or a static variable.
141+
142+
Here is an example of the output of the `gcroot` command:
143+
144+
```console
145+
Thread 4ac:
146+
000000cf9499dd20 00007ffa7d0236bc example.Program.Main(System.String[]) [E:\unloadability\example\Program.cs @ 70]
147+
rbp-20: 000000cf9499dd90
148+
-> 000002b78000d328 System.Reflection.RuntimeMethodInfo
149+
-> 000002b78000d1f8 System.RuntimeType+RuntimeTypeCache
150+
-> 000002b78000d1d0 System.RuntimeType
151+
-> 000002b78000ce40 System.Reflection.LoaderAllocator
152+
153+
HandleTable:
154+
000002b7f8a81198 (strong handle)
155+
-> 000002b78000d948 test.Test
156+
-> 000002b78000ce40 System.Reflection.LoaderAllocator
157+
158+
000002b7f8a815f8 (pinned handle)
159+
-> 000002b790001038 System.Object[]
160+
-> 000002b78000d390 example.TestInfo
161+
-> 000002b78000d328 System.Reflection.RuntimeMethodInfo
162+
-> 000002b78000d1f8 System.RuntimeType+RuntimeTypeCache
163+
-> 000002b78000d1d0 System.RuntimeType
164+
-> 000002b78000ce40 System.Reflection.LoaderAllocator
165+
166+
Found 3 roots.
167+
```
168+
169+
Now you need 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. In that case, the `gcroot` shows you the name of the function whose frame contains the root and the thread executing that function. The difficult case is when the root is a static variable or a GC handle.
170+
171+
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).
172+
173+
The second root is a normal (strong) `GCHandle` that holds a reference to an instance of the `test.Test` class.
174+
175+
The third root is a pinned `GCHandle`. This one is actually a static variable. Unfortunately, there is no way to tell. Statics for reference types are stored in a managed object array in internal runtime structures.
176+
177+
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:
178+
179+
```console
180+
~*e !clrstack
181+
```
182+
183+
The command means "apply to all threads the `!clrstack` command". Here is the output of that command for our example. Unfortunately, LLDB on Unix doesn't have any way to apply a command to all threads, so you'll need to resort to manually switching threads and repeating the `clrstack` command.
184+
You should ignore all threads where the debugger says "Unable to walk the managed stack."
185+
186+
```console
187+
OS Thread Id: 0x6ba8 (0)
188+
Child SP IP Call Site
189+
0000001fc697d5c8 00007ffb50d9de12 [HelperMethodFrame: 0000001fc697d5c8] System.Diagnostics.Debugger.BreakInternal()
190+
0000001fc697d6d0 00007ffa864765fa System.Diagnostics.Debugger.Break()
191+
0000001fc697d700 00007ffa864736bc example.Program.Main(System.String[]) [E:\unloadability\example\Program.cs @ 70]
192+
0000001fc697d998 00007ffae5fdc1e3 [GCFrame: 0000001fc697d998]
193+
0000001fc697df28 00007ffae5fdc1e3 [GCFrame: 0000001fc697df28]
194+
OS Thread Id: 0x2ae4 (1)
195+
Unable to walk the managed stack. The current thread is likely not a
196+
managed thread. You can run !threads to get a list of managed threads in
197+
the process
198+
Failed to start stack walk: 80070057
199+
OS Thread Id: 0x61a4 (2)
200+
Unable to walk the managed stack. The current thread is likely not a
201+
managed thread. You can run !threads to get a list of managed threads in
202+
the process
203+
Failed to start stack walk: 80070057
204+
OS Thread Id: 0x7fdc (3)
205+
Unable to walk the managed stack. The current thread is likely not a
206+
managed thread. You can run !threads to get a list of managed threads in
207+
the process
208+
Failed to start stack walk: 80070057
209+
OS Thread Id: 0x5390 (4)
210+
Unable to walk the managed stack. The current thread is likely not a
211+
managed thread. You can run !threads to get a list of managed threads in
212+
the process
213+
Failed to start stack walk: 80070057
214+
OS Thread Id: 0x5ec8 (5)
215+
Child SP IP Call Site
216+
0000001fc70ff6e0 00007ffb5437f6e4 [DebuggerU2MCatchHandlerFrame: 0000001fc70ff6e0]
217+
OS Thread Id: 0x4624 (6)
218+
Child SP IP Call Site
219+
GetFrameContext failed: 1
220+
0000000000000000 0000000000000000
221+
OS Thread Id: 0x60bc (7)
222+
Child SP IP Call Site
223+
0000001fc727f158 00007ffb5437fce4 [HelperMethodFrame: 0000001fc727f158] System.Threading.Thread.SleepInternal(Int32)
224+
0000001fc727f260 00007ffb37ea7c2b System.Threading.Thread.Sleep(Int32)
225+
0000001fc727f290 00007ffa865005b3 test.Program.ThreadProc() [E:\unloadability\test\Program.cs @ 17]
226+
0000001fc727f2c0 00007ffb37ea6a5b System.Threading.Thread.ThreadMain_ThreadStart()
227+
0000001fc727f2f0 00007ffadbc4cbe3 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
228+
0000001fc727f568 00007ffae5fdc1e3 [GCFrame: 0000001fc727f568]
229+
0000001fc727f7f0 00007ffae5fdc1e3 [DebuggerU2MCatchHandlerFrame: 0000001fc727f7f0]
230+
231+
```
232+
233+
As you can see, the last thread has `test.Program.ThreadProc()`. This is a function from the assembly loaded into the `AssemblyLoadContext`, and so it keeps the `AssemblyLoadContext` alive.
234+
235+
## Example source with unloadability issues
236+
237+
This example is used in the debugging above.
238+
239+
### Main testing program
240+
241+
[!code-csharp[Main testing program](~/samples/snippets/standard/assembly/unloading/unloadability_issues_example_main.cs)]
242+
243+
## Program loaded into the TestAssemblyLoadContext
244+
245+
This is the `test.dll` passed to the `ExecuteAndUnload` method in the main testing program.
246+
247+
[!code-csharp[Program loaded into the TestAssemblyLoadContext](~/samples/snippets/standard/assembly/unloading/unloadability_issues_example_test.cs)]

docs/toc.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
## [Handling and throwing exceptions](standard/exceptions/)
3434
## [Assemblies in .NET](standard/assembly/index.md)
3535
### [.NET Assembly File Format](standard/assembly/file-format.md)
36+
### [How to: Use and Debug Assembly Unloadability in .NET Core](standard/assembly/unloadability-howto.md)
3637
## [Garbage Collection](standard/garbage-collection/)
3738
## [Generic types](standard/generics.md)
3839
## [Delegates and lambdas](standard/delegates-lambdas.md)

0 commit comments

Comments
 (0)