Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
.NET (Core) zavedl možnost načtení a pozdějšího uvolnění sady sestavení. V rozhraní .NET Framework se pro tento účel používaly vlastní domény aplikací, ale .NET (Core) podporuje pouze jednu výchozí doménu aplikace.
Podpora vykládání je zajištěna prostřednictvím AssemblyLoadContext. Sadu sestavení můžete načíst do collectible AssemblyLoadContext, spustit metody v nich nebo jen zkontrolovat pomocí reflexe a nakonec uvolnit AssemblyLoadContext. Tím se uvolní sestavení načtená do souboru AssemblyLoadContext.
Existuje jeden pozoruhodný rozdíl mezi uvolňováním s použitím AssemblyLoadContext a používáním AppDomains. U appDomains je uvolnění vynucené. V době uvolnění jsou všechna vlákna spuštěná v cílové AppDomain přerušena, spravované COM objekty vytvořené v cílové AppDomain jsou zničeny a tak dále. S AssemblyLoadContext, unload je "kooperativní". Volání metody AssemblyLoadContext.Unload pouze zahájí uvolňování. Vyložení skončí po:
- Žádná vlákna nemají metody ze sestavení načtených do
AssemblyLoadContextna jejich zásobnících volání. - Na žádné typy ze sestavení načtených do
AssemblyLoadContext, ani na instance těchto typů či na samotná sestavení není odkazováno:- Odkazy mimo
AssemblyLoadContext, s výjimkou slabých odkazů (WeakReference nebo WeakReference<T>). - Silný správce paměti (GC) obsluhuje (GCHandleType.Normal nebo GCHandleType.Pinned) z prostředí i mimo prostředí
AssemblyLoadContext.
- Odkazy mimo
Použití modulárního kontextu načítání sestav
Tato část obsahuje podrobný kurz, který ukazuje jednoduchý způsob načtení aplikace .NET (Core) do collectible AssemblyLoadContext, spuštění jeho vstupního bodu a jeho následné uvolnění. Kompletní ukázku najdete na adrese https://github.com/dotnet/samples/tree/main/core/tutorials/Unloading.
Vytvořte collectible AssemblyLoadContext
Odvoďte svou třídu od AssemblyLoadContext a přepište její metodu AssemblyLoadContext.Load. Tato metoda řeší odkazy na všechna sestavení, která jsou závislostmi sestavení načtených do tohoto AssemblyLoadContext.
Následující kód je příkladem nejjednoduššího vlastního AssemblyLoadContextkódu:
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(isCollectible: true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
Jak vidíte, Load metoda vrátí null. To znamená, že všechna sestavení závislostí jsou načtena do výchozího kontextu a nový kontext obsahuje pouze sestavení explicitně načtená do něj.
Pokud chcete také načíst některé nebo všechny závislosti do AssemblyLoadContext, můžete použít AssemblyDependencyResolver v metodě Load. Přeloží AssemblyDependencyResolver názvy sestavení na absolutní cesty k souborům sestavení. Rešitel používá .deps.json soubor a soubory sestavení v adresáři hlavního sestavení načteného do kontextu.
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;
}
}
}
Použijte vlastní kontext AssemblyLoadContext pro sběr dat
V této části se předpokládá, že se používá jednodušší verze TestAssemblyLoadContext .
Instanci vlastního AssemblyLoadContext objektu můžete vytvořit a načíst do ní sestavení následujícím způsobem:
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
Pro každé sestavení odkazované načteným sestavením je volána metoda TestAssemblyLoadContext.Load, aby se TestAssemblyLoadContext mohl rozhodnout, odkud získat sestavení. V tomto případě se vrátí null pro označení, že se má načíst do výchozího kontextu z míst, která runtime standardně používá k načítání sestavení.
Teď, když bylo načteno sestavení, můžete z něj spustit metodu. Spusťte metodu Main :
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
Main Po dokončení metody můžete zahájit uvolňování buď voláním metody Unload na vlastní AssemblyLoadContext, nebo odstraněním vašeho odkazu na AssemblyLoadContext:
alc.Unload();
To stačí k uvolnění testovacího sestavení. Dále vložíte všechny tyto prvky do samostatné nelinkovatelná metody, abyste zajistili, že TestAssemblyLoadContext, Assembly, a MethodInfo (tedy Assembly.EntryPoint) nemohou být udržovány naživu pomocí odkazů na sloty zásobníku (místní proměnné zavedené skutečně nebo JIT). To by mohlo udržet TestAssemblyLoadContext v provozu a zabránit vyložení.
Také vraťte slabý odkaz na AssemblyLoadContext, abyste ho mohli později použít ke zjištění dokončení uvolnění.
[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();
}
Nyní můžete tuto funkci spustit k načtení, provedení a uvolnění sestavení.
WeakReference testAlcWeakRef;
ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);
Uvolnění se ale nedokončí okamžitě. Jak už bylo zmíněno dříve, spoléhá na garbage collector, aby shromáždil všechny objekty z testovacího sestavení. V mnoha případech není nutné čekat na dokončení vyložení. Existují však případy, kdy je užitečné vědět, že se uvolnění dokončilo. Můžete například chtít odstranit soubor sestavení, který byl načten do vlastního AssemblyLoadContext disku. V takovém případě je možné použít následující fragment kódu. Aktivuje uvolňování paměti a čeká na čekající finalizátory ve smyčce, dokud slabý odkaz na vlastní AssemblyLoadContext není nastaven na hodnotu null, což znamená, že cílový objekt byl sesbíraný. Ve většině případů se vyžaduje jenom jeden průchod smyčkou. V případě složitějších situací, kdy objekty vytvořené kódem spuštěným v AssemblyLoadContext mají finalizátory, může být potřeba více průchodů.
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Omezení
Sestavení načtená do collectible AssemblyLoadContext se musí řídit obecnými omezeními na kolektivní sestavení. Platí také následující omezení:
- Sestavení napsaná v C++/CLI nejsou podporována.
- Vygenerovaný kód ReadyToRun bude ignorován.
Událost uvolňování
V některých případech může být nutné, aby kód načtený do vlastního AssemblyLoadContext souboru provedl vyčištění při zahájení uvolňování. Může například potřebovat zastavit vlákna nebo vyčistit silné úchyty GC. Událost Unloading lze v takových případech použít. Můžete připojit obslužnou rutinu k této události, která provede potřebné vyčištění.
Řešení potíží s uvolněním zatížení
Vzhledem ke spolupracující povaze vykládání je snadné zapomenout na odkazy, které by mohly ponechávat věci ve sbírce AssemblyLoadContext naživu a zabránit jeho uvolnění. Tady je souhrn entit (některé z nich ne zcela zřejmé), které můžou obsahovat odkazy:
- Pravidelné odkazy uchovávané mimo kolekci
AssemblyLoadContext, které jsou uložené v slotu zásobníku nebo v registru procesoru (místní metody, buď explicitně vytvořené uživatelským kódem, nebo implicitně kompilátorem JIT), statickou proměnnou nebo silnou (připnutou) rukojeť GC a přechodně odkazující na:- Sestavení načtené do sběrného
AssemblyLoadContextobjektu. - Typ z takového sestavení.
- Instance typu z takového sestavení.
- Sestavení načtené do sběrného
- Vlákna, která spouštějí kód z načteného sestavení do kontejneru
AssemblyLoadContext. - Instance vlastních neshromažďovatelných
AssemblyLoadContexttypů vytvořených uvnitř shromážditelnéhoAssemblyLoadContext. - Čekající RegisteredWaitHandle instance se zpětnými voláními nastavenými na metody ve vlastním
AssemblyLoadContextobjektu. - Pole ve vaší vlastní
AssemblyLoadContextpodtřídě, která odkazují na sestavení, typy nebo instance typů načtených do collectibleAssemblyLoadContext. V průběhu uvolňování drží modul runtime silný popisovač GC keAssemblyLoadContextkoordinaci procesu uvolnění. To znamená, že GC neshromáždí tyto odkazy na pole, i když odstraníte svůj vlastní odkaz naAssemblyLoadContext. Vymažte tato pole, aby bylo možné dokončit vyskladnění.
Návod
Odkazy na objekty uložené ve slotech zásobníku nebo v registrech procesoru, které by mohly zabránit uvolnění AssemblyLoadContext, mohou nastat v následujících situacích:
- Když se výsledky volání funkce předají přímo jiné funkci, i když neexistuje žádná místní proměnná vytvořená uživatelem.
- Když kompilátor JIT uchovává odkaz na objekt, který byl k dispozici v určitém okamžiku v metodě.
Ladění problémů s uvolňováním
Ladění problémů s vykládáním může být zdlouhavé. Můžete se dostat do situací, kdy nevíte, co může udržovat AssemblyLoadContext při životě, ale uvolnění selže. Nejlepší nástroj, který vám pomůže s tím, je WinDbg (nebo LLDB v Unixu) s modulem plug-in SOS. Musíte zjistit, co drží konkrétní LoaderAllocator, který patří ke konkrétnímu AssemblyLoadContext, v chodu. Plugin SOS umožňuje podívat se na objekty haldy GC, jejich hierarchie a kořeny.
Pokud chcete do ladicího programu načíst modul plug-in SOS, zadejte do příkazového řádku ladicího programu jeden z následujících příkazů.
V WinDbg (pokud ještě není načten):
.loadby sos coreclr
V LLDB:
plugin load /path/to/libsosplugin.so
Teď budete ladit ukázkový program, který má problémy s ukončením. Zdrojový kód je k dispozici v části Příklad zdrojového kódu . Když ji spustíte pod WinDbg, program se hned po pokusu o kontrolu úspěšného uvolnění přeruší a vstoupí do ladicího programu. Pak můžete začít hledat viníky.
Návod
Pokud ladíte pomocí LLDB v unixu, příkazy SOS v následujících příkladech nemají ! před sebou.
!dumpheap -type LoaderAllocator
Tento příkaz vypíše všechny objekty s názvem typu obsahujícím LoaderAllocator, které jsou v haldě GC. Tady je příklad:
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
V části Statistika:, zkontrolujte MT (MethodTable), která patří do objektu System.Reflection.LoaderAllocator, který vás zajímá. Pak v seznamu na začátku vyhledejte položku, MT která odpovídá této položce, a získejte adresu samotného objektu. V tomto případě je to "000002b78000ce40".
Teď, když znáte adresu objektu LoaderAllocator , můžete pomocí jiného příkazu najít jeho kořeny GC:
!gcroot 0x000002b78000ce40
Tento příkaz vysadí řetěz odkazů na objekty, které vedou k LoaderAllocator instanci. Seznam začíná kořenem, což je entita, která udržuje LoaderAllocator aktivní, a proto je jádrem problému. Kořenem může být slot zásobníku, registr procesoru, GC ukazatel nebo statická proměnná.
Tady je příklad výstupu gcroot příkazu:
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.
Dalším krokem je zjistit, kde se nachází kořen, abyste ho mohli opravit. Nejjednodušším případem je, když je kořen slotem zásobníku nebo registrem procesoru. V takovém případě se zobrazí název funkce, gcroot jejíž rámec obsahuje kořen a vlákno, které danou funkci spouští. Složitým případem je, když je kořen statická proměnná nebo popisovač GC.
V předchozím příkladu je prvním kořenem místní typ System.Reflection.RuntimeMethodInfo uložený v rámci funkce example.Program.Main(System.String[]) na adrese rbp-20 (rbp je registr rbp procesoru a -20 je šestnáctkový posun od tohoto registru).
Druhý kořen je normální (silný) GCHandle, který obsahuje odkaz na instanci třídy test.Test.
Třetí kořen je připnutý GCHandle. Tato proměnná je ve skutečnosti statická, ale bohužel neexistuje způsob, jak to říct. Statické objekty pro odkazové typy jsou uloženy ve spravovaném poli objektů v interních strukturách modulu runtime.
Další případ, který může zabránit uvolnění AssemblyLoadContext, nastane, když má vlákno ve svém zásobníku rámec metody ze sestavení načteného do AssemblyLoadContext. Můžete to zkontrolovat dumpingem spravovaných zásobníků volání všech vláken:
~*e !clrstack
Příkaz znamená "použít !clrstack příkaz na všechna vlákna". Následuje výstup tohoto příkazu pro příklad. LLDB v Unixu bohužel nemá žádný způsob, jak použít příkaz pro všechna vlákna, takže musíte ručně přepnout vlákna a opakovat clrstack příkaz. Ignorujte všechna vlákna, ve kterých ladicí program říká "Nejde procházet spravovaný zásobník".
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]
Jak vidíte, poslední vlákno má test.Program.ThreadProc(). Jedná se o funkci ze sestavení, která byla načtena do AssemblyLoadContext, a tak udržuje AssemblyLoadContext naživu.
Příklad zdrojového kódu
Níže uvedený kód, který obsahuje problémy s neuložitelností, se používá v předchozím příkladu ladění.
Hlavní testovací 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;
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}");
}
}
}
Program načtený do TestAssemblyLoadContext
Následující kód představuje test.dll předán metodě ExecuteAndUnload v hlavním testovacím programu.
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;
}
}
}