.NET Core 3.0 可卸载程序集原理简析
因为最近在群里被问到如何理解 .NET Core 3.0 可卸载程序集,所以就写了这篇简单的分析。
因为时间实在很少,这篇文章只简单的罗列了相关的代码,请配合官方说明文档理解。
另外,书籍《.NET Core 底层原理》预计 11 月出版,出版社比较拖 😮。
链接
可卸载程序集的官方说明文档如下:
程序集中的 IL 代码经过 JIT 编译后,会储存原生代码在 LoaderAllocator 管理的 Code Heap 中,LoaderAllocator 的代码地址如下:
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/loaderallocator.hpp
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/loaderallocator.cpp
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/loaderallocator.inl
负责分配原生代码的是 CodeManager ,代码地址如下:
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/codeman.h
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/codeman.cpp
GC 实现代码如下:
对象头 (MethodTable) 代码如下:
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/methodtable.h
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/methodtable.inl
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/methodtable.cpp
AssemblyLoadContext 的代码如下:
分析
在 .NET Core 中我们不能新建 AppDomain (尽管有默认的几个 AppDomain),程序集会通过 AssemblyLoadContext 管理。简单的来说,AssemblyLoadContext 负责管理有依赖关系的一组程序集,例如程序集 A 依赖程序集 B,那么 A 和 B 需要使用同一个 AssemblyLoadContext 加载。每个 AssemblyLoadContext 都会关联不同的 LoaderAllocator,也就是拥有不同的 Code Heap。
.NET Core 3.0 开始允许卸载用户创建的 AssemblyLoadContext ,也就是回收 AssemblyLoadContext 为程序集分配的各种资源,包括 JIT 生成的原生代码,PreCode,类型元数据等,流程大致如下:
- 用户创建 AssemblyLoadContext (isCollectible = true)
- 用户使用 AssemblyLoadContext 加载程序集 A
- 用户使用 AssemblyLoadContext 加载程序集 B
- 用户创建程序集 A 和 B 中的类型的实例,并执行其中的方法
- 用户卸载 AssemblyLoadContext
- .NET Core 等待所有程序集 A 和 B 中的类型的实例都被回收后,释放 AssemblyLoadContext 管理的 LoaderAllocator 分配的资源
可以参考下图理解 (这是经过简化的流程,详细流程可以看前面给出的官方说明文档链接):
卸载 AssemblyLoadContext 时,取消对 LoaderAllocator 的关联的代码如下:
GC 标记对象时,同时标记关联的 LoaderAllocator 的代码如下 (在 gc.cpp 里面):
#define go_through_object_cl(mt,o,size,parm,exp) \
{ \
// 如果对象的 MethodTable 是由可回收的 AssemblyLoadContext 加载的
if (header(o)->Collectible()) \
{ \
// 获取关联的 LoaderAllocator
uint8_t* class_obj = get_class_object (o); \
uint8_t** parm = &class_obj; \
// 标记 LoaderAllocator (根据 exp 的具体逻辑而定)
do {exp} while (false); \
} \
// 如果对象包含引用类型的成员
if (header(o)->ContainsPointers()) \
{ \
go_through_object_nostart(mt,o,size,parm,exp); \
} \
}
// 调用 MethodTable::GetLoaderAllocatorObjectForGC
#define get_class_object(i) GCToEEInterface::GetLoaderAllocatorObjectForGC((Object *)i)
LoaderAllocator 被回收以后到释放资源的相关代码 (被回收之前的逻辑参考官方说明文档) :
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/loaderallocator.cpp#L520
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/appdomain.cpp#L857
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/appdomain.cpp#L817
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/appdomain.hpp#L3086
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/appdomain.cpp#L6240
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/appdomain.cpp#L6283
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/loaderallocator.cpp#L88
- https://github.com/dotnet/coreclr/blob/release/3.0/src/vm/loaderallocator.cpp#L1301
说明就到此为止了。你可能会奇怪为什么这篇文章没有提到 Assembly 和 DomainAssembly ,这是因为它们在可卸载程序集的实现中并不重要,资源是通过 AssemblyLoadContext 关联的 LoaderAllocator 统一分配和释放的,与其说是可卸载程序集,不如说是可卸载程序集加载上下文 (AssemblyLoadContext)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
2016-09-13 如何编写一个简单的依赖注入容器