Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
.NET (Core) представила возможность загрузки и последующей выгрузки набора сборок. В .NET Framework домены пользовательских приложений использовались для этой цели, но .NET (Core) поддерживает только один домен приложения по умолчанию.
Поддержка разгружаемости реализована через AssemblyLoadContext. Вы можете загрузить набор сборок в коллекцию AssemblyLoadContext, выполнить методы в них или просто проверить их с помощью отражения и, наконец, выгрузить AssemblyLoadContext. Это выгрузит сборки, загруженные в AssemblyLoadContext.
Существует одно важное различие между выгрузками с помощью AssemblyLoadContext и использованием доменов приложений. При использовании доменов приложений принудительно выполняется выгрузка. Во время выгрузки все потоки, выполняемые в целевом домене приложений, прерваны, управляемые COM-объекты, созданные в целевом домене приложений, уничтожаются и т. д. С помощью AssemblyLoadContext выгрузка является "кооперативной". Вызов метода AssemblyLoadContext.Unload просто инициирует выгрузку. Выгрузка завершается после:
- Ни один поток не имеет методов из загруженных сборок в
AssemblyLoadContextна своих стеках вызовов. - Ни один из типов из сборок, загруженных в
AssemblyLoadContext, ни экземпляры этих типов, ни сами сборки не ссылаются на:- Ссылки за пределами
AssemblyLoadContext, за исключением слабых ссылок (WeakReference или WeakReference<T>). - Надежный сборщик мусора (GC) обрабатывает (GCHandleType.Normal или GCHandleType.Pinned) как внутри, так и за пределами
AssemblyLoadContext.
- Ссылки за пределами
Использование собираемого AssemblyLoadContext
В этом разделе содержится подробное пошаговое руководство, в котором показано, как просто загрузить приложение .NET (Core) в собираемую область AssemblyLoadContext, вызвать его точку входа и затем выгрузить. Полный пример можно найти по адресу https://github.com/dotnet/samples/tree/main/core/tutorials/Unloading.
Создание объекта collectible AssemblyLoadContext
Создайте производный класс от AssemblyLoadContext и переопределите его метод AssemblyLoadContext.Load. Этот метод разрешает ссылки на все сборки, которые являются зависимостями сборок, загруженных в эту область AssemblyLoadContext.
Следующий код является примером простейшей пользовательской AssemblyLoadContextфункции:
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(isCollectible: true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
Как видно, метод Load возвращает null. Это означает, что все сборки зависимостей загружаются в контекст по умолчанию, а новый контекст содержит только сборки, явно загруженные в него.
Если вы хотите загрузить некоторые или все зависимости в AssemblyLoadContext, можно использовать AssemblyDependencyResolver в методе Load. Компонент AssemblyDependencyResolver преобразует имена сборок в абсолютные пути к файлам сборки. Резолвер использует файл .deps.json и файлы сборок в каталоге основной сборки, загруженной в контекст.
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;
}
}
}
Использование настраиваемого собираемого AssemblyLoadContext
В этом разделе предполагается использование более простой версии TestAssemblyLoadContext.
Вы можете создать экземпляр настраиваемой AssemblyLoadContext сборки и загрузить ее следующим образом:
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
Для каждой сборки, на которую ссылается загруженная сборка, вызывается метод TestAssemblyLoadContext.Load, чтобы TestAssemblyLoadContext мог решить, откуда получить сборку. В этом случае значение null возвращается, чтобы указать, что его следует загрузить в контекст по умолчанию из расположений, которые среда выполнения по умолчанию использует для загрузки сборок.
Теперь, когда сборка была загружена, можно выполнить метод из него.
Main Запустите метод:
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
После возврата метода Main, можно инициировать выгрузку, вызвав метод Unload на пользовательском AssemblyLoadContext или удалив ссылку на AssemblyLoadContext.
alc.Unload();
Это достаточно для выгрузки тестовой сборки. Затем вы поместите все это в отдельный неинлайнимый метод, чтобы гарантировать, что TestAssemblyLoadContext, Assembly и MethodInfo (то есть Assembly.EntryPoint) не могут поддерживаться благодаря ссылкам на слоты стека (реальные или добавленные JIT локальные объекты). Это может сохранить TestAssemblyLoadContext в рабочем состоянии и предотвратить завершение выгрузки.
Кроме того, верните слабую ссылку на AssemblyLoadContext так, чтобы её можно было использовать позже для обнаружения завершения разгрузки.
[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();
}
Теперь эту функцию можно запустить для загрузки, выполнения и выгрузки сборки.
WeakReference testAlcWeakRef;
ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);
Однако выгрузка не завершается немедленно. Как упоминалось ранее, система полагается на сборщик мусора для сбора всех объектов из тестовой сборки. Во многих случаях не нужно ждать завершения выгрузки. Однако есть случаи, когда полезно знать, что выгрузка завершена. Например, может потребоваться удалить файл сборки, который был загружен в кастомный AssemblyLoadContext с диска. В таком случае можно использовать следующий фрагмент кода. Он активирует сборку мусора и в цикле ожидает завершения всех отложенных финализаторов до тех пор, пока слабая ссылка на пользовательский AssemblyLoadContext не станет равной null, указывая на то, что целевой объект был собран. В большинстве случаев требуется только один проход через цикл. Однако в более сложных случаях, когда объекты, созданные кодом, выполняющимся в AssemblyLoadContext, имеют методы завершения, могут потребоваться дополнительные проходы.
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Ограничения
Сборки, загруженные в коллекцию AssemblyLoadContext , должны соблюдать общие ограничения на сборные сборки. Кроме того, применяются следующие ограничения:
- Сборки, написанные в C++/CLI, не поддерживаются.
- Созданный код ReadyToRun будет игнорироваться.
Событие выгрузки
В некоторых случаях может потребоваться, чтобы код, загруженный в пользовательскую AssemblyLoadContext, выполнял определённую очистку при инициировании выгрузки. Например, может потребоваться остановить потоки или очистить надежные дескрипторы GC. Это Unloading событие можно использовать в таких случаях. Вы можете подключить обработчик, который выполняет необходимую очистку для этого события.
Устранение неполадок при выгрузке
Из-за кооперативного характера выгрузки легко забыть о ссылках, которые могут поддерживать объекты в коллекционируемых AssemblyLoadContext актуальными и препятствовать их выгрузке. Ниже приведена сводка сущностей (некоторые из них неочевидны), которые могут содержать ссылки:
- Регулярные ссылки, находящиеся за пределами
AssemblyLoadContext, хранящиеся в слоте стека или регистре процессора (локальные переменные, созданные явным образом пользовательским кодом или неявно компилятором JIT), статическая переменная или строгий (закрепляющий) дескриптор GC и транзитивно указывающие на:- Сборка, загруженная в коллекцию
AssemblyLoadContext. - Тип из такой сборки.
- Экземпляр типа из такой сборки.
- Сборка, загруженная в коллекцию
- Потоки, работающие с кодом из сборки, загруженной в коллекцию
AssemblyLoadContext. - Экземпляры пользовательских, несобираемых
AssemblyLoadContextтипов, созданные внутри собираемыхAssemblyLoadContextтипов. - Ожидающие RegisteredWaitHandle экземпляры с обратными вызовами, заданными для методов в пользовательском объекте
AssemblyLoadContext. - Поля в пользовательском
AssemblyLoadContextподклассе, ссылающиеся на сборки, типы или экземпляры типов, загруженных в коллекциюAssemblyLoadContext. Во время выгрузки среда выполнения содержит сильный дескриптор GC дляAssemblyLoadContextкоординации выгрузки. Это означает, что GC не будет собирать эти ссылки на поля, даже после того как вы удалите собственную ссылку наAssemblyLoadContext. Очистите эти поля, чтобы завершить выгрузку.
Подсказка
Ссылки на объекты, хранящиеся в слотах стека или регистрах процессора и которые могут предотвратить выгрузку AssemblyLoadContext, могут возникнуть в следующих ситуациях:
- Когда результаты вызова функции передаются непосредственно в другую функцию, несмотря на отсутствие локальной переменной, созданной пользователем.
- Когда компилятор JIT сохраняет ссылку на объект, который был доступен в какой-то момент в методе.
Отладка проблем выгрузки
Отладка проблем с выгрузкой может оказаться утомительной. Вы можете попасть в ситуации, когда вы не знаете, что может удерживать AssemblyLoadContext в рабочем состоянии, но выгрузка не удается. Лучший инструмент, помогающий с этим — WinDbg (или LLDB в Unix) с подключаемым модулем SOS. Вам нужно найти, что поддерживает LoaderAllocator, относящийся к конкретному AssemblyLoadContext, в живом состоянии. Подключаемый модуль SOS позволяет взглянуть на объекты кучи GC, их иерархии и корни.
Чтобы загрузить подключаемый модуль SOS в отладчик, введите одну из следующих команд в командной строке отладчика.
В WinDbg (если он еще не загружен):
.loadby sos coreclr
В LLDB:
plugin load /path/to/libsosplugin.so
Теперь вы отладите пример программы, которая имеет проблемы с выгрузкой. Исходный код доступен в разделе "Пример исходного кода ". Когда вы запускаете программу под WinDbg, она переходит в отладчик сразу после попытки проверить успешность выгрузки. Затем можно начать искать виновных.
Подсказка
При отладке с помощью LLDB на Unix команды SOS в следующих примерах не имеют перед ними !.
!dumpheap -type LoaderAllocator
Эта команда выводит все объекты с именем типа, содержащим LoaderAllocator, которые находятся в куче GC. Приведем пример:
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
В части "Статистика:" проверьте MT (MethodTable), который принадлежит System.Reflection.LoaderAllocator, это объект, который вас интересует. Затем в списке в начале найдите запись с MT, которая соответствует этой, и получите адрес самого объекта. В этом случае это "000002b78000ce40".
Теперь, когда вы знаете адрес LoaderAllocator объекта, можно использовать другую команду, чтобы найти его корни GC:
!gcroot 0x000002b78000ce40
Эта команда сбрасывает цепочку ссылок на объекты, которые приводят к экземпляру LoaderAllocator . Список начинается с корневого элемента, который является сущностью, поддерживающей существование LoaderAllocator, и, следовательно, являющейся ядром проблемы. Корневой каталог может быть слотом стека, регистром процессора, дескриптором GC или статической переменной.
Ниже приведен пример выходных данных gcroot команды:
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.
Следующий шаг — выяснить, где расположен корень, чтобы исправить его. Самый простой случай — когда корень — это слот стека или регистр процессора. В этом случае gcroot отображает имя функции, кадр которой содержит корень, а также поток, выполняющий эту функцию. Трудным случаем является то, что корневой каталог является статической переменной или дескриптором GC.
В предыдущем примере первый корень является локальным типом System.Reflection.RuntimeMethodInfo , хранящимся в кадре функции example.Program.Main(System.String[]) по адресу rbp-20 (rbp является регистром rbp процессора и -20 является шестнадцатеричным смещением из этого регистра).
Второй корень — это обычный (сильный) GCHandle, который содержит ссылку на экземпляр класса test.Test.
Третий корень является закрепленным GCHandle. Это на самом деле статическая переменная, но, к сожалению, нет способа сказать. Статические типы ссылок хранятся в массиве управляемых объектов во внутренних структурах среды выполнения.
Другой случай, который может препятствовать выгрузке
~*e !clrstack
Команда означает "применить ко всем потокам команду !clrstack". Ниже приведены выходные данные этой команды для примера. К сожалению, LLDB в Unix не имеет способа применить команду ко всем потокам, поэтому необходимо вручную переключать потоки и повторять clrstack команду. Игнорировать все потоки, в которых отладчик говорит: "Не удается пройти управляемый стек".
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]
Как видите, последний поток имеет test.Program.ThreadProc(). Это функция из сборки, загруженной в AssemblyLoadContext, и поэтому она поддерживает AssemblyLoadContext в рабочем состоянии.
Пример исходного кода
Следующий код, содержащий проблемы с выгрузками, используется в предыдущем примере отладки.
Основная программа тестирования
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}");
}
}
}
Программа, загруженная в TestAssemblyLoadContext
Следующий код представляет test.dll , переданный ExecuteAndUnload методу в основной программе тестирования.
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;
}
}
}