Jak používat a ladit možnost uvolnění sestavení v .NET
.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.
Unloadability is supported through 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
.
Mezi uvolňováním a AssemblyLoadContext
používáním appDomains je jeden pozoruhodný rozdíl. U appDomains je uvolnění vynucené. V době uvolnění jsou všechna vlákna spuštěná v cílové doméně AppDomain přerušena, spravované objekty MODELU COM vytvořené v cílové doméně AppDomain jsou zničeny atd. S AssemblyLoadContext
, unload je "družstevní". Volání metody AssemblyLoadContext.Unload právě zahájí uvolňování. Uvolnění skončí po:
- Žádná vlákna nemají metody ze sestavení načtených
AssemblyLoadContext
do jejich zásobníků volání. - Na žádné typy ze sestavení načtených do
AssemblyLoadContext
, instance těchto typů a samotné sestavení se odkazují:- Odkazy mimo
AssemblyLoadContext
, s výjimkou slabých odkazů (WeakReference nebo WeakReference<T>). - Silné úchyty uvolňování paměti (GCHandleType.Normal nebo GCHandleType.Pinned) z vnitřní i vnější strany
AssemblyLoadContext
.
- Odkazy mimo
Použití collectible AssemblyLoadContext
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ření collectible AssemblyLoadContext
Odvozujte třídu z AssemblyLoadContext metody a přepište její AssemblyLoadContext.Load metodu. Tato metoda vyř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 AssemblyLoadContext
kó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
této metody, můžete použít metodu AssemblyDependencyResolver
Load
. Přeloží AssemblyDependencyResolver
názvy sestavení na absolutní cesty k souborům sestavení. Překladač používá soubor .deps.json 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žití vlastního collectible AssemblyLoadContext
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 TestAssemblyLoadContext.Load
je volána metoda, aby TestAssemblyLoadContext
se mohl rozhodnout, odkud získat sestavení. V tomto případě se vrátí null
k označení, že by se měl načíst do výchozího kontextu z umístění, která modul runtime používá k načtení sestavení ve výchozím nastavení.
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 vrácení metody můžete zahájit uvolňování voláním Unload
metody na vlastní AssemblyLoadContext
nebo odebráním odkazu, který máte na AssemblyLoadContext
:
alc.Unload();
To stačí k uvolnění testovacího sestavení. Dále ho vložíte do samostatné nelineable metody, abyste zajistili, že TestAssemblyLoadContext
odkazy na sloty zásobníku ( Assembly
real- nebo JIT-zavedené místní prostředí) a MethodInfo
(the Assembly.EntryPoint
) nemohou být udržovány naživu. To by mohlo udržet TestAssemblyLoadContext
život a zabránit uvolnění.
Také vraťte slabý odkaz, 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();
}
Teď můžete tuto funkci spustit a načíst, spustit a uvolnit 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 systém uvolňování paměti ke shromáždění všech objektů z testovacího sestavení. V mnoha případech není nutné čekat na dokončení uvolnění. 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 null
hodnotu , což znamená, že cílový objekt byl shromážděn. Ve většině případů se vyžaduje jenom jeden průchod smyčkou. V případě složitějších případů, kdy objekty vytvořené kódem spuštěným v AssemblyLoadContext
finalizačních metodách mohou 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 shromažďování 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 zavěsit obslužnou rutinu, která provede potřebné vyčištění této události.
Řešení potíží s uvolněním zatížení
Vzhledem k družstevní povaze uvolňování je snadné zapomenout na odkazy, které by mohly uchovávat věci v sběritelné AssemblyLoadContext
naživu a zabránit uvolnění. Tady je souhrn entit (některé z nich neposlušné), 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
AssemblyLoadContext
objektu . - Typ z takového sestavení.
- Instance typu z takového sestavení.
- Sestavení načtené do sběrného
- Vlákna se spuštěným kódem ze sestavení načteného do collectible
AssemblyLoadContext
. - Instance vlastních neshromažďovatelných
AssemblyLoadContext
typů vytvořených uvnitř collectibleAssemblyLoadContext
. - Čekající RegisteredWaitHandle instance se zpětnými voláními nastavenými na metody ve vlastním
AssemblyLoadContext
objektu .
Tip
Odkazy na objekty uložené v slotech zásobníku nebo registrech procesorů a které by mohly zabránit uvolnění objektu AssemblyLoadContext
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 uvolňováním může být zdlouhavé. Můžete se dostat do situací, kdy nevíte, co může držet naživu AssemblyLoadContext
, 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. Potřebujete zjistit, co je udržování LoaderAllocator
, které patří konkrétnímu AssemblyLoadContext
živému. Modul plug-in 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 uvolněním. Zdrojový kód je k dispozici v části Příklad zdrojového kódu . Když ji spustíte v systému WinDbg, program se hned po pokusu o kontrolu úspěšného uvolnění rozdělí do ladicího programu. Pak můžete začít hledat viníky.
Tip
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 vysadí všechny objekty s názvem typu, LoaderAllocator
který je 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, úchyt GC 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, že kořen je statická proměnná nebo popisovač GC.
V předchozím příkladu je první kořen místním typem System.Reflection.RuntimeMethodInfo
uloženým 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 test.Test
třídy.
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í je AssemblyLoadContext
, když vlákno má rámec metody ze sestavení načteno do jeho zásobníku AssemblyLoadContext
. Můžete to zkontrolovat dumpingem spravovaných zásobníků volání všech vláken:
~*e !clrstack
Příkaz znamená "použít na všechna vlákna, která !clrstack
příkaz". 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í načteného AssemblyLoadContext
do a tak udržuje naživu AssemblyLoadContext
.
Příklad zdrojového kódu
Následující kód, který obsahuje problémy s uvolněním, 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;
}
}
}