我用这样的方式引发了内存泄漏:
public class TestClass
{
}
public class ExecutionQueue
{
private IList<Action> actions = new List<Action>();
public void AddToQueue(TestClass testclass)
{
actions.Add(() =>
{
Console.WriteLine(testclass); //here we keep a reference to the TestClass instance
});
}
}
您可以看到,通过创建匿名方法,我们还保留了TestClass
实例的引用。
现在您可以构建一个如下所示的单元测试:
[MethodImpl(MethodImplOptions.NoInlining)]
private WeakReference DoCreateMemoryLeak(out ExecutionQueue executionQueue)
{
TestClass test = new TestClass();
executionQueue = new ExecutionQueue();
executionQueue.Foo(test);
return new WeakReference(test);
}
[Fact]
public void CanFindMemoryLeakCausedByCapturingInAnonymousMethod()
{
WeakReference weakReference = DoCreateMemoryLeak(out ExecutionQueue test);
test.Should().NotBeNull();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.WaitForFullGCComplete();
GC.Collect();
weakReference.IsAlive.Should().BeTrue("We wanted to create a memory leak");
}
哪个按预期工作。现在我想使用ClrMD检查 CLR并找出实例无法被 GC 收集的原因。为此,我遵循文章Exploring .NET managed heap with ClrMD中描述的方法。在第一次迭代中保持接近此代码(但在 ClrMD 2.0 中采用它)。它看起来像这样:
public static bool TryFindHeapReferences(IntPtr instanceAddress, out string feedback, bool suspend = true)
{
object instance = AddressTool.GetInstance<object>(instanceAddress);
string instanceTypeName = instance.GetType().FullName;
ClrObject clrInstance = default;
bool foundInstance = false;
HashSet<ulong> reportedRetentions = new HashSet<ulong>();
StringBuilder sb = new StringBuilder();
StringBuilder sbInner = new StringBuilder();
lock (inspectionLock)
{
try
{
using (DataTarget target = DataTarget.AttachToProcess(
Process.GetCurrentProcess().Id, suspend))
{
ClrRuntime runtime = target.ClrVersions.First().CreateRuntime();
var heap = runtime.Heap;
if (heap.CanWalkHeap)
{
foreach (var clrObject in heap.EnumerateObjects())
{
var typeName = clrObject.Type.Name;
if (typeName == instanceTypeName)
{
if (AddressTool.GetAddress(instance) == (IntPtr)clrObject.Address)
{
//we found the memory address of the instance to be inspected
clrInstance = clrObject;
foundInstance = true;
break;
}
}
}
if (foundInstance)
{
// Enumerate roots and try to find the current object
var stack = new Stack<ulong>();
var roots = heap.EnumerateRoots();
foreach (var root in roots)
{
stack.Clear();
stack.Push(root.Object.Address);
if (GetPathToObject(heap, clrInstance.Address, stack, new HashSet<ulong>()))
{
// Print retention path
var depth = 0;
object previousInstance = null;
foreach (var address in stack)
{
var t = heap.GetObjectType(address);
if (t == null)
{
continue;
}
sbInner.Append($"{new string('+', depth++)} {address} - {t.Name}");
sbInner.Append(Environment.NewLine);
}
sbInner.AppendLine();
string innerString = sbInner.ToString();
sbInner.Clear();
ulong hash = CalculateHash(innerString);
if (!reportedRetentions.Contains(hash))
{
sb.Append(innerString);
reportedRetentions.Add(hash);
}
}
}
}
}
}
}
catch (Exception e)
{
feedback = "Exception during inspection: " + e;
return false;
}
}
feedback = sb.ToString();
return foundInstance;
}
private static bool GetPathToObject(ClrHeap heap, ulong objectPointer, Stack<ulong> stack, HashSet<ulong> touchedObjects)
{
var currentObject = stack.Peek();
if (!touchedObjects.Add(currentObject))
{
return false;
}
if (currentObject == objectPointer)
{
return true;
}
var found = false;
var type = heap.GetObjectType(currentObject);
if (type != null)
{
ClrObject clrObject = heap.GetObject(currentObject);
foreach (var innerObject in clrObject.EnumerateReferences( false, false))
{
if (innerObject == 0 || touchedObjects.Contains(innerObject))
{
continue;
}
stack.Push(innerObject);
if (GetPathToObject(heap, objectPointer, stack, touchedObjects))
{
found = true;
continue;
}
stack.Pop();
};
}
return found;
}
取自此答案并稍作修改以修复内存泄漏的类 AddressTool 如下所示:
public class AddressTool
{
private static object mutualObject;
private static ObjectReinterpreter reinterpreter;
static AddressTool()
{
mutualObject = new object();
reinterpreter = new ObjectReinterpreter();
reinterpreter.AsObject = new ObjectWrapper();
}
public static IntPtr GetAddress(object obj)
{
lock (mutualObject)
{
reinterpreter.AsObject.Object = obj;
IntPtr address = reinterpreter.AsIntPtr.Value;
reinterpreter.AsObject.Object = null;
return address;
}
}
public static T GetInstance<T>(IntPtr address)
{
lock (mutualObject)
{
reinterpreter.AsIntPtr.Value = address;
T instance = (T)reinterpreter.AsObject.Object;
reinterpreter.AsObject.Object = null;
return instance;
}
}
[StructLayout(LayoutKind.Explicit)]
private struct ObjectReinterpreter
{
[FieldOffset(0)] public ObjectWrapper AsObject;
[FieldOffset(0)] public IntPtrWrapper AsIntPtr;
}
private class ObjectWrapper
{
public object Object;
}
private class IntPtrWrapper
{
public IntPtr Value;
}
}
hasing 函数是这个答案中的一个
static ulong CalculateHash(string read)
{
ulong hashedValue = 3074457345618258791ul;
for(int i=0; i<read.Length; i++)
{
hashedValue += read[i];
hashedValue *= 3074457345618258799ul;
}
return hashedValue;
}
现在我遇到的问题是,当我开始搜索heap.EnumerateRoots()
. heap.EnumerateObjects()
如果我使用非常慢的方式枚举所有 CLR 对象,我实际上可以找到我正在寻找的保留路径。我需要将TryFindHeapReferences
(上面给出的)的一部分更改为:
//...
if (foundInstance)
{
// Enumerate all CLR objects
foreach (var clrObject in heap.EnumerateObjects())
{
stack.Clear();
stack.Push(clrObject.Address);
if (GetPathToObject(heap, clrInstance.Address, stack, new HashSet<ulong>()))
//...
现在代码将正确找到保留路径,反馈字符串也将包含以下内容:
2241974294480 - MemoryInspection.Test.MemoryLeakInspectorTest+TestClass
+ 2241974294584 - MemoryInspection.Test.MemoryLeakInspectorTest+ExecutionQueue+<>c__DisplayClass1_0
++ 2241974294608 - System.Action
+++ 2241974294752 - System.Action[]
++++ 2241974294528 - System.Collections.Generic.List<System.Action>
+++++ 2241974294504 - MemoryInspection.Test.MemoryLeakInspectorTest+ExecutionQueue
所以现在我的问题是:如果我使用 迭代根,为什么找不到保留路径heap.EnumerateRoots()
?要么我的代码有错误,要么存在一些概念上的误解,并且ExecutionQueue
在探索根源时找不到实例,在这种情况下,我想了解原因。虽然heap.EnumerateObjects()
工作它会很慢。我假设一个有效的版本heap.EnumerateRoots()
无论如何都可以使用。否则我将不得不优化使用heap.EnumerateObjects()
. 但正如我所说,我真的很想了解为什么它不能使用根。