一:背景
1. 讲故事
最近在分析一个崩溃dump时,发现祸首和AssemblyLoadContext有关,说实话这东西我也比较陌生,后来查了下大模型,它主要奔着替代 .NetFrameWork 时代的 AppDomain 的,都是用来做晚期加卸载,实现对宿主程序的可插拔,AppDomain.Create 是在AppDomain级别上,后者是在 Assembly 级别上。
二:Assembly 插拔分析
1. 一个简单的案例
简单来说这东西可以实现 Assembly 的可插拔,这个小案例有三个基本元素。
这块比较简单,新建一个类库,里面主要就是组件需要实现的接口。- namespace MyClassLibrary.Interfaces
- {
- public interface IPlugin
- {
- string Name { get; }
- string Version { get; }
- void Execute();
- string GetResult();
- }
- }
复制代码 新建一个组件,完成这些接口方法的实现。- public class SamplePlugin : IPlugin
- {
- public string Name => "Sample Plugin";
- public string Version => "1.0.0";
- public void Execute()
- {
- Console.WriteLine("SamplePlugin is executing...");
- }
- public string GetResult()
- {
- return "Hello from SamplePlugin!";
- }
- }
复制代码
- 自定义的 CustomAssemblyLoadContext 上下文
最后就是在调用处自定义下 AssemblyLoadContext 以及简单调用,参考代码如下:- namespace Example_1_6
- {
- internal class Program
- {
- static void Main(string[] args)
- {
- Console.WriteLine("=== 插件系统启动 ===");
- // 设置插件目录
- string pluginsPath = @"D:\sources\woodpecker\Test\MyClassLibrary\bin\Debug\net8.0";
- Console.WriteLine($"插件路径: {pluginsPath}");
- var dllFile = Directory.GetFiles(pluginsPath, "MyClassLibrary.dll").FirstOrDefault();
- var _loadContext = new CustomAssemblyLoadContext("MyPluginContext", pluginsPath);
- var assembly = _loadContext.LoadAssembly(dllFile);
- var type = assembly.GetType("MyClassLibrary.SamplePlugin");
- IPlugin plugin = (IPlugin)Activator.CreateInstance(type);
- Console.WriteLine($"- {plugin.Name} v{plugin.Version}");
- Console.WriteLine($"\n执行插件: {plugin.Name} v{plugin.Version}");
- plugin.Execute();
- string result = plugin.GetResult();
- Console.WriteLine($"插件返回: {result}");
- Console.ReadKey();
- }
- }
- public class CustomAssemblyLoadContext : AssemblyLoadContext
- {
- private readonly string _dependenciesPath;
- public CustomAssemblyLoadContext(string name, string dependenciesPath)
- : base(name, isCollectible: true)
- {
- _dependenciesPath = dependenciesPath;
- }
- public Assembly LoadAssembly(string assemblyPath)
- {
- return LoadFromAssemblyPath(assemblyPath);
- }
- public new void Unload()
- {
- base.Unload();
- }
- }
- }
复制代码 将代码运行起来,可以看到插件代码得到执行。
2. 组件已经插上了吗
plugin中的方法都已经执行了,那 MyClassLibrary.dll 自然就插上去了,接下来如何验证呢?可以使用 windbg 的 !dumpdomain 命令即可。- 0:015> !dumpdomain
- --------------------------------------
- System Domain: 00007ff8e9d4b150
- LowFrequencyHeap: 00007FF8E9D4B628
- HighFrequencyHeap: 00007FF8E9D4B6B8
- StubHeap: 00007FF8E9D4B748
- Stage: OPEN
- Name: None
- --------------------------------------
- Domain 1: 00000211d617dc80
- LowFrequencyHeap: 00007FF8E9D4B628
- HighFrequencyHeap: 00007FF8E9D4B6B8
- StubHeap: 00007FF8E9D4B748
- Stage: OPEN
- Name: clrhost
- Assembly: 00000211d613e560 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.16\System.Private.CoreLib.dll]
- ClassLoader: 00000211D613E5F0
- Module
- 00007ff889d54000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.16\System.Private.CoreLib.dll
- ...
- Assembly: 000002118052b0d0 [D:\sources\woodpecker\Test\MyClassLibrary\bin\Debug\net8.0\MyClassLibrary.dll]
- ClassLoader: 000002118052B160
- Module
- 00007ff88a11c060 D:\sources\woodpecker\Test\MyClassLibrary\bin\Debug\net8.0\MyClassLibrary.dll
复制代码 从卦中可以清晰的看到 MyClassLibrary.dll 已经成功的送入。
3. 组件如何卸载掉
能不能卸载掉,其实取决于你在 new AssemblyLoadContext() 时塞入的 isCollectible 字段决定的,如果为true就是一个可卸载的程序集,参考代码如下:- public CustomAssemblyLoadContext(string name, string dependenciesPath)
- : base(name, isCollectible: true)
- {
- _dependenciesPath = dependenciesPath;
- }
复制代码 其次要知道的是卸载程序集是一个异步操作,不要以为调用了 UnLoad() 就会立即卸载,它只是起到了一个标记删除的作用,只有程序集中的实例无引用根了,即垃圾对象的时候,再后续由 GC 来实现卸载。
这一块我们可以写段代码来验证下,我故意将逻辑包装到 DoWork() 方法中,然后处理完之后再次触发GC,修改后的代码如下:- internal class Program
- {
- static void Main(string[] args)
- {
- DoWork();
- GC.Collect();
- GC.WaitForPendingFinalizers();
- Console.WriteLine("GC已触发,请再次观察 Assembly 是否被卸载...");
- Console.ReadLine();
- }
- static void DoWork()
- {
- Console.WriteLine("=== 插件系统启动 ===");
- // 设置插件目录
- string pluginsPath = @"D:\sources\woodpecker\Test\MyClassLibrary\bin\Debug\net8.0";
- Console.WriteLine($"插件路径: {pluginsPath}");
- var dllFile = Directory.GetFiles(pluginsPath, "MyClassLibrary.dll").FirstOrDefault();
- var _loadContext = new CustomAssemblyLoadContext("MyPluginContext", pluginsPath);
- var assembly = _loadContext.LoadAssembly(dllFile);
- var type = assembly.GetType("MyClassLibrary.SamplePlugin");
- IPlugin plugin = (IPlugin)Activator.CreateInstance(type);
- Console.WriteLine($"- {plugin.Name} v{plugin.Version}");
- Console.WriteLine($"\n执行插件: {plugin.Name} v{plugin.Version}");
- plugin.Execute();
- string result = plugin.GetResult();
- Console.WriteLine($"插件返回: {result}");
- _loadContext.Unload();
- Console.WriteLine("程序集已标记为卸载... 请观察 Assembly 是否被卸载...");
- Console.ReadKey();
- }
- }
复制代码
从卦中可以看到确实已经不再有 MyClassLibrary.dll 程序集了,但托管堆上还有 CustomAssemblyLoadContext 死对象,当后续GC触发时再回收,用windbg验证如下:- 0:014> !dumpobj /d 238e9c464c8
- Name: Example_1_6.CustomAssemblyLoadContext
- MethodTable: 00007ff88a06f098
- EEClass: 00007ff88a079008
- Tracked Type: false
- Size: 88(0x58) bytes
- File: D:\sources\woodpecker\Test\Example_1_6\bin\Debug\net8.0\Example_1_6.dll
- Fields:
- MT Field Offset Type VT Attr Value Name
- 00007ff889e870a0 4001116 30 System.IntPtr 1 instance 000002388042A8F0 _nativeAssemblyLoadContext
- 00007ff889dd5fa8 4001117 8 System.Object 0 instance 00000238e9c46520 _unloadLock
- 0000000000000000 4001118 10 0 instance 0000000000000000 _resolvingUnmanagedDll
- 0000000000000000 4001119 18 0 instance 0000000000000000 _resolving
- 0000000000000000 400111a 20 0 instance 0000000000000000 _unloading
- 00007ff889e8ec08 400111b 28 System.String 0 instance 0000023880006a30 _name
- 00007ff889e3a5f0 400111c 38 System.Int64 1 instance 0 _id
- 00007ff889f2f108 400111d 40 System.Int32 1 instance 1 _state
- 00007ff889ddd070 400111e 44 System.Boolean 1 instance 1 _isCollectible
- 00007ff88a0ed120 4001114 a00 ...Private.CoreLib]] 0 static 00000238e9c46550 s_allContexts
- 00007ff889e3a5f0 4001115 bc0 System.Int64 1 static 1 s_nextId
- 0000000000000000 400111f a08 ...yLoadEventHandler 0 static 0000000000000000 AssemblyLoad
- 0000000000000000 4001120 a10 ...solveEventHandler 0 static 0000000000000000 TypeResolve
- 0000000000000000 4001121 a18 ...solveEventHandler 0 static 0000000000000000 ResourceResolve
- 0000000000000000 4001122 a20 ...solveEventHandler 0 static 0000000000000000 AssemblyResolve
- 0000000000000000 4001123 a28 0 static 0000000000000000 s_asyncLocalCurrent
- 00007ff889e8ec08 4000001 48 System.String 0 instance 0000023880006938 _dependenciesPath
- 0:014> !gcroot 238e9c464c8
- Caching GC roots, this may take a while.
- Subsequent runs of this command will be faster.
- Found 0 unique roots.
复制代码 三:总结
有时候感叹 知识无涯人有涯,在 dump分析中不断的螺旋式提升,理论指导实践,实践反哺理论。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |