Jak używać i debugować możliwość zwolnienia zestawu na platformie .NET
Platforma .NET (Core) wprowadziła możliwość ładowania i późniejszego zwalniania zestawu zestawów. W programie .NET Framework niestandardowe domeny aplikacji były używane w tym celu, ale platforma .NET (Core) obsługuje tylko jedną domyślną domenę aplikacji.
Możliwość zwolnienia jest obsługiwana za pośrednictwem programu AssemblyLoadContext. Zestaw zestawów można załadować do obiektu zbieralnego AssemblyLoadContext
, wykonać w nich metody lub po prostu sprawdzić je przy użyciu odbicia, a na koniec zwolnić element AssemblyLoadContext
. Zwalnia to zestawy załadowane do pliku AssemblyLoadContext
.
Istnieje jedna godna uwagi różnica między zwalnianiem przy użyciu i AssemblyLoadContext
używaniem obiektów AppDomains. W przypadku parametrów AppDomains zwalnianie jest wymuszane. W czasie zwolnienia wszystkie wątki uruchomione w docelowej domenie aplikacji są przerywane, zarządzane obiekty COM utworzone w docelowej domenie aplikacji są niszczone itd. W przypadku AssemblyLoadContext
, rozładunek jest "spółdzielnia". AssemblyLoadContext.Unload Wywołanie metody po prostu inicjuje zwalnianie. Zwalnianie kończy się po:
- Żadne wątki nie mają metod z zestawów załadowanych do
AssemblyLoadContext
stosów wywołań. - Żadne z typów z zestawów załadowanych do
AssemblyLoadContext
wystąpienia tych typów i same zestawy nie są przywoływane przez:- Odwołania poza
AssemblyLoadContext
elementem , z wyjątkiem słabych odwołań (WeakReference lub WeakReference<T>). - Silne uchwyty modułu odśmiecania pamięci (GCHandleType.Normal lub GCHandleType.Pinned) zarówno wewnątrz, jak i na zewnątrz
AssemblyLoadContext
.
- Odwołania poza
Używanie funkcji Collectible AssemblyLoadContext
Ta sekcja zawiera szczegółowy samouczek krok po kroku, który przedstawia prosty sposób ładowania aplikacji platformy .NET (Core) do obiektu zbieralnego AssemblyLoadContext
, wykonywania punktu wejścia, a następnie zwalniania aplikacji. Pełny przykład można znaleźć na stronie https://github.com/dotnet/samples/tree/main/core/tutorials/Unloading.
Tworzenie zbieralnego elementu AssemblyLoadContext
Utwórz klasę na podstawie AssemblyLoadContext metody i zastąpij jej AssemblyLoadContext.Load metodę. Ta metoda rozpoznaje odwołania do wszystkich zestawów, które są zależnościami zestawów załadowanych do tej AssemblyLoadContext
metody .
Poniższy kod jest przykładem najprostszego niestandardowego AssemblyLoadContext
kodu:
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(isCollectible: true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
Jak widać, Load
metoda zwraca null
wartość . Oznacza to, że wszystkie zestawy zależności są ładowane do kontekstu domyślnego, a nowy kontekst zawiera tylko zestawy jawnie załadowane do niego.
Jeśli chcesz załadować niektóre lub wszystkie zależności do AssemblyLoadContext
elementu , możesz użyć AssemblyDependencyResolver
metody w metodzie Load
. Element AssemblyDependencyResolver
rozpoznaje nazwy zestawów na bezwzględne ścieżki plików zestawu. Narzędzie rozpoznawania używa plików .deps.json i plików zestawów w katalogu głównego zestawu załadowanego do kontekstu.
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;
}
}
}
Używanie niestandardowego zbieralnego elementu AssemblyLoadContext
W tej sekcji założono, że używana jest prostsza wersja elementu TestAssemblyLoadContext
.
Możesz utworzyć wystąpienie niestandardowe AssemblyLoadContext
i załadować do niego zestaw w następujący sposób:
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
Dla każdego z zestawów, do których odwołuje się załadowany zestaw, metoda jest wywoływana tak, TestAssemblyLoadContext.Load
aby TestAssemblyLoadContext
można było zdecydować, skąd ma być pobierany zestaw. W takim przypadku zwraca null
wartość , aby wskazać, że powinien zostać załadowany do domyślnego kontekstu z lokalizacji używanych przez środowisko uruchomieniowe do ładowania zestawów domyślnie.
Teraz, gdy zestaw został załadowany, możesz wykonać z niego metodę. Uruchom metodę Main
:
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
Po powrocie Main
metody można zainicjować zwalnianie, wywołując metodę Unload
w niestandardowym AssemblyLoadContext
lub usuwając odwołanie do elementu AssemblyLoadContext
:
alc.Unload();
Wystarczy zwolnić zestaw testowy. Następnie umieścisz wszystkie te elementy w oddzielnej metodzie nieinlineable, aby upewnić się, że TestAssemblyLoadContext
nie można zachować aktywności elementów , Assembly
i MethodInfo
() Assembly.EntryPoint
przez odwołania do miejsca stosu (lokalne wprowadzone w rzeczywistości lub JIT). To może utrzymać TestAssemblyLoadContext
życie i zapobiec rozładowaniu.
Ponadto zwróć słabe odwołanie do elementu AssemblyLoadContext
, aby można było go później użyć do wykrywania zakończenia zwolnienia.
[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();
}
Teraz możesz uruchomić tę funkcję, aby załadować, wykonać i zwolnić zestaw.
WeakReference testAlcWeakRef;
ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);
Jednak zwolnienie nie zostanie ukończone natychmiast. Jak wspomniano wcześniej, opiera się na modułze odśmiecającym elementy do zbierania wszystkich obiektów z zestawu testowego. W wielu przypadkach nie trzeba czekać na zakończenie zwolnienia. Istnieją jednak przypadki, w których warto wiedzieć, że zwolnienie zostało zakończone. Możesz na przykład usunąć plik zestawu, który został załadowany do niestandardowego AssemblyLoadContext
dysku. W takim przypadku można użyć następującego fragmentu kodu. Wyzwala odzyskiwanie pamięci i czeka na oczekujące finalizatory w pętli, dopóki słabe odwołanie do niestandardowego AssemblyLoadContext
nie zostanie ustawione na null
, wskazując, że obiekt docelowy został zebrany. W większości przypadków wymagana jest tylko jedna pętla przekazywania. Jednak w przypadku bardziej złożonych przypadków, w których obiekty utworzone przez kod uruchomiony w AssemblyLoadContext
finalizatorach mogą być potrzebne więcej przebiegów.
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Ograniczenia
Zestawy ładowane do zestawu zbieralnego AssemblyLoadContext
muszą przestrzegać ogólnych ograniczeń dotyczących zestawów zbieralnych. Ponadto obowiązują następujące ograniczenia:
- Zestawy napisane w języku C++/CLI nie są obsługiwane.
- Wygenerowany kod ReadyToRun zostanie zignorowany.
Zdarzenie zwalniania
W niektórych przypadkach może być konieczne załadowanie kodu do niestandardowego AssemblyLoadContext
w celu przeprowadzenia czyszczenia podczas inicjowania zwolnienia. Na przykład może być konieczne zatrzymanie wątków lub wyczyszczenie silnych uchwytów GC. Zdarzenie Unloading
może być używane w takich przypadkach. Można podłączyć procedurę obsługi, która wykonuje niezbędne czyszczenie tego zdarzenia.
Rozwiązywanie problemów z rozładowywaniem
Ze względu na spółdzielczy charakter rozładowy, łatwo zapomnieć o odniesieniach, które mogą być utrzymanie rzeczy w przedmiotach zbieralnych AssemblyLoadContext
przy życiu i zapobieganie rozładowaniu. Oto podsumowanie jednostek (niektóre z nich nieobjętne), które mogą zawierać odwołania:
- Regularne odwołania przechowywane spoza kolekcji
AssemblyLoadContext
, które są przechowywane w miejscu stosu lub rejestrze procesora (metody lokalne, jawnie utworzone przez kod użytkownika lub niejawnie przez kompilator just in time (JIT), zmienną statyczną lub silne (przypinanie) uchwyt GC i przechodnio wskazujące:- Zestaw załadowany do kolekcji
AssemblyLoadContext
. - Typ z takiego zestawu.
- Wystąpienie typu z takiego zestawu.
- Zestaw załadowany do kolekcji
- Wątki uruchamiane z zestawu załadowane do kolekcji
AssemblyLoadContext
. - Wystąpienia niestandardowych, niekolektowalnych
AssemblyLoadContext
typów utworzonych wewnątrz kolekcji .AssemblyLoadContext
- Oczekujące RegisteredWaitHandle wystąpienia z wywołaniami zwrotnymi ustawione na metody w niestandardowym
AssemblyLoadContext
obiekcie .
Napiwek
Odwołania do obiektów, które są przechowywane w miejscach stosu lub rejestrach procesora i które mogą zapobiec rozładowaniu obiektu AssemblyLoadContext
, mogą wystąpić w następujących sytuacjach:
- Gdy wyniki wywołania funkcji są przekazywane bezpośrednio do innej funkcji, mimo że nie ma zmiennej lokalnej utworzonej przez użytkownika.
- Gdy kompilator JIT przechowuje odwołanie do obiektu, który był dostępny w pewnym momencie w metodzie.
Debugowanie problemów z zwalnianiem
Problemy z debugowaniem podczas zwalniania mogą być żmudne. Możesz dostać się do sytuacji, w których nie wiesz, co może być trzymane AssemblyLoadContext
przy życiu, ale zwalnianie kończy się niepowodzeniem. Najlepszym narzędziem, które pomaga w tym jest WinDbg (lub LLDB w systemie Unix) z wtyczką SOS. Musisz znaleźć, co utrzymuje LoaderAllocator
element, który należy do konkretnego AssemblyLoadContext
żywego. Wtyczka SOS umożliwia przyjrzenie się obiektom sterty GC, ich hierarchiom i katalogom korzeniowym.
Aby załadować wtyczkę SOS do debugera, wprowadź jedno z następujących poleceń w wierszu polecenia debugera.
W systemie WinDbg (jeśli jeszcze nie został załadowany):
.loadby sos coreclr
W usłudze LLDB:
plugin load /path/to/libsosplugin.so
Teraz będziesz debugować przykładowy program, który ma problemy z zwalnianiem. Kod źródłowy jest dostępny w sekcji Przykładowy kod źródłowy. Po uruchomieniu go w systemie WinDbg program dzieli się na debuger bezpośrednio po próbie sprawdzenia powodzenia zwolnienia. Następnie możesz zacząć szukać sprawców.
Napiwek
W przypadku debugowania przy użyciu usługi LLDB w systemie Unix polecenia SOS w poniższych przykładach nie są !
przed nimi dostępne.
!dumpheap -type LoaderAllocator
To polecenie zrzutuje wszystkie obiekty o nazwie typu zawierającej LoaderAllocator
sterty GC. Oto przykład:
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
W części "Statistics:" (Statystyka:) sprawdź MT
element (MethodTable
), który należy do System.Reflection.LoaderAllocator
obiektu , którego dotyczy. Następnie na liście na początku znajdź wpis zgodny z MT
tym elementem i pobierz adres samego obiektu. W tym przypadku jest to "000002b78000ce40".
Teraz, gdy znasz adres LoaderAllocator
obiektu, możesz użyć innego polecenia, aby znaleźć jego korzenie GC:
!gcroot 0x000002b78000ce40
To polecenie zrzutuje łańcuch odwołań do obiektów, które prowadzą do LoaderAllocator
wystąpienia. Lista zaczyna się od katalogu głównego, który jest jednostką, która utrzymuje LoaderAllocator
przy życiu, a tym samym jest rdzeniem problemu. Katalog główny może być miejscem stosu, rejestrem procesora, uchwytem GC lub zmienną statyczną.
Oto przykład danych wyjściowych gcroot
polecenia:
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.
Następnym krokiem jest ustalenie, gdzie znajduje się katalog główny, aby można było go naprawić. Najprostszym przypadkiem jest, gdy katalog główny jest miejscem stosu lub rejestrem procesora. W takim przypadku parametr pokazuje nazwę funkcji, gcroot
której ramka zawiera katalog główny i wątek wykonujący tę funkcję. Trudny przypadek polega na tym, że katalog główny jest zmienną statyczną lub uchwytem GC.
W poprzednim przykładzie pierwszy katalog główny jest lokalnym typem System.Reflection.RuntimeMethodInfo
przechowywanym w ramce funkcji example.Program.Main(System.String[])
pod adresem rbp-20
(rbp
czy rejestr rbp
procesora i -20 jest przesunięciem szesnastkowym z tego rejestru).
Drugi element główny to normalny (silny), GCHandle
który przechowuje odwołanie do wystąpienia test.Test
klasy.
Trzeci katalog główny jest przypięty GCHandle
. Jest to rzeczywiście zmienna statyczna, ale niestety, nie ma sposobu, aby powiedzieć. Statyczne typy odwołań są przechowywane w tablicy obiektów zarządzanych w wewnętrznych strukturach środowiska uruchomieniowego.
Innym przypadkiem, który może zapobiec rozładowaniu elementu AssemblyLoadContext
, jest sytuacja, gdy wątek ma ramkę metody z zestawu załadowanego do stosu AssemblyLoadContext
. Możesz to sprawdzić, dumpingując zarządzane stosy wywołań wszystkich wątków:
~*e !clrstack
Polecenie oznacza "zastosuj do wszystkich wątków !clrstack
polecenia". Poniżej przedstawiono dane wyjściowe tego polecenia dla przykładu. Niestety, usługa LLDB w systemie Unix nie ma możliwości zastosowania polecenia do wszystkich wątków, więc należy ręcznie przełączać wątki i powtarzać clrstack
polecenie. Ignoruj wszystkie wątki, w których debuger mówi "Nie można przejść przez zarządzany stos".
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 widać, ostatni wątek ma wartość test.Program.ThreadProc()
. Jest to funkcja z zestawu załadowanego do AssemblyLoadContext
elementu , a więc utrzymuje żywcem AssemblyLoadContext
.
Przykładowy kod źródłowy
Poniższy kod, który zawiera problemy z rozładowaniem, jest używany w poprzednim przykładzie debugowania.
Główny program testowania
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 załadowany do elementu TestAssemblyLoadContext
Poniższy kod reprezentuje test.dll przekazaną ExecuteAndUnload
do metody w głównym programie testowym.
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;
}
}
}