Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Platforma .NET (Core) wprowadziła możliwość ładowania i późniejszego zwalniania 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 przez AssemblyLoadContext. Zbiór zestawów można załadować do obiektu zbieralnego AssemblyLoadContext, wykonać w nim metody lub po prostu sprawdzić go przy użyciu odbić, a na koniec zwolnić zbiór AssemblyLoadContext. To rozładowuje moduły załadowane do AssemblyLoadContext.
Istnieje jedna godna uwagi różnica między rozładowywaniem przy użyciu AssemblyLoadContext a używaniem obiektów AppDomains. W przypadku AppDomains odciążenie 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 "współpracujący". Wywołanie metody AssemblyLoadContext.Unload po prostu inicjuje usuwanie. Rozładunek kończy się po:
- Żadne wątki nie mają metod z zestawów załadowanych do
AssemblyLoadContextstosów wywołań. - Żaden z typów z zestawów załadowanych do
AssemblyLoadContext, ani wystąpienia tych typów, ani same zestawy nie są przywoływane przez:- Odwołania poza elementem
AssemblyLoadContext, z wyjątkiem słabych odwołań (WeakReference lub WeakReference<T>). - Silne uchwyty GC (GCHandleType.Normal lub GCHandleType.Pinned) zarówno z wnętrza, jak i spoza
AssemblyLoadContext.
- Odwołania poza elementem
Używanie funkcji Collectible AssemblyLoadContext
Ta sekcja zawiera szczegółowy samouczek krok po kroku, który przedstawia prosty sposób załadowania aplikacji .NET (Core) do kolekcjonowalnego AssemblyLoadContext, uruchomienia jej punktu wejścia, a następnie jej zwolnienia. 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 swoją klasę na podstawie AssemblyLoadContext i nadpisz jej metodę AssemblyLoadContext.Load. Ta metoda rozwiązuje odwołania do wszystkich zestawów, które są zależnościami zestawów załadowanych do tej AssemblyLoadContext.
Poniższy kod jest przykładem najprostszego niestandardowego AssemblyLoadContextkodu:
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(isCollectible: true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
Jak widać, metoda Load zwraca null. 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 elementu AssemblyLoadContext, możesz użyć AssemblyDependencyResolver w metodzie Load. Element AssemblyDependencyResolver przekształca nazwy zestawów na bezwzględne ścieżki plików zestawów. Program 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żdej z bibliotek, do których odwołuje się załadowana biblioteka, metoda TestAssemblyLoadContext.Load jest wywoływana tak, aby TestAssemblyLoadContext mogła zdecydować, skąd ma być pobierana biblioteka. W takim przypadku zwraca null, aby wskazać, że powinien zostać załadowany do domyślnego kontekstu z lokalizacji używanych przez czas wykonywania 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 zakończeniu działania metody Main, można zainicjować proces zwalniania zasobów, wywołując metodę Unload na niestandardowym AssemblyLoadContext lub usunięciem odwołania do AssemblyLoadContext.
alc.Unload();
Wystarczy odładować zestaw testowy. Następnie umieścisz wszystkie te elementy w oddzielnej metodzie nieinlineowalnej, aby upewnić się, że TestAssemblyLoadContext, Assembly i MethodInfo (czyli Assembly.EntryPoint) nie mogą być utrzymywane aktywne przez odwołania do przestrzeni stosu, które wprowadzają lokalne zmienne rzeczywiste lub JIT. To może utrzymać TestAssemblyLoadContext w stanie aktywnym oraz zapobiec wyłączeniu.
Ponadto zwróć słabe odwołanie do elementu AssemblyLoadContext aby można było go później użyć do wykrywania zakończenia odciążenia.
[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 rozładowanie nie zostanie ukończone natychmiast. Jak wspomniano wcześniej, opiera się na odśmiecaczu do zbierania wszystkich obiektów z zestawu testowego. W wielu przypadkach nie trzeba czekać na zakończenie rozładowania. Istnieją jednak przypadki, w których warto wiedzieć, że rozładowanie zostało zakończone. Możesz na przykład usunąć plik assembly, który został załadowany do niestandardowego AssemblyLoadContext z 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 iteracja przez pętlę. Jednak w przypadku bardziej złożonych przypadków, w których obiekty utworzone przez kod uruchomiony w AssemblyLoadContext mają finalizatory, może 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 rozładowania
W niektórych przypadkach może być konieczne, aby kod załadowany do niestandardowego AssemblyLoadContext przeprowadził czyszczenie, gdy inicjowane jest wyładowywanie. 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 współpracujący charakter procesu rozładunku, łatwo zapomnieć o odniesieniach, które mogą utrzymywać rzeczy w zbieralnych przedmiotach AssemblyLoadContext przy życiu, zapobiegając rozładowaniu. Oto podsumowanie jednostek (niektóre z nich nieoczywistych), które mogą zawierać odwołania:
- Regularne odwołania z zewnątrz obiektu kolekcjonowanego
AssemblyLoadContext, które są przechowywane w slocie stosu lub rejestrze procesora (lokalne zmienne metod, jawnie utworzone w kodzie użytkownika lub niejawnie przez kompilator JIT), a także zmienne statyczne lub silne uchwyty GC (przypinanie), które przechodnio wskazują na:- Zestaw załadowany do kolekcji
AssemblyLoadContext. - Typ z takiego zestawu.
- Wystąpienie typu z takiego zestawu.
- Zestaw załadowany do kolekcji
- Wątki uruchamiające kod z zestawu załadowanego do kolekcjonowanego
AssemblyLoadContext. - Wystąpienia niestandardowych, niekolektowalnych
AssemblyLoadContexttypów utworzonych wewnątrz kolekcji .AssemblyLoadContext - Oczekujące RegisteredWaitHandle wystąpienia z wywołaniami zwrotnymi ustawione jako metody w niestandardowym obiekcie
AssemblyLoadContext. - Pola w niestandardowej
AssemblyLoadContextpodklasie odwołujące się do zestawów, typów lub wystąpień typów załadowanych do kolekcjiAssemblyLoadContext. Podczas trwania procesu zwalniania, środowisko uruchomieniowe przechowuje silny uchwyt GC do modułuAssemblyLoadContextw celu koordynacji tego procesu. Oznacza to, że GC nie będzie zbierać tych odwołań do pól nawet gdy usuniesz własne odwołanie doAssemblyLoadContext. Wyczyść te pola, aby można było zakończyć rozładunek.
Wskazówka
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 zwalniania
Problemy z debugowaniem przy rozładowywaniu mogą być żmudne. Możesz znaleźć się w sytuacjach, w których nie wiesz, co może być utrzymywane AssemblyLoadContext w stanie aktywności, ale nie udało się go zwolnić. Najlepszym narzędziem, które pomaga w tym jest WinDbg (lub LLDB w systemie Unix) z wtyczką SOS. Musisz znaleźć, co utrzymuje przy życiu element LoaderAllocator, który należy do konkretnego AssemblyLoadContext. Wtyczka SOS pozwala na przeglądanie obiektów sterty GC, ich hierarchii i korzeni.
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 przeprowadzać debugowanie przykładowego programu, który ma problemy ze zwalnianiem. Kod źródłowy jest dostępny w sekcji Przykładowy kod źródłowy . Kiedy uruchamiasz go w WinDbg, program przechodzi do debugera bezpośrednio po próbie sprawdzenia pomyślnego zwolnienia zasobu. Następnie możesz zacząć szukać sprawców.
Wskazówka
W przypadku debugowania przy użyciu LLDB na systemie Unix, polecenia SOS w poniższych przykładach nie mają przed sobą !.
!dumpheap -type LoaderAllocator
To polecenie wyświetla wszystkie obiekty w stercie GC, których nazwa typu zawiera LoaderAllocator. 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 "Statystyki:" sprawdź MT (MethodTable) należący do System.Reflection.LoaderAllocator, czyli obiektu, którego dotyczy. Następnie na liście na początku znajdź wpis z MT pasujący do tego 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 zrzuca łańcuch odwołań do obiektów prowadzących do wystąpienia LoaderAllocator. Lista zaczyna się od korzenia, który jest podmiotem, utrzymującym LoaderAllocator przy życiu, a więc stanowi rdzeń problemu. Root może być slotem 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ę przyczyna problemu, aby można było go naprawić. Najprostszym przypadkiem jest, gdy katalog główny jest miejscem stosu lub rejestrem procesora. W takim przypadku gcroot pokazuje nazwę funkcji, której ramka zawiera główny węzeł i wątek wykonujący tę funkcję. Trudny przypadek to sytuacja, gdy korzeń jest zmienną statyczną lub uchwytem GC.
W poprzednim przykładzie pierwszy korzeń jest lokalnym typem System.Reflection.RuntimeMethodInfo przechowywanym w ramce funkcji example.Program.Main(System.String[]) pod adresem rbp-20 (rbp to rejestr rbp procesora, a -20 jest przesunięciem szesnastkowym z tego rejestru).
Drugi korzeń to normalny (silny) GCHandle, który przechowuje odwołanie do wystąpienia klasy test.Test.
Trzeci rdzeń jest przypięty GCHandle. Jest to rzeczywiście zmienna statyczna, ale niestety, nie ma sposobu, aby powiedzieć. Statyczne typy referencyjne są przechowywane w zarządzanej tablicy obiektów w wewnętrznych strukturach środowiska uruchomieniowego.
Innym przypadkiem, który może uniemożliwić rozładowanie AssemblyLoadContext, jest sytuacja, gdy wątek ma ramkę metody z zestawu załadowanego do AssemblyLoadContext. Możesz to sprawdzić, zrzucając zarządzane stosy wywołań wszystkich wątków:
~*e !clrstack
Polecenie oznacza "zastosuj polecenie !clrstack do wszystkich wątków". 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, a więc podtrzymuje 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 przekazany do metody ExecuteAndUnload 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;
}
}
}