
深入解析 .NET CoreCLR 类型系统核心数据结构、查找算法与 GC/栈遍历约束【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime导读本文基于 dotnet/runtime 仓库中 Book of the RuntimeBOTR系列的 type-system.md 编写系统讲解 CoreCLR 类型系统的整体架构。类型系统是 CLR 中表示 ECMA-335 规范含扩展所描述类型体系的运行时数据机构集合它既不是反射暴露出来的那个类型系统尽管反射依赖它又是 JIT、GC、栈遍历器、安全系统等几乎所有核心组件的信息底座。读完本文你将掌握 MethodTable、EEClass、MethodDesc、FieldDesc、TypeDesc、ClassLoader 等核心数据结构的分工理解类型查找RidMap、TypeRef/TypeSpec 解析、类型比较CanCastTo 系列与虚方法分派等关键算法的设计取舍以及 GC 与栈遍历器对类型系统施加的严格约束并深入了解 .NET 9 时代静态变量与线程静态变量的存储与生命周期管理机制。类型系统是什么数据机构 算法CLR 类型系统是对 ECMA-335 规范中描述的类型系统的运行时表示并带有若干扩展。它的构成可以概括为两部分数据结构一组运行期数据结构其中部分在 BOTR 其他章节如 type-loader.md、method-descriptor.md中有专门论述算法一组对这些数据结构进行创建与操作的算法。需要特别澄清的是它并不是通过反射暴露出来的类型系统虽然反射类型系统正是建立在该系统之上——反射借助类型系统来相对简单地访问 ECMA 标准化概念这些概念恰好被保存在 CLR 类型系统数据结构中。主要数据结构数据结构职责MethodTable每个已加载类型含数组、泛型实例等的核心描述托管对象头部即指向它GC 布局信息挂接其上EEClass存放类型中偏向编译期/布局期的信息与 MethodTable 配合BuildMethodTable 负责构建它MethodDesc描述一个方法承载方法的入口点、签名、实现信息FieldDesc描述一个字段包括字段偏移、静态字段的 statics 基址调整等TypeDesc描述非普通类的类型变体数组、指针、泛型变量等ClassLoader负责查找、加载类型并维护已加载类型索引核心算法Type Loader类型加载器加载类型并创建上述大部分核心数据结构CanCastTo 及同类比较算法完成类型之间的比较类型转换判断LoadTypeHandle主要用于查找类型签名解析Signature parsing比较并收集方法/字段信息GetMethod / GetFieldDesc查找或加载方法/字段Virtual Stub Dispatch虚拟桩分派解析对接口的虚调用目标。此外还有大量提供各类辅助信息的附属数据结构与算法但对整体理解类型系统而言优先级较低。组件依赖类型系统在 CLR 中的位置类型系统本质上是向 CLR 众多组件提供服务的服务方几乎所有核心组件都不同程度地依赖它的行为。下图为类型系统主要信息流依赖关系非穷尽类型系统所依赖的组件Loader需要正确的元数据才能开展工作Metadata 系统通过元数据 API 收集信息参见仓库内 dotnet-standards.md 中关于 ECMA CLI 元数据的说明Security 系统告知类型系统某些类型系统结构是否被允许例如继承关系AppDomain提供 LoaderAllocator 以处理类型系统数据结构的内存分配行为。依赖类型系统的三大组件JIT 接口与 JIT helpers主要依赖类型、方法、字段的查找功能。一旦找到类型系统对象返回的数据结构已经按 JIT 的需求裁剪好——这也是从源码结构看methodtable.h、typehandle.h这些头文件被 JIT 接口广泛引用的原因Reflection借助类型系统相对简单地访问 ECMA 标准化概念一般托管代码执行类型比较逻辑与虚拟桩分派都依赖类型系统。类型系统的设计目标与非目标目标从执行中的非反射代码访问运行时所需信息非常快从生成代码的角度访问编译期所需信息直接明了垃圾回收器/栈遍历器在不加锁、不分配内存的情况下也能取得必要信息一次加载的类型数量最少某个类型在类型加载时刻只加载最少量的内容类型系统数据结构必须可存储进 NGEN 映像便于跨进程共享与快速启动。非目标元数据中的全部信息都被直接反映到 CLR 数据结构中反射的所有用法都很快。这两条非目标解释了为什么反射通常比直接 JIT 代码访问信息慢以及为什么 CLR 数据结构刻意按需裁剪而非元数据全量镜像。典型运行时算法设计以类型转换casting为例类型转换算法是托管代码执行期间被高频使用的典型算法。它至少有 4 个独立入口每个入口都被设计成不同的快速路径以期获得最佳性能对象能否转换为某个非类型等价、非数组类型含转换到父类型变体对象能否转换为某个不支持泛型变体的接口类型对象能否转换为某个数组类型某类型的对象能否转换为任意其他托管类型。其中前三个实现都以牺牲完全通用性为代价换取更优性能。例如能否转换为父类型这一变体用一个单循环遍历单向链表实现只能搜索转换操作的子集但通过检查目标类型即可判断是否属于适用集合对应 JIT helperJIT_ChkCastClass_Portable。在仓库中通用性更强的比较逻辑以MethodTable::CanCastToInterfacemethodtable.cpp与MethodTable::CanCastToClassmethodtable.cpp等成员函数实现而TypeHandle::CastResult枚举则定义了CannotCast / CanCast / MaybeCast三态结果typehandle.hMaybeCast正是通用路径中需要进一步递归判断的信号。设计假设专用化的算法实现总体上是一种性能改进算法的额外版本不会带来不可逾越的维护负担。类型系统典型查找算法设计类型系统最常用的操作之一就是查找一个类型。触发方多种多样JIT、反射、序列化、远程调用remoting等。输入与解码查找的基本输入为搜索起始上下文Module 或 assembly 指针标识符描述在初始上下文中要查找的类型通常是一个 token或当搜索上下文为程序集时一个字符串。算法必须先解码标识符。针对查找类型场景token 可能是 TypeDef、TypeRef、TypeSpec 或字符串四种输入走四条不同路径输入查找方式typedef token在 Module 的RidMap中查找——本质是简单的数组下标索引速度最快typeref token先查找该 typeref 指向的程序集然后用从 typeref 表取得的字符串以新程序集指针重新启动类型查找算法typespec token表示必须解析签名才能得到信息解析签名、取得加载类型所需信息这会递归触发更多类型查找name名称用于程序集之间绑定在 TypeDef/ExportedTypes 表中搜索匹配项该搜索通过清单模块对象上的哈希表做了优化由此体现的通用特征搜索输入与元数据紧密耦合元数据 token 与字符串名被频繁传递且搜索与 Module 绑定Module 直接映射 .dll/.exe 文件使用缓存信息提升性能RidMap 与哈希表正是为此优化的数据结构算法通常根据输入拥有3~4 条不同路径。附加约束假设ASSUMPTION在 GC 暂停期间搜索已加载类型是安全的不变量INVARIANT已加载的类型只要被搜索就一定能找到问题ISSUE搜索例程依赖元数据读取在某些场景下性能可能不理想。该搜索算法是 JIT 编译期间例程的典型代表其特征是使用元数据、需要在多处查找数据、数据结构中数据重复度相对较低、通常不深度递归且无循环——这正满足了基于 IL 的 JIT 的性能要求与工作特性。垃圾回收器对类型系统的要求GC 需要获知 GC 堆上分配的类型实例的信息实现方式是在每个托管对象头部放置一个指向类型系统数据结构MethodTable的指针。MethodTable 上挂接着描述该类型实例 GC 布局的数据结构布局有两种形式普通类型与对象数组object arrays值类型数组arrays of valuetypes。关键假设与要求假设类型系统数据结构的生命周期长于其描述类型所对应的托管对象要求GC 需要在运行时挂起期间执行栈遍历器详见下节。栈遍历器对类型系统的要求栈遍历器含 GC 栈遍历器在两种情况下需要类型系统输入查找栈上值类型的大小查找栈上值类型内部需要上报的 GC 根。由于延迟加载类型与避免生成仅 GC 信息不同而重复的代码版本这两个原因CLR 目前要求遍历栈上方法的签名signature walk。这一需求很少被触发——它要求栈遍历器在非常特定的时刻执行但为了满足可靠性目标签名遍历器必须能在栈遍历期间正常工作。栈遍历器约在三种模式下运行出于安全或异常处理目的遍历当前线程栈出于 GC 目的遍历所有线程栈此时所有线程已被 EE 挂起为 profiler 遍历特定线程栈该线程被挂起。在 GC 栈遍历与 profiler 栈遍历两种场景下由于线程被挂起分配内存或获取大多数锁都是不安全的。这促使类型系统提供一条可依赖的、满足上述约束的路径其核心规则是如果一个方法已被调用那么被调用方法的所有值类型参数必然已在进程中的某个 AppDomain 加载从含签名的程序集到实现该类型程序集的程序集引用必须在栈遍历需要签名遍历之前完成解析。该规则由类型加载器、NGEN 映像生成过程与 JIT 中一整套复杂且繁多的强制机制来保障。文档同时指出三个已知问题栈遍历器对类型系统的要求极度脆弱其实现要求类型系统中每个可能在搜索已加载类型时被触及的函数都进行一组契约违反contract violations签名遍历使用常规签名遍历代码该代码设计为边遍历边加载类型但此场景下类型加载功能被假定不会真正触发加载该需求不仅要求类型系统支持还要求程序集加载器配合Loader 在这方面出现过不少问题。静态变量Static Variables在 CoreCLR 中静态变量的处理方式为先取得static base静态基址再按偏移量调整得到指向实际值的指针。每个字段的 statics base 都被区分为non-gc或gc两类Non-GC statics由基本类型byte、sbyte、char、int、uint、long、ulong、float、double、各种形式的指针与枚举表示的静态变量GC statics由类或非基本值类型表示的静态变量对于属于 GC statics 的值类型静态变量该静态变量实际是指向该值类型装箱实例的指针。每类型的静态变量信息.NET 9 起自 .NET 9 起静态变量基址全部与其所属类型关联。数据获取路径为从MethodTable出发取MethodTableAuxiliaryData通过m_pAuxData再按类型情况取DynamicStaticsInfo获得 statics 指针、GenericStaticsInfo泛型类型的 FieldDesc 列表或ThreadStaticsInfo获得 TLSIndex进而配合线程静态系统取得实际线程静态基址关键设计点普通静态变量使用单个指针宽度字段该字段同时编码类构造函数是否已运行HasClassConstructorBeenRun。这是为了能以无锁原子方式同时完成取静态字段地址与判断是否需要触发类构造函数TLS 静态变量的类构造函数是否已运行检测更复杂由线程静态基础设施处理DynamicStaticsInfo与ThreadStaticsInfo结构在不加锁的情况下被访问因此必须保证对这些结构字段的单次内存访问即可完成避免内存序撕裂memory order tearing问题对于泛型类型每个字段都有一个按类型实例分配的FieldDesc不被多个 canonical 实例共享。可回收程序集的静态变量生命周期管理CoreCLR 支持可回收程序集collectible assemblies因此需要专门管理静态变量的生命周期。采用方案是构建一种特殊的 GC handle 类型允许运行时数据结构持有指向 GC 堆上托管对象内部interior的指针。行为要求静态变量不能让自己的可回收程序集保持存活。因此可回收静态变量具有一个奇特性质——它们可以在可回收程序集最终被回收之前就存在并被终结finalize。如果出现复活resurrection场景可能产生非常令人惊讶的行为。线程静态变量Thread Statics线程静态变量的生命周期被定义为包含该静态变量的类型的生命周期与访问该静态变量的线程的生命周期两者中的较短者。其创建方式是在类型上声明带有[System.Runtime.Serialization...ThreadStaticAttribute]即[System.Runtime.CompilerServices.ThreadStaticAttribute]特性的静态变量。整体方案为类型分配一个在所有线程上都相同的索引index每个线程持有一个可通过该索引高效访问的数据结构。CoreCLR 的实现有几个特色将可回收与非可回收的线程静态分开TLSIndexType::NonCollectible与TLSIndexType::Collectible提供在 native CoreCLR 代码与托管代码之间共享 non-gc 线程静态的能力TLSIndexType::DirectOnThreadLocalData的子集提供一种极其高效的方式访问少量 non-gc 线程静态TLSIndexType::DirectOnThreadLocalData的其余用法。上述TLSIndexType分类与GetThreadLocalStaticBase、DirectOnThreadLocalData路径在源码中均有对应实现见 threadstatics.cpp例如 threadstatics.cpp 的GetThreadLocalStaticBase及 threadstatics.cpp 附近对DirectOnThreadLocalData索引的分配逻辑。每线程静态数据结构线程静态地址的访问模式JIT 生成非DirectOnThreadLocalData的线程静态JIT 生成的访问模式以某种方式取得 TLS index取得当前线程 OS 管理的 TLS 块指针即pThreadLocalData t_ThreadStatics读取 1 个整数值pThreadLocalData-cCollectibleTlsData或pThreadLocalData-cNonCollectibleTlsData与要查找的索引比较if (cTlsData index.GetIndexOffset())若索引不在范围内跳到第 11 步从 TLS 块读取 1 个指针值pThreadLocalData-pCollectibleTlsArrayData或pThreadLocalData-pNonCollectibleTlsArrayData从 TLS 数组内读取 1 个指针pTLSBaseAddress *(intptr_t*)(((uint8_t*)pTlsArrayData) index.GetIndexOffset())若指针为 NULLpTLSBaseAddress NULL跳到第 11 步若 TLS index 不是 Collectible 索引直接返回pTLSBaseAddress若ObjectFromHandle((OBJECTHANDLE)pTLSBaseAddress)为 NULL跳到第 11 步返回ObjectFromHandle((OBJECTHANDLE)pTLSBaseAddress)尾调用 helperreturn GetThreadLocalStaticBase(index)。DirectOnThreadLocalData上的线程静态JIT 生成的访问模式以某种方式取得 TLS index取得当前线程 OS 管理的 TLS 块指针pThreadLocalData t_ThreadStatics将索引偏移量加到 ThreadLocalData 结构起始处pTLSBaseAddress ((uint8_t*)pThreadLocalData) index.GetIndexOffset()。可以看到DirectOnThreadLocalData路径将访问压缩到取块指针 一次偏移计算这正是文档所称极其高效的原因而普通路径则是一组精心排序的快速失败判断最后才尾调用GetThreadLocalStaticBase对应 threadstatics.cpp 的实现慢速兜底。线程静态变量的生命周期管理实现区分可回收与非可回收线程静态是出于效率考虑。非可回收线程静态定义在运行时不可回收类型上的线程静态实际观察中绝大多数线程静态属于此类。DirectOnThreadLocalData静态是该类别的特殊优化子集不需要任何 GC 上报。其ThreadLocalData中的指针pNonCollectibleTlsArrayData指向一个托管object[]该数组的槽位指向object[]、byte[]或double[]数组。GC 扫描时只需上报指向初始object[]的指针这一个细节。可回收线程静态定义在可被运行时回收类型上的线程静态。其ThreadLocalData中的指针pCollectibleTlsArrayData指向一块由malloc分配的内存槽位持有指向object[]、byte[]或double[]数组的指针。GC 扫描时每个托管对象仅在类型与线程都存活时才需要被单独保活这要求妥善处理以下四种情况可回收程序集变为无引用但与其关联的线程静态变量带有终结器——对象必须移入终结队列与可回收程序集关联的线程静态变量通过一串对象引用指向该程序集的LoaderAllocator——它不能成为该程序集被视为有引用的理由可回收程序集被回收后关联的静态变量不再存在该程序集关联的TLSIndex值变得可复用线程不再执行后该线程关联的所有线程静态不再被保活。采用的方案是一对不同类型的 handle为高效访问动态调整数组槽位中存放的是WeakTrackResurrection GCHandle。该 handle 实例与 TLS 数据中的槽位而非具体实例化关联因此当关联的可回收程序集被回收、槽位被复用时可以复用每个使用中的槽位还持有一个LOADERHANDLE负责在LoaderAllocator释放前保活对象。若LoaderAllocator被回收该LOADERHANDLE会被弃置——这没问题因为LOADERHANDLE只在LoaderAllocator未被回收时才需要清理在线程销毁时针对 TLS 数组中每个可回收槽位会在正确的LoaderAllocator上显式释放LOADERHANDLE。物理架构代码分布在哪些文件类型系统的主要代码分布于src/coreclr/vm目录下文件内容Class.cpp/inl/hEEClass 相关函数以及 BuildMethodTableMethodTable.cpp/inl/h操作 MethodTable 的函数TypeDesc.cpp/inl/h检查 TypeDesc 的代码MetaSig.cpp / SigParser签名代码FieldDesc / MethodDesc检查这些数据结构的函数Generics泛型专属逻辑Array数组处理特例代码VirtualStubDispatch.cpp/h/inl虚拟桩分派代码VirtualCallStubCpu.hpp虚拟桩分派的处理器相关代码threadstatics.cpp/h线程静态变量处理threadstatics.cpp主要入口点包括BuildMethodTable构建类型核心结构LoadTypeHandleThrowing带异常语义的 TypeHandle 加载CanCastTo*类型转换判断系列GetMethodDescFromMemberDefOrRefOrSpecThrowing从 MemberDef/Ref/Spec token 获取 MethodDescGetFieldDescFromMemberRefThrowing从 MemberRef 获取 FieldDescCompareSigs签名比较VirtualCallStubManager::ResolveWorkerStatic虚拟桩分派解析。延伸阅读ECMA CLI 规范说明Type Loader类型加载器——类型加载的数据结构与算法Virtual Stub Dispatch虚拟桩分派——虚调用目标解析的复杂案例MethodDesc方法描述——方法数据结构的专门章节Shared Generics共享泛型——泛型实例化的表示与共享策略Stackwalking栈遍历——栈遍历器的工作方式【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考