调试 .NET 5 中的程序集可卸载性
我有一个需要动态卸载的程序集。我已按照 https://learn.microsoft.com/ 中的步骤操作en-us/dotnet/standard/ assembly/unloadability 用于我的加载程序的开发以及使用 WinDbg 的调试。我有“内存泄漏”,因为我的卸载命令不起作用。我的问题是如何使卸载工作正常进行?
最初,我注意到使用卸载命令后内存并没有减少,因此我创建了一个指向程序集的弱引用字典。每次执行操作时,我都会动态加载和卸载程序集,这会在内存中创建程序集的多个副本。我可以使用 Dictionary
我没有犯他们在示例代码中犯的任何错误 - 我保留的唯一参考是弱引用。一旦我完成了程序集和程序集加载器,我什至将它们分配为空,尽管它们无论如何都会立即超出范围。我没有任何回调,或者据我所知没有跨界类型,因为我跨界进行的唯一通信是通过文件。大会正在顺利结束,没有出现任何问题。所以我启动了 WinDbg 并尝试按照他们的指示进行操作。一切都很顺利,直到我尝试获取 AssemblyLoader 的 GC 根:
0:000> !gcroot -all 0x00007ff7b943f9c8
Found 0 roots.
当我单步执行应用程序中的线程时,我没有看到任何可疑的东西,尽管老实说,我没有足够的直觉这个领域意义重大。输出如下。
0:021> ~*e !clrstack
OS Thread Id: 0x3510 (0)
Child SP IP Call Site
000000FEA5D7E6E0 00007ff8813111c4 [InlinedCallFrame: 000000fea5d7e6e0] Interop+User32.WaitMessage()
000000FEA5D7E6E0 00007ff7b8f38a15 [InlinedCallFrame: 000000fea5d7e6e0] Interop+User32.WaitMessage()
000000FEA5D7E6B0 00007ff7b8f38a15 ILStubClass.IL_STUB_PInvoke()
000000FEA5D7E770 00007ff813db015e System.Windows.Forms.Application+ComponentManager.Interop.Mso.IMsoComponentManager.FPushMessageLoop(UIntPtr, msoloop, Void*) [/_/src/System.Windows.Forms/src/System/Windows/Forms/Application.ComponentManager.cs @ 398]
000000FEA5D7E860 00007ff813db28f5 System.Windows.Forms.Application+ThreadContext.RunMessageLoopInner(msoloop, System.Windows.Forms.ApplicationContext) [/_/src/System.Windows.Forms/src/System/Windows/Forms/Application.ThreadContext.cs @ 1111]
000000FEA5D7E8F0 00007ff813db2568 System.Windows.Forms.Application+ThreadContext.RunMessageLoop(msoloop, System.Windows.Forms.ApplicationContext) [/_/src/System.Windows.Forms/src/System/Windows/Forms/Application.ThreadContext.cs @ 975]
000000FEA5D7E950 00007ff813a89c86 System.Windows.Forms.Application.Run(System.Windows.Forms.Form) [/_/src/System.Windows.Forms/src/System/Windows/Forms/Application.cs @ 1198]
000000FEA5D7E990 00007ff7b8bd9f4a ConfigTool.Program.Main() [C:\Users\isherman\Documents\Projects\twmp\FormsConfigTool\Program.cs @ 42]
<snipping out unmanaged threads>
OS Thread Id: 0xa2dc (6)
Child SP IP Call Site
000000FEA667FC30 00007ff88338d8c4 [DebuggerU2MCatchHandlerFrame: 000000fea667fc30]
OS Thread Id: 0xc5fc (7)
Child SP IP Call Site
<snip>
OS Thread Id: 0x900c (12)
Child SP IP Call Site
<snip>
OS Thread Id: 0x9b88 (16)
Child SP IP Call Site
<snip>
OS Thread Id: 0xa358 (18)
Child SP IP Call Site
OS Thread Id: 0x9a8c (19)
Child SP IP Call Site
<snip>
这是我用来执行和卸载程序集加载上下文的代码:
private static Dictionary<int, WeakReference> references;
private static int refCount =0;
[MethodImpl(MethodImplOptions.NoInlining)]
private static int ExecuteAndUnload(string assemblyPath,
XmlDocument doc, out WeakReference alcWeakRef)
{
references ??= new Dictionary<int, WeakReference>();
assemblyPath = Path.GetFullPath(assemblyPath);
var alc = new SequesteredTagBrowser(assemblyPath);
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
Debug.Assert(a.EntryPoint !=null);
alcWeakRef = new WeakReference(alc, true);
references.Add(refCount++, alcWeakRef);
//I pass some xml in as an argument to the Program.Main function
var args = new object[] {new[] {doc.OuterXml}};
a.EntryPoint.Invoke(null, args);
a = null;
alc.Unload();
alc = null;
GC.Collect();
return 0;
}
这是 SequesteredTagBrowser 类的代码:
public class SequesteredTagBrowser:AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public SequesteredTagBrowser(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;
}
}
I have an assembly which I need to unload dynamically. I have followed the steps in https://learn.microsoft.com/en-us/dotnet/standard/assembly/unloadability for both the development of my loader as well as the debugging using WinDbg. I have a "memory leak" because my unload command is not working. My question is how do I make my unload work?
Initially, I noticed that memory was not going down after using the unload command, so I created a dictionary of weak references which point at the assemblies. I dynamically load and unload the assembly every time I perform an operation, which is creating multiple copies of the assembly in memory. I can use the Dictionary<int, WeakReference> to see if the assemblies have been unloaded or not as they do in the guide above. They aren't getting released, no matter how long I wait, or how many times the garbage gets collected. This says to me that there's some outstanding reference, but for the life of me I can't find it.
I'm not making either of the mistakes they made in the example code- the only reference I hold onto is the weak reference. I even assign the assembly and the assembly loader to null once I'm done with them, though they go out of scope immediately anyway. I don't have any callbacks or, to the best of my knowledge Types across the border because the only communication I do across the boundary is via a file. The assembly is ending appropriately without issues. So I fired up WinDbg and tried to follow their instructions. Everything goes fine until I try to get the GC roots of the AssemblyLoader:
0:000> !gcroot -all 0x00007ff7b943f9c8
Found 0 roots.
And when I step through the threads in my application, I don't see anything that jumps out to me as suspicious, though honestly I don't have enough intuition in this area that that means much. The output is below.
0:021> ~*e !clrstack
OS Thread Id: 0x3510 (0)
Child SP IP Call Site
000000FEA5D7E6E0 00007ff8813111c4 [InlinedCallFrame: 000000fea5d7e6e0] Interop+User32.WaitMessage()
000000FEA5D7E6E0 00007ff7b8f38a15 [InlinedCallFrame: 000000fea5d7e6e0] Interop+User32.WaitMessage()
000000FEA5D7E6B0 00007ff7b8f38a15 ILStubClass.IL_STUB_PInvoke()
000000FEA5D7E770 00007ff813db015e System.Windows.Forms.Application+ComponentManager.Interop.Mso.IMsoComponentManager.FPushMessageLoop(UIntPtr, msoloop, Void*) [/_/src/System.Windows.Forms/src/System/Windows/Forms/Application.ComponentManager.cs @ 398]
000000FEA5D7E860 00007ff813db28f5 System.Windows.Forms.Application+ThreadContext.RunMessageLoopInner(msoloop, System.Windows.Forms.ApplicationContext) [/_/src/System.Windows.Forms/src/System/Windows/Forms/Application.ThreadContext.cs @ 1111]
000000FEA5D7E8F0 00007ff813db2568 System.Windows.Forms.Application+ThreadContext.RunMessageLoop(msoloop, System.Windows.Forms.ApplicationContext) [/_/src/System.Windows.Forms/src/System/Windows/Forms/Application.ThreadContext.cs @ 975]
000000FEA5D7E950 00007ff813a89c86 System.Windows.Forms.Application.Run(System.Windows.Forms.Form) [/_/src/System.Windows.Forms/src/System/Windows/Forms/Application.cs @ 1198]
000000FEA5D7E990 00007ff7b8bd9f4a ConfigTool.Program.Main() [C:\Users\isherman\Documents\Projects\twmp\FormsConfigTool\Program.cs @ 42]
<snipping out unmanaged threads>
OS Thread Id: 0xa2dc (6)
Child SP IP Call Site
000000FEA667FC30 00007ff88338d8c4 [DebuggerU2MCatchHandlerFrame: 000000fea667fc30]
OS Thread Id: 0xc5fc (7)
Child SP IP Call Site
<snip>
OS Thread Id: 0x900c (12)
Child SP IP Call Site
<snip>
OS Thread Id: 0x9b88 (16)
Child SP IP Call Site
<snip>
OS Thread Id: 0xa358 (18)
Child SP IP Call Site
OS Thread Id: 0x9a8c (19)
Child SP IP Call Site
<snip>
And here's the code that I use to execute and unload the Assembly Load Context:
private static Dictionary<int, WeakReference> references;
private static int refCount =0;
[MethodImpl(MethodImplOptions.NoInlining)]
private static int ExecuteAndUnload(string assemblyPath,
XmlDocument doc, out WeakReference alcWeakRef)
{
references ??= new Dictionary<int, WeakReference>();
assemblyPath = Path.GetFullPath(assemblyPath);
var alc = new SequesteredTagBrowser(assemblyPath);
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
Debug.Assert(a.EntryPoint !=null);
alcWeakRef = new WeakReference(alc, true);
references.Add(refCount++, alcWeakRef);
//I pass some xml in as an argument to the Program.Main function
var args = new object[] {new[] {doc.OuterXml}};
a.EntryPoint.Invoke(null, args);
a = null;
alc.Unload();
alc = null;
GC.Collect();
return 0;
}
And here's the code for the SequesteredTagBrowser class:
public class SequesteredTagBrowser:AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public SequesteredTagBrowser(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;
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论