0

我用这样的方式引发了内存泄漏:

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(). 但正如我所说,我真的很想了解为什么它不能使用根。

4

0 回答 0