Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
.NET (Core) memperkenalkan kemampuan untuk memuat dan kemudian membongkar satu set rakitan. Di .NET Framework, domain aplikasi kustom digunakan untuk tujuan ini, tetapi .NET (Core) hanya mendukung satu domain aplikasi default.
Unloadability didukung melalui AssemblyLoadContext. Anda dapat memuat satu set assembly ke dalam AssemblyLoadContextyang dapat dikoleksi, mengeksekusi metode di dalamnya atau hanya memeriksanya menggunakan refleksi, dan akhirnya membongkar AssemblyLoadContext. Melepaskan rakitan yang dimuat ke dalam AssemblyLoadContext.
Ada satu perbedaan penting antara proses pembongkaran menggunakan AssemblyLoadContext dan menggunakan AppDomains. Dengan AppDomains, pelepasan dipaksa. Pada waktu pembongkaran, semua utas yang berjalan di AppDomain target dibatalkan, objek COM terkelola yang dibuat di AppDomain target dihapus, dan sebagainya. Dengan AssemblyLoadContext, pembongkaran adalah "kooperatif". Memanggil metode AssemblyLoadContext.Unload hanya memulai pembongkaran. Pembongkaran selesai setelah:
- Tidak ada utas yang memiliki metode dari rakitan yang dimuat ke
AssemblyLoadContextdalam tumpukan panggilan mereka. - Tidak ada jenis dari rakitan yang dimuat ke dalam
AssemblyLoadContext, instans dari jenis tersebut, dan rakitan itu sendiri direferensikan oleh:- Referensi di luar
AssemblyLoadContext, kecuali untuk referensi yang lemah (WeakReference atau WeakReference<T>). - Handles pengumpul sampah (GC) tingkat tinggi (GCHandleType.Normal atau GCHandleType.Pinned) baik dari dalam maupun luar
AssemblyLoadContext.
- Referensi di luar
Menggunakan AssemblyLoadContext yang dapat dikumpulkan
Bagian ini berisi tutorial langkah demi langkah terperinci yang menunjukkan cara sederhana untuk memuat aplikasi .NET (Core) ke dalam collectible AssemblyLoadContext, jalankan titik masuknya, lalu bongkar. Anda dapat menemukan sampel lengkap di https://github.com/dotnet/samples/tree/main/core/tutorials/Unloading.
Membuat AssemblyLoadContext yang dapat dikumpulkan
Dapatkan kelas Anda dari AssemblyLoadContext dan ambil alih metodenya AssemblyLoadContext.Load . Metode itu menyelesaikan referensi ke semua assembly yang merupakan dependensi assembly yang dimuat ke dalam konteks tersebut AssemblyLoadContext.
Kode berikut adalah contoh kustom AssemblyLoadContextpaling sederhana :
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(isCollectible: true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
Seperti yang Anda lihat, metode ini mengembalikan null. Itu berarti bahwa semua rakitan dependensi dimuat ke dalam konteks default, dan konteks baru hanya berisi rakitan yang dimuat secara eksplisit ke dalamnya.
Jika Anda juga ingin memuat beberapa atau semua dependensi ke AssemblyLoadContext, Anda dapat menggunakan metode AssemblyDependencyResolver dalam Load.
AssemblyDependencyResolver mengonversi nama rakitan menjadi jalur file rakitan yang absolut. Resolver menggunakan file .deps.json dan file perakitan di direktori perakitan utama yang dimuat ke dalam konteks.
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;
}
}
}
Menggunakan AssemblyLoadContext kustom yang dapat dikumpulkan
Bagian ini mengasumsikan versi yang lebih sederhana dari yang TestAssemblyLoadContext digunakan.
Anda dapat membuat instance kustom AssemblyLoadContext dan memuat assembly ke dalamnya sebagai berikut:
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
Untuk setiap rakitan yang direferensikan oleh rakitan yang dimuat, metode TestAssemblyLoadContext.Load dipanggil agar TestAssemblyLoadContext dapat memutuskan dari mana mendapatkan rakitan tersebut. Dalam hal ini, ia mengembalikan null untuk menunjukkan bahwa itu harus dimuat ke dalam konteks default dari lokasi yang digunakan runtime untuk memuat assembly secara default.
Setelah sebuah assembly dimuat, Anda kini dapat mengeksekusi sebuah metode darinya. Jalankan Main metode :
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
Main Setelah metode dikembalikan, Anda dapat memulai pembongkaran dengan memanggil metode Unload pada AssemblyLoadContext kustom atau menghapus referensi Anda ke AssemblyLoadContext.
alc.Unload();
Ini cukup untuk membongkar rakitan pengujian. Selanjutnya, Anda akan memasukkan semua ini ke dalam metode terpisah yang tidak dapat di-inline untuk memastikan bahwa TestAssemblyLoadContext, Assembly, dan MethodInfo (Assembly.EntryPoint) tidak dapat tetap hidup dengan referensi ruang tumpukan (lokal yang diperkenalkan sebenarnya atau oleh JIT). Hal itu bisa menjaga TestAssemblyLoadContext tetap aktif dan mencegah pemuatan ulang.
Selain itu, kembalikan referensi lemah ke AssemblyLoadContext sehingga Anda dapat menggunakannya nanti untuk mendeteksi penyelesaian pembongkaran.
[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();
}
Sekarang Anda dapat menjalankan fungsi ini untuk memuat, menjalankan, dan membongkar rakitan.
WeakReference testAlcWeakRef;
ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);
Namun, pembongkaran tidak segera selesai. Seperti yang disebutkan sebelumnya, mengandalkan pemungut sampah untuk mengumpulkan semua objek dari bundel pengujian. Dalam banyak kasus, tidak perlu menunggu penyelesaian pembongkaran. Namun, ada kasus di mana berguna untuk mengetahui bahwa pembongkaran telah selesai. Misalnya, Anda mungkin ingin menghapus file rakitan yang dimuat ke dalam kustom AssemblyLoadContext dari disk. Dalam kasus seperti itu, cuplikan kode berikut dapat digunakan. Ini memicu pengumpulan sampah dan menunggu finalizer yang tertunda dalam perulangan sampai referensi lemah terhadap objek khusus AssemblyLoadContext diubah menjadi null, yang menunjukkan bahwa objek target telah dikumpulkan. Dalam kebanyakan kasus, hanya diperlukan satu kali melalui perulangan. Namun, untuk kasus yang lebih kompleks di mana objek yang dibuat oleh kode yang berjalan di AssemblyLoadContext memiliki finalizer, lebih banyak tahapan mungkin diperlukan.
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Keterbatasan
Rakitan yang dimuat dalam koleksi AssemblyLoadContext harus mematuhi pembatasan umum pada rakitan yang dapat dikumpulkan. Batasan berikut juga berlaku:
- Rakitan yang ditulis dalam C++/CLI tidak didukung.
- Kode yang dihasilkan ReadyToRun akan diabaikan.
Peristiwa Pembongkaran
Dalam beberapa kasus, mungkin perlu bagi kode yang dimuat ke dalam kustom AssemblyLoadContext untuk melakukan pembersihan saat pembongkaran dimulai. Misalnya, mungkin perlu menghentikan thread atau membersihkan GC handle yang kuat. Peristiwa Unloading dapat digunakan dalam kasus seperti itu. Anda dapat menghubungkan handler yang melakukan pembersihan yang diperlukan untuk peristiwa ini.
Memecahkan masalah ketidakmampuan memuat
Karena sifat kooperatif dari pembongkaran, mudah untuk melupakan referensi yang mungkin menjaga barang dalam koleksi AssemblyLoadContext tetap ada dan mencegah pembongkaran. Berikut adalah ringkasan entitas (beberapa di antaranya tidak terlihat jelas) yang dapat menyimpan referensi:
- Referensi reguler disimpan dari luar koleksi
AssemblyLoadContextyang disimpan dalam slot tumpukan atau daftar prosesor (metode lokal, baik yang dibuat secara eksplisit oleh kode pengguna atau secara implisit oleh pengkompilasi just-in-time (JIT), variabel statis, atau handel GC yang kuat (menyematkan), dan secara transitif menunjuk ke:- Rakitan yang dimuat ke dalam koleksi
AssemblyLoadContext. - Tipe dari rakitan semacam itu.
- Instans jenis dari rakitan seperti itu.
- Rakitan yang dimuat ke dalam koleksi
- Utas yang menjalankan kode dari rakitan yang dimuat ke dalam koleksi
AssemblyLoadContext. - Instans tipe kustom dan tidak dapat dikoleksi
AssemblyLoadContextyang dibuat di dalam koleksiAssemblyLoadContext. - Instans tertunda RegisteredWaitHandle dengan panggilan balik diatur ke metode dalam kustom
AssemblyLoadContext. - Bidang pada subkelas kustom
AssemblyLoadContextAnda yang mereferensikan rakitan, jenis, atau instans jenis yang dimuat ke dalam koleksiAssemblyLoadContext. Saat pembongkaran sedang berlangsung, runtime memegang handel GC yang kuatAssemblyLoadContextuntuk mengoordinasikan pembongkaran. Ini berarti GC tidak akan mengumpulkan referensi field tersebut bahkan setelah Anda menghapus referensi Anda sendiri keAssemblyLoadContext. Kosongkan kolom ini agar pembongkaran dapat selesai.
Petunjuk / Saran
Referensi objek yang disimpan dalam slot stack atau register prosesor dan yang dapat mencegah pemindahan AssemblyLoadContext dapat terjadi dalam situasi berikut:
- Ketika hasil panggilan fungsi diteruskan langsung ke fungsi lain, meskipun tidak ada variabel lokal yang dibuat pengguna.
- Ketika pengkompilasi JIT menyimpan referensi ke objek yang tersedia di beberapa titik dalam metode.
Masalah debug pemuatanξ
Men-debug masalah dengan pembongkaran bisa melelahkan. Anda bisa masuk ke dalam situasi di mana Anda tidak tahu apa yang bisa menahan AssemblyLoadContext tetap aktif, tetapi pembongkaran gagal. Alat terbaik untuk membantu dengan itu adalah WinDbg (atau LLDB di Unix) dengan plugin SOS. Anda perlu menemukan apa yang membuat LoaderAllocator yang merupakan bagian dari AssemblyLoadContext tertentu tetap berlanjut. Plugin SOS memungkinkan Anda melihat objek tumpukan GC, hierarkinya, dan akarnya.
Untuk memuat plugin SOS ke debugger, masukkan salah satu perintah berikut di baris perintah debugger.
Di WinDbg (jika belum dimuat):
.loadby sos coreclr
Dalam LLDB:
plugin load /path/to/libsosplugin.so
Sekarang Anda akan men-debug contoh program yang memiliki masalah dengan proses "unloading". Kode sumber tersedia di bagian Contoh kode sumber . Ketika Anda menjalankannya menggunakan WinDbg, program akan masuk ke debugger tepat setelah mencoba memeriksa keberhasilan memuat ulang. Anda kemudian dapat mulai mencari pelakunya.
Petunjuk / Saran
Jika Anda melakukan debug menggunakan LLDB di Unix, perintah SOS dalam contoh berikut tidak memiliki ! di depannya.
!dumpheap -type LoaderAllocator
Perintah ini membuang semua objek dengan nama jenis yang berisi LoaderAllocator yang ada di heap GC. Berikut adalah sebuah contoh:
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
Di bagian "Statistik:", periksa MT (MethodTable) yang terkait dengan System.Reflection.LoaderAllocator, objek yang Anda fokuskan. Kemudian, dalam daftar di awal, temukan entri dengan yang cocok dengan MT yang satu itu, dan dapatkan alamat objek itu sendiri. Dalam hal ini, itu adalah "000002b78000ce40".
Sekarang setelah Anda mengetahui alamat LoaderAllocator objek, Anda dapat menggunakan perintah lain untuk menemukan akar GC-nya:
!gcroot 0x000002b78000ce40
Perintah ini mengeluarkan rantai referensi objek yang mengarah ke contoh LoaderAllocator. Daftar dimulai dengan akar, yang merupakan entitas yang menjaga LoaderAllocator tetap hidup dan dengan demikian adalah inti dari masalah. Akar dapat berupa ruang tumpukan, register prosesor, pegangan GC, atau variabel statis.
Berikut adalah contoh output gcroot perintah:
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.
Langkah selanjutnya adalah mencari tahu di mana akar berada sehingga Anda dapat memperbaikinya. Kasus yang paling mudah adalah ketika akar adalah stack slot atau register pemroses. Dalam hal ini, gcroot menunjukkan nama fungsi yang bingkainya berisi akar dan utas yang menjalankan fungsi tersebut. Kasus yang sulit adalah ketika akar adalah variabel statis atau pegangan GC.
Dalam contoh sebelumnya, akar pertama adalah lokal dari jenis System.Reflection.RuntimeMethodInfo yang disimpan dalam bingkai fungsi example.Program.Main(System.String[]) di alamat rbp-20 (rbp adalah prosesor register rbp dan -20 adalah offset heksadesimal dari register tersebut).
Akar kedua adalah normal (kuat) GCHandle yang menyimpan referensi untuk sebuah kelas test.Test.
Akar ketiga disematkan pada GCHandle. Yang satu ini sebenarnya adalah variabel statis, tetapi sayangnya, tidak ada cara untuk mengetahuinya. Statis untuk tipe referensi disimpan dalam array objek yang dikelola dalam struktur runtime internal.
Kasus lain yang dapat mencegah pembongkaran AssemblyLoadContext adalah ketika sebuah thread memiliki frame metode dari assembly yang dimuat ke AssemblyLoadContext pada tumpukannya. Anda dapat memeriksanya dengan mencadangkan tumpukan panggilan terkelola dari semua utas:
~*e !clrstack
Perintah ini berarti "terapkan perintah !clrstack ke semua utas". Berikut ini adalah output dari perintah tersebut untuk contoh. Sayangnya, LLDB di Unix tidak memiliki cara apa pun untuk menerapkan perintah ke semua utas, jadi Anda harus beralih utas secara manual dan mengulangi clrstack perintah. Abaikan semua utas saat debugger menunjukkan "Tidak dapat mengakses stack yang dikelola".
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]
Seperti yang Anda lihat, utas terakhir memiliki test.Program.ThreadProc(). Ini adalah fungsi dari rakitan yang dimuat ke dalam AssemblyLoadContext, sehingga AssemblyLoadContext tetap hidup.
Contoh kode sumber
Dalam contoh debug sebelumnya, kode berikut yang berisi masalah unloadability digunakan.
Program pengujian utama
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 yang dimuat ke dalam TestAssemblyLoadContext
Kode berikut mewakili test.dll yang diteruskan ke ExecuteAndUnload metode dalam program pengujian utama.
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;
}
}
}