Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
.NET (Core) hat die Möglichkeit zum Laden und späteren Entladen einer Gruppe von Assemblys eingeführt. In .NET Framework wurden benutzerdefinierte App-Domänen zu diesem Zweck verwendet, aber .NET (Core) unterstützt nur eine einzige Standard-App-Domäne.
Unloadability wird durch AssemblyLoadContextunterstützt. Sie können eine Gruppe von Assemblys in eine sammelbare AssemblyLoadContext laden, Methoden darin ausführen oder einfach nur mithilfe der Reflexion prüfen und schließlich die AssemblyLoadContextAssemblys wieder entladen. Dadurch werden die Assemblys entlädt, die in die AssemblyLoadContext geladen wurden.
Es gibt einen bemerkenswerten Unterschied zwischen dem Entladen mit AssemblyLoadContext und der Verwendung von AppDomains. Mit AppDomains wird das Entladen erzwungen. Beim Entladen werden alle Threads, die in der Ziel-AppDomain ausgeführt werden, abgebrochen, verwaltete COM-Objekte, die in der Ziel-AppDomain erstellt wurden, zerstört usw. Mit AssemblyLoadContext ist das Entladen "kooperativ". Durch Aufrufen der AssemblyLoadContext.Unload Methode wird nur die Entladung initiiert. Die Entladung endet nach:
- Keine Threads haben Methoden aus den Assemblys in ihren Aufrufstapeln, die in die
AssemblyLoadContextgeladen wurden. - Auf keine der Typen aus den geladenen Assemblys, die Instanzen dieser Typen und die Assemblys selbst wird verwiesen von:
- Bezüge außerhalb der
AssemblyLoadContext, mit Ausnahme schwacher Bezüge (WeakReference oder WeakReference<T>). - Starke Garbage Collector (GC) Handles (GCHandleType.Normal oder GCHandleType.Pinned) von innen und außerhalb der
AssemblyLoadContext.
- Bezüge außerhalb der
Verwenden von „collectible AssemblyLoadContext“
Dieser Abschnitt enthält eine detaillierte Schritt-für-Schritt-Anleitung, die eine einfache Möglichkeit zeigt, eine .NET (Core)-Anwendung in ein sammelbares AssemblyLoadContext zu laden, deren Einstiegspunkt auszuführen und sie dann wieder zu entladen. Ein vollständiges Beispiel finden Sie unter https://github.com/dotnet/samples/tree/main/core/tutorials/Unloading.
Erstellen eines sammelbaren AssemblyLoadContexts
Leiten Sie Ihre Klasse von der AssemblyLoadContext ab und überschreiben Sie die AssemblyLoadContext.Load Methode. Diese Methode löst Verweise auf alle Assemblys auf, die Abhängigkeiten von in diese AssemblyLoadContext geladenen Assemblys sind.
Der folgende Code ist ein Beispiel für den einfachsten benutzerdefinierten AssemblyLoadContextCode:
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(isCollectible: true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
Wie Sie sehen können, gibt die Load Methode zurück null. Das bedeutet, dass alle Abhängigkeitsassemblys in den Standardkontext geladen werden und der neue Kontext nur die Assemblys enthält, die explizit in den Kontext geladen wurden.
Wenn Sie auch einige oder alle Abhängigkeiten AssemblyLoadContext laden möchten, können Sie AssemblyDependencyResolver in der Load-Methode verwenden. Der AssemblyDependencyResolver löst die Assemblynamen in absolute Assemblydateipfade auf. Der Resolver verwendet die .deps.json Datei und die Assemblydateien im Verzeichnis der Hauptassembly, die in den Kontext geladen ist.
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;
}
}
}
Benutzerdefinierten auflösbaren AssemblyLoadContext verwenden
In diesem Abschnitt wird davon ausgegangen, dass die einfachere Version von TestAssemblyLoadContext verwendet wird.
Sie können eine Instanz der benutzerdefinierten AssemblyLoadContext erstellen und eine Assembly wie folgt darin laden:
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
Für jede der Assemblys, auf die von der geladenen Assembly verwiesen wird, wird die TestAssemblyLoadContext.Load Methode aufgerufen, sodass TestAssemblyLoadContext entscheiden kann, von woher die Assembly abgerufen werden soll. In diesem Fall gibt es null zurück, um anzugeben, dass es in den Standardkontext geladen werden soll, von den Speicherorten, die die Laufzeit nutzt, um Assemblys standardmäßig zu laden.
Nachdem eine Assembly geladen wurde, können Sie eine Methode daraus ausführen. Führen Sie die Main Methode aus:
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
Nachdem die Main Methode zurückkehrt, können Sie den Unladenvorgang starten, indem Sie entweder einen Aufruf an den benutzerdefinierten AssemblyLoadContext mittels der Unload Methode tätigen oder die Referenz entfernen, die Sie auf das folgende AssemblyLoadContext haben:
alc.Unload();
Dies reicht aus, um die Testassembly zu entladen. Als Nächstes fügen Sie all dies in eine separate, nichtlineable Methode ein, um sicherzustellen, dass die TestAssemblyLoadContext, Assemblyund MethodInfo (die Assembly.EntryPoint) nicht durch Stapelplatzverweise (real- oder JIT-eingeführte Lokale) lebendig gehalten werden können. Dies könnte das TestAssemblyLoadContext aktiv halten und das Entladen verhindern.
Geben Sie außerdem eine schwache Referenz auf AssemblyLoadContext zurück, damit Sie sie später verwenden können, um den Abschluss des Entladens zu erkennen.
[MethodImpl(MethodImplOptions.NoInlining)]
static void 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"}};
_ = a.EntryPoint?.Invoke(null, args);
alc.Unload();
}
Jetzt können Sie diese Funktion ausführen, um die Assembly zu laden, auszuführen und zu entladen.
WeakReference testAlcWeakRef;
ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);
Das Entladen wird jedoch nicht sofort abgeschlossen. Wie bereits erwähnt, verlässt sie sich auf den Garbage Collector, um alle Objekte aus der Test-Assembly zu sammeln. In vielen Fällen ist es nicht erforderlich, auf den Abschluss des Entladens zu warten. Es gibt jedoch Fälle, in denen es hilfreich ist, zu wissen, dass das Entladen abgeschlossen ist. Sie können beispielsweise die Assemblydatei löschen, die auf den benutzerdefinierten AssemblyLoadContext Datenträger geladen wurde. In diesem Fall kann der folgende Codeausschnitt verwendet werden. Sie löst die Garbage Collection aus und wartet in einer Schleife auf ausstehende Finalizer, bis der schwache Verweis auf den benutzerdefinierten AssemblyLoadContext festgelegt ist null, was angibt, dass das Zielobjekt aufgesammelt wurde. In den meisten Fällen ist nur ein Durchlauf der Schleife erforderlich. Bei komplexeren Fällen, in denen Objekte, die vom Code erstellt wurden, der im AssemblyLoadContext ausgeführt wird und Finalizer hat, sind möglicherweise weitere Durchläufe erforderlich.
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Einschränkungen
Assemblies, die in einem sammelbaren AssemblyLoadContext geladen werden, müssen die allgemeinen Einschränkungen für sammelbare Assemblys einhalten. Die folgenden Einschränkungen gelten zusätzlich:
- Assemblys, die in C++/CLI geschrieben wurden, werden nicht unterstützt.
- ReadyToRun generierter Code wird ignoriert.
Das Entladungsereignis
de-DE: In einigen Fällen kann es erforderlich sein, dass der Code, der in ein benutzerdefiniertes AssemblyLoadContext geladen wird, einige Bereinigungsaktionen ausführt, wenn die Entladung initiiert wird. So kann es beispielsweise erforderlich sein, Threads zu beenden oder starke GC-Handles zu bereinigen. Das Unloading Ereignis kann in solchen Fällen verwendet werden. Sie können einen Handler an dieses Ereignis anhängen, der die erforderliche Bereinigung ausführt.
Beheben von Problemen mit der Entladebarkeit
Aufgrund der kooperativen Natur der Entladung ist es einfach, Verweise zu vergessen, die die Sachen in einem sammelbaren AssemblyLoadContext am Leben halten und das Entladen verhindern. Hier ist eine Zusammenfassung der Entitäten (einige davon nicht offensichtlich), die die Verweise enthalten können:
- Regelmäßige Verweise, die von außerhalb des sammelbaren
AssemblyLoadContextstammen und entweder in einem Stapelslot oder einem Prozessorregister (Methodenlokale, entweder explizit vom Benutzercode erstellt oder implizit vom Just-in-Time(JIT)-Compiler), einer statischen Variablen oder einem starken (Pinning)-GC-Handle gespeichert sind und transitiv verweisen auf:- Eine Assembly, die in das sammelbare Objekt
AssemblyLoadContextgeladen wurde. - Ein Typ aus einer solchen Baugruppe.
- Eine Instanz eines Typs aus einer solchen Assembly.
- Eine Assembly, die in das sammelbare Objekt
- Threads, die Code aus einer Assembly ausführen, die in das Sammelbare
AssemblyLoadContextgeladen wurde. - Instanzen von benutzerdefinierten, nicht sammelbaren
AssemblyLoadContextTypen, die innerhalb des sammelbarenAssemblyLoadContexterstellt wurden. - Ausstehende RegisteredWaitHandle Instanzen mit Rückrufen, die auf Methoden in der benutzerdefinierten
AssemblyLoadContextFestgelegt sind. - Felder in Ihrer benutzerdefinierten
AssemblyLoadContextUnterklasse, die auf Assemblys, Typen oder Instanzen von Typen verweisen, die in das sammelbareAssemblyLoadContextgeladen wurden. Während das Entladen erfolgt, hält die Laufzeit einen starken GC-Handle amAssemblyLoadContext, um das Entladen zu koordinieren. Dies bedeutet, dass die GC diese Feldverweise nicht sammelt, auch wenn Sie Ihren eigenen Verweis auf dieAssemblyLoadContext. Löschen Sie diese Felder, damit das Entladen abgeschlossen werden kann.
Tipp
Objektverweise, die in Stapelpositionen oder Prozessorregistern gespeichert sind und die das Entladen eines Objekts AssemblyLoadContext verhindern können, können in den folgenden Situationen auftreten:
- Wenn Die Ergebnisse des Funktionsaufrufs direkt an eine andere Funktion übergeben werden, obwohl keine vom Benutzer erstellte lokale Variable vorhanden ist.
- Wenn der JIT-Compiler einen Verweis auf ein Objekt speichert, das zu einem bestimmten Zeitpunkt in einer Methode verfügbar war.
Debuggen von Entladungsproblemen
Das Debuggen von Problemen beim Entladen kann mühsam sein. Sie können in Situationen geraten, in denen Sie nicht wissen, was eine AssemblyLoadContext lebendig halten kann, aber das Entladen schlägt fehl. Das beste Tool, um zu helfen, ist WinDbg (oder LLDB auf Unix) mit dem SOS-Plug-In. Sie müssen herausfinden, was eine LoaderAllocator von dem spezifischen AssemblyLoadContext am Leben hält. Mit dem SOS-Plug-In können Sie GC-Heap-Objekte, deren Hierarchien und Wurzeln betrachten.
Um das SOS-Plug-In in den Debugger zu laden, geben Sie einen der folgenden Befehle in der Befehlszeile des Debuggers ein.
In WinDbg (falls es noch nicht geladen ist):
.loadby sos coreclr
In LLDB:
plugin load /path/to/libsosplugin.so
Jetzt debuggen Sie ein Beispielprogramm, das Probleme beim Entladen hat. Der Quellcode ist im Abschnitt "Quellcodebeispiel " verfügbar. Wenn Sie es unter WinDbg ausführen, wechselt das Programm direkt nach dem Versuch, den Unload-Erfolg zu überprüfen, in den Debugger. Sie können dann nach den Tätern suchen.
Tipp
Wenn Sie unter Unix mit LLDB debuggen, haben die SOS-Befehle in den folgenden Beispielen kein ! davor.
!dumpheap -type LoaderAllocator
Mit diesem Befehl werden alle Objekte im GC-Heap aufgelistet, deren Typname LoaderAllocator enthält. Ein Beispiel:
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
Überprüfen Sie im Abschnitt "Statistics:" das MT (MethodTable), das zu dem System.Reflection.LoaderAllocator gehört und für Sie von Interesse ist. Suchen Sie dann in der Liste am Anfang den Eintrag mit MT, der mit diesem übereinstimmt, und rufen Sie die Adresse des Objekts ab. In diesem Fall ist es "000002b78000ce40".
Nachdem Sie nun die Adresse des LoaderAllocator Objekts kennen, können Sie einen anderen Befehl verwenden, um die GC-Wurzeln zu finden:
!gcroot 0x000002b78000ce40
Mit diesem Befehl wird die Kette der Objektverweise abgeschaltet, die LoaderAllocator zur Instanz führen. Die Liste beginnt mit der Wurzel, bei dem es sich um die Entität handelt, die das LoaderAllocator am Leben hält und daher der Kern des Problems ist. Der Stamm kann ein Stapelplatz, ein Prozessorregister, ein GC-Handle oder eine statische Variable sein.
Hier ist ein Beispiel für die Ausgabe des gcroot Befehls:
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.
Der nächste Schritt besteht darin, herauszufinden, wo sich der Stamm befindet, damit Sie ihn beheben können. Der einfachste Fall ist, wenn die Wurzel ein Stack-Slot oder ein Prozessorregister ist. In diesem Fall wird der gcroot Name der Funktion angezeigt, deren Frame den Stamm und den Thread enthält, der diese Funktion ausführt. Der schwierige Fall ist, wenn der Stamm eine statische Variable oder ein GC-Handle ist.
Im vorherigen Beispiel ist der erste Stamm ein lokaler Typ System.Reflection.RuntimeMethodInfo, der im Frame der Funktion example.Program.Main(System.String[]) an der Adresse rbp-20 gespeichert ist (rbp ist das Prozessorregister rbp und -20 ist ein hexadezimaler Versatz von diesem Register).
Der zweite Stamm ist ein normaler (starker) GCHandle Stamm, der einen Verweis auf eine Instanz der test.Test Klasse enthält.
Der dritte Stamm ist eine angeheftete GCHandle. Dies ist eigentlich eine statische Variable, aber leider gibt es keine Möglichkeit zu sagen. Statiken für Verweistypen werden in einem verwalteten Objektarray in internen Laufzeitstrukturen gespeichert.
Ein weiterer Fall, der das Entladen eines AssemblyLoadContext verhindern kann, besteht darin, dass ein Thread ein Stackframe einer Methode von einer Assembly auf seinem Stapel hat, die in den AssemblyLoadContext geladen wurde. Sie können dies überprüfen, indem Sie verwaltete Aufrufstapel aller Threads dumpingn:
~*e !clrstack
Der Befehl bedeutet "Für alle Threads den !clrstack Befehl übernehmen". Im Folgenden sehen Sie die Ausgabe dieses Befehls für das Beispiel. Leider hat LLDB unter Unix keine Möglichkeit, einen Befehl auf alle Threads anzuwenden, daher müssen Sie Threads manuell wechseln und den clrstack Befehl wiederholen. Ignorieren Sie alle Threads, in denen der Debugger "Der verwaltete Stapel kann nicht durchlaufen werden" sagt.
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]
Wie Sie sehen können, hat der letzte Thread test.Program.ThreadProc(). Dies ist eine Funktion aus der Assembly, die in AssemblyLoadContext geladen wird, und so hält sie das AssemblyLoadContext am Leben.
Beispielquellcode
Der folgende Code, der Unloadability-Probleme enthält, wird im vorherigen Debugbeispiel verwendet.
Haupttestprogramm
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;
var oResult = a.EntryPoint?.Invoke(null, args);
alc.Unload();
return (oResult is int result) ? result : -1;
}
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}");
}
}
}
Programm, das in den TestAssemblyLoadContext geladen wurde
Der folgende Code repräsentiert die an die Methode ExecuteAndUnload im Haupttestprogramm übergebene test.dll.
using System;
using System.Runtime.InteropServices;
using System.Threading;
namespace test
{
class Test
{
}
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;
}
}
}