Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
.NET (Core) heeft de mogelijkheid geïntroduceerd om een set assembly's te laden en later te verwijderen. In .NET Framework zijn hiervoor aangepaste app-domeinen gebruikt, maar .NET (Core) ondersteunt slechts één standaard-app-domein.
Ontlaadbaarheid wordt ondersteund via AssemblyLoadContext. U kunt een set assembly's laden in een verzamelbare AssemblyLoadContext, methoden erin uitvoeren of ze gewoon inspecteren met reflectie, en ten slotte de AssemblyLoadContext ontladen. Hiermee worden de assembly's die in AssemblyLoadContext zijn geladen, ontladen.
Er is één opmerkelijk verschil tussen het ontladen met AssemblyLoadContext en het gebruik van AppDomains. Met AppDomains wordt het ontladen geforceerd. Bij het uitladen worden alle threads die in het doel-AppDomain worden uitgevoerd, afgebroken, beheerde COM-objecten die in het doel-AppDomain zijn gemaakt, vernietigd, enzovoort. Met AssemblyLoadContext, de ontlading is "coöperatief". Door de AssemblyLoadContext.Unload methode aan te roepen, wordt alleen het lossen geïnitieerd. Het lossen is voltooid na:
- Er zijn geen threads die methoden hebben van de assembly's die in de
AssemblyLoadContextaanroepstacks zijn geladen. - Geen van de typen van de assembly's die in
AssemblyLoadContextzijn geladen, exemplaren van deze typen en de assembly's zelf worden aangeroepen door:- Verwijzingen buiten de
AssemblyLoadContext, met uitzondering van zwakke verwijzingen (WeakReference of WeakReference<T>). - Sterke garbagecollection (GC) ingangen (GCHandleType.Normal of GCHandleType.Pinned) van zowel binnen als buiten de
AssemblyLoadContext.
- Verwijzingen buiten de
Collectible AssemblyLoadContext gebruiken
Deze sectie bevat een gedetailleerde stapsgewijze zelfstudie waarin een eenvoudige manier wordt getoond om een .NET-toepassing (Core) in een verzamelbare AssemblyLoadContexttoepassing te laden, het ingangspunt uit te voeren en het vervolgens te verwijderen. U vindt een volledig voorbeeld op https://github.com/dotnet/samples/tree/main/core/tutorials/Unloading.
Een verzamelbare AssemblyLoadContext maken
Leid uw klasse af van de AssemblyLoadContext en overschrijf de AssemblyLoadContext.Load-methode. Deze methode lost verwijzingen op naar alle assembly's die afhankelijkheden zijn van assembly's die in dat AssemblyLoadContextbestand zijn geladen.
De volgende code is een voorbeeld van de eenvoudigste aangepaste AssemblyLoadContextcode:
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(isCollectible: true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
Zoals u ziet, retourneert de methode Loadnull. Dit betekent dat alle afhankelijkheidsassembly's in de standaardcontext worden geladen en dat de nieuwe context alleen de assembly's bevat die expliciet daarin zijn geladen.
Als u bepaalde of alle afhankelijkheden ook in de AssemblyLoadContext wilt laden, kunt u de AssemblyDependencyResolver in de Load-methode gebruiken. Hiermee AssemblyDependencyResolver worden de assemblynamen omgezet in absolute assemblybestandspaden. De resolver gebruikt de .deps.json bestands- en assemblybestanden in de map van de hoofdassembly die in de context is geladen.
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;
}
}
}
Een aangepaste collectible AssemblyLoadContext gebruiken
In deze sectie wordt ervan uitgegaan dat de eenvoudigere versie van de TestAssemblyLoadContext versie wordt gebruikt.
U kunt als volgt een exemplaar van de aangepaste AssemblyLoadContext maken en een assembly erin laden:
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
Voor elk van de assemblies waar de geladen assembly naar verwijst, wordt de TestAssemblyLoadContext.Load-methode aangeroepen zodat de TestAssemblyLoadContext kan beslissen waar de assembly vandaan gehaald moet worden. In dit geval retourneert het null om aan te geven dat het moet worden geladen in de standaardcontext vanaf locaties die de runtime standaard gebruikt voor het laden van assembly's.
Nu een assembly is geladen, kunt u er een methode van uitvoeren. Voer de Main methode uit:
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
Nadat de Main methode is geretourneerd, kunt u het loskoppelen initiëren door de Unload methode aan te roepen op de aangepaste AssemblyLoadContext of door de verwijzing naar het AssemblyLoadContext te verwijderen.
alc.Unload();
Dit is voldoende om de testopstelling te ontladen. Vervolgens plaatst u dit allemaal in een afzonderlijke niet-inlijnbare methode om ervoor te zorgen dat de TestAssemblyLoadContext, Assembly en MethodInfo (de Assembly.EntryPoint) niet kunnen worden bewaard door stackslot-verwijzingen (echte of door JIT geïntroduceerde locals). Dat kan het TestAssemblyLoadContext in leven houden en het ontladen verhinderen.
Retourneer ook een zwakke verwijzing naar de AssemblyLoadContext zodat u deze later kunt gebruiken om de voltooiing van het ontladen te detecteren.
[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();
}
U kunt deze functie nu uitvoeren om de assembly te laden, uit te voeren en te ontladen.
WeakReference testAlcWeakRef;
ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);
Het uitladen wordt echter niet onmiddellijk voltooid. **
Zoals eerder vermeld, is het afhankelijk van de garbagecollector om alle objecten uit de testassemblage te verzamelen. In veel gevallen is het niet nodig om te wachten totdat het laden is voltooid. Er zijn echter gevallen waarin het nuttig is om te weten dat de ontlading is voltooid. U kunt bijvoorbeeld het assemblybestand dat in de aangepaste AssemblyLoadContext vanaf de schijf was geladen, verwijderen. In dat geval kan het volgende codefragment worden gebruikt. Het activeert de garbagecollection en wacht in een lus op lopende finalizers totdat de zwakke verwijzing naar de aangepaste AssemblyLoadContext is ingesteld op null, wat aangeeft dat het doelobject is opgehaald. In de meeste gevallen is slechts één keer door de loop gaan vereist. Voor complexere gevallen waarin objecten die zijn gemaakt door de code die wordt uitgevoerd in de AssemblyLoadContext finalizers hebben, zijn er echter mogelijk meer doorgangen nodig.
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Beperkingen
Assemblies die in een verzamelobject AssemblyLoadContext zijn geladen, moeten voldoen aan de algemene beperkingen voor verzamelbare assemblies. De volgende beperkingen zijn ook van toepassing:
- Assembly's die zijn geschreven in C++/CLI worden niet ondersteund.
- ReadyToRun gegenereerde code wordt genegeerd.
Het lossingsevenement
In sommige gevallen kan het nodig zijn voor de code die in een aangepaste AssemblyLoadContext code is geladen om een opschoonbewerking uit te voeren wanneer het lossen wordt gestart. Het kan bijvoorbeeld nodig zijn om threads te stoppen of sterke GC-ingangen op te schonen. De Unloading gebeurtenis kan in dergelijke gevallen worden gebruikt. U kunt een handler koppelen die de benodigde opschoonactie voor deze gebeurtenis uitvoert.
Problemen met uitlaadbaarheid oplossen
Vanwege de coöperatieve aard van het lossen is het gemakkelijk om verwijzingen te vergeten die het spul in een verzamelbaar AssemblyLoadContext leven kunnen houden en het ontladen verhinderen. Hier volgt een samenvatting van entiteiten (sommige hiervan niet-evident) die de verwijzingen kunnen houden:
- Reguliere verwijzingen van buiten het verzamelobject
AssemblyLoadContextdie zijn opgeslagen in een stackslot of een processorregister (lokale variabelen, expliciet gemaakt door de gebruikerscode of impliciet door de Just-In-Time (JIT) compiler), een statische variabele of een sterke (pinning) GC-handle, en transitief verwijzen naar:- Een assembly die in het verzamelobject
AssemblyLoadContextis geladen. - Een type uit een dergelijke assembly.
- Een exemplaar van een type van een dergelijke assembly.
- Een assembly die in het verzamelobject
- Threads die code uitvoeren vanuit een assembly die in het verzamelobject
AssemblyLoadContextis geladen. - Exemplaren van aangepaste, niet-verzamelbare
AssemblyLoadContext-typen die zijn gemaakt binnen de verzamelbareAssemblyLoadContext. - Wachtende RegisteredWaitHandle exemplaren met callbacks ingesteld op de methoden in de aangepaste
AssemblyLoadContext. - Velden in uw aangepaste
AssemblyLoadContextsubklasse die verwijzen naar assembly's, typen of exemplaren van typen die in de verzamelbareAssemblyLoadContextklasse zijn geladen. Terwijl het lossen wordt uitgevoerd, heeft de runtime een sterke GC-handle op deAssemblyLoadContextom het ontladen te coördineren. Dit betekent dat de GC deze veldverwijzingen niet zal verzamelen, zelfs nadat u uw verwijzing naarAssemblyLoadContexthebt laten vallen. Maak deze velden leeg zodat het lossen kan worden voltooid.
Aanbeveling
Objectverwijzingen die zijn opgeslagen in stacksleuven of processorregisters en die het ontladen van een AssemblyLoadContext zouden kunnen verhinderen, kunnen zich in de volgende situaties voordoen:
- Wanneer resultaten van functieaanroep rechtstreeks worden doorgegeven aan een andere functie, ook al is er geen door de gebruiker gemaakte lokale variabele.
- Wanneer de JIT-compiler een verwijzing houdt naar een object dat op een bepaald moment in een methode beschikbaar was.
Losproblemen oplossen
Problemen met foutopsporing tijdens het ontladen kunnen vervelend zijn. U kunt in situaties terechtkomen waarin u niet weet wat een AssemblyLoadContext levend houdt, maar het ontladen mislukt. Het beste hulpprogramma om daarmee te helpen is WinDbg (of LLDB op Unix) met de SOS-invoegtoepassing. Je moet ontdekken wat een LoaderAllocator die tot de specifieke AssemblyLoadContext behoort, in leven houdt. Met de SOS-invoegtoepassing kunt u GC-heapobjecten, hun hiërarchieën en wortels bekijken.
Als u de SOS-invoegtoepassing wilt laden in het foutopsporingsprogramma, voert u een van de volgende opdrachten in de opdrachtregel voor foutopsporingsprogramma in.
In WinDbg (als het nog niet geladen is):
.loadby sos coreclr
In LLDB:
plugin load /path/to/libsosplugin.so
U gaat nu fouten opsporen in een voorbeeldprogramma dat problemen heeft met het lossen. De broncode is beschikbaar in de sectie Voorbeeldbroncode . Wanneer u het uitvoert onder WinDbg, stapt het programma in de debugger zodra geprobeerd wordt te controleren of het ontladen is geslaagd. Vervolgens kunt u op zoek gaan naar de schuldigen.
Aanbeveling
Als u debugt met behulp van LLDB op Unix, staan de SOS-opdrachten in de volgende voorbeelden niet ! ervoor.
!dumpheap -type LoaderAllocator
Met deze opdracht worden alle objecten gedumpt met een typenaam LoaderAllocator die zich in de GC-heap bevindt. Hier is een voorbeeld:
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
Controleer in het gedeelte Statistieken: de MT (MethodTable) die deel uitmaakt van het System.Reflection.LoaderAllocator, voor het object waar het om draait. Zoek vervolgens in de lijst aan het begin de vermelding met MT die daarmee overeenkomt en haal het adres van het object zelf op. In dit geval is het '000002b78000ce40'.
Nu u het adres van het LoaderAllocator object kent, kunt u een andere opdracht gebruiken om de GC-wortels te vinden:
!gcroot 0x000002b78000ce40
Met deze opdracht wordt de keten van objectverwijzingen gedumpt die naar het LoaderAllocator exemplaar leiden. De lijst begint met de root, de entiteit die LoaderAllocator in leven houdt en dus de kern van het probleem is. De hoofdmap kan een stackslot, een processorregister, een GC-handle of een statische variabele zijn.
Hier volgt een voorbeeld van de uitvoer van de gcroot opdracht:
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.
De volgende stap is om erachter te komen waar de root zich bevindt, zodat u het kunt oplossen. Het eenvoudigste geval is wanneer de wortel een stackslot of een processorregister is. In dat geval toont gcroot de naam van de functie waarvan het frame de basis en de thread die de functie uitvoert, bevat. Het moeilijke geval is wanneer de wortel een statische variabele of een GC-handle is.
In het vorige voorbeeld is de eerste root een lokaal van het type System.Reflection.RuntimeMethodInfo dat is opgeslagen in het frame van de functie example.Program.Main(System.String[]) op adres rbp-20 (rbp is het processorregister rbp en -20 is een hexadecimale offset ten opzichte van dat register).
De tweede wortel is een normale (sterke) GCHandle die een verwijzing naar een exemplaar van de test.Test-klasse bevat.
De derde root is een vastgepind GCHandle. Deze is eigenlijk een statische variabele, maar helaas is er geen manier om het te vertellen. Statische gegevens voor referentietypen worden opgeslagen in een beheerde objectmatrix in interne runtimestructuren.
Een ander geval dat het ontladen van een AssemblyLoadContext kan voorkomen, is wanneer een thread een frame van een methode van een assembly heeft die op zijn stack in de AssemblyLoadContext is geladen. U kunt dit controleren door beheerde aanroepstacks van alle threads te dumpen:
~*e !clrstack
De opdracht betekent "de !clrstack opdracht toepassen op alle threads". Hier volgt de uitvoer van die opdracht voor het voorbeeld. Helaas heeft LLDB op Unix geen manier om een opdracht toe te passen op alle threads, dus u moet threads handmatig wisselen en de clrstack opdracht herhalen. Negeer alle threads waarin het foutopsporingsprogramma zegt 'Kan de beheerde stack niet doorlopen'.
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]
Zoals u kunt zien, heeft de laatste thread test.Program.ThreadProc(). Dit is een functie van de assembly die in de AssemblyLoadContext is geladen, en dus blijft AssemblyLoadContext behouden.
Voorbeeld van broncode
De volgende code die problemen met de laadbaarheid bevat, wordt gebruikt in het vorige voorbeeld van foutopsporing.
Hoofdtestprogramma
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}");
}
}
}
Het programma is geladen in de TestAssemblyLoadContext
De volgende code vertegenwoordigt de test.dll doorgegeven aan de ExecuteAndUnload methode in het hoofdtestprogramma.
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;
}
}
}