C#内存分配、GC、死对象的内存诊断不匹配。

huangapple go评论92阅读模式
英文:

C# memory allocation, GC, memory diagnostic for dead objects not matching

问题

I have some level of understanding of how the memory is allocated and garbage collected in C#. But I'm missing some clarity on GC even after reading multiple articles and trying out a few test C# programs. To simplify the ask, I have created a dummy ASP Core Web API project with a DummyPojo class, which holds one int property, and a DummyClass, which creates a million objects of the DummyPojo. Even after running a forced GC, the dead objects are not collected. Can someone please throw some light on this.

public class DummyPojo
{
    public int MyInt { get; set; }
    ~DummyPojo()
    {
        //Debug.WriteLine("Finalizer");
    }
}
public class DummyClass
{
    public async Task TestObjectsMemory()
    {
        await Task.Delay(1000 * 10);
        CreatePerishableObjects();
        await Task.Delay(1000 * 10);
        GC.Collect();
    }
    private void CreatePerishableObjects()
    {
        for (int i = 0; i < 100000; i++)
        {
            DummyPojo obj = new() { MyInt = i };
        }

    }
}

Then in my program.cs file, I just called the method to start creating objects.

DummyClass dc = new DummyClass();
_ = dc.TestObjectsMemory();

I ran the program and took 3 memory snapshots with three 10-second waits each using Visual Studio's memory diagnostic tool, one before creating objects, the second one after creating objects, and the last one after triggering GC.
C#内存分配、GC、死对象的内存诊断不匹配。

My questions are:

  1. Why is memory usage not going down to the original size? I understand that GC runs only when needed, like memory crunch. In my case, I triggered the GC, and I see finalizers also getting called. I was expecting the GC to collect dead objects and compact the memory. As per my understanding, this has nothing to do with LOH as I did not use any large objects or arrays. Please look at the below images for each snapshot and GC numbers.
    C#内存分配、GC、死对象的内存诊断不匹配。
    C#内存分配、GC、死对象的内存诊断不匹配。
    C#内存分配、GC、死对象的内存诊断不匹配。
    C#内存分配、GC、死对象的内存诊断不匹配。
  • The 2nd snapshot shows 118MB (4MB jump from 114MB) is due to dead objects?
  • Why did it jump to 125MB in snapshot 3 even after the forced GC instead of going back to 114MB?
  1. While memory keeps increasing, the snapshots differences show in green color, which indicates memory decrease. Why would it show the opposite?

  2. When I opened snapshot #3, I see DummyPojo dead objects. Shouldn't they be collected by the forced GC?
    C#内存分配、GC、死对象的内存诊断不匹配。

英文:

I have some level of understanding of how the memory is allocated and garbage collected in C#. But I'm missing some clarity on GC even after reading multiple articles and trying out few test c# programs. To simplify the ask, I have created as dummy ASP core Web API project with a DummyPojo class which holds one int property and DummyClass which creates million object of the DummyPojo. Even after running forced GC the dead objects are not collected. Can someone please throw some light on this.

public class DummyPojo
{
    public int MyInt { get; set; }
    ~DummyPojo() {
        //Debug.WriteLine(&quot;Finalizer&quot;);
    }
}
public class DummyClass
{
    public async Task TestObjectsMemory()
    {
        await Task.Delay(1000 * 10);
        CreatePerishableObjects();
        await Task.Delay(1000 * 10);
        GC.Collect();
    }
    private void CreatePerishableObjects()
    {
        for (int i = 0; i &lt; 100000; i++)
        {
            DummyPojo obj = new() { MyInt = i };
        }

    }
}

Then in my program.cs file I just called the method to start creating objects.

DummyClass dc = new DummyClass();
_ = dc.TestObjectsMemory();

I ran the program and took 3 memory snapshots with three 10 seconds wait each using visual studio's memory diagnostic tool, one at before creating objects, second one after creating object and the last one is after triggering GC.
C#内存分配、GC、死对象的内存诊断不匹配。

My questions are:

  1. Why is memory usage not going down to the original size? I understand that GC runs only when it needed only like memory crunch. In my case I triggered the GC and I see finalizers also getting called. I was expecting the GC to collect dead objects and compact the memory. As per my understanding this is nothing to do with LOH as I did not use any large objects or arrays. Please look at the below images for each snapshot and GC numbers.
    C#内存分配、GC、死对象的内存诊断不匹配。
    C#内存分配、GC、死对象的内存诊断不匹配。
    C#内存分配、GC、死对象的内存诊断不匹配。
    C#内存分配、GC、死对象的内存诊断不匹配。

    • The 2nd snapshot shows 118MB (4MB jump from 114MB) is due to dead objects?
    • why it jumped to 125MB in snapshot3 even after the forced GC instead of going back to 114MB.
  2. While memory keep increasing, the snapshots differences shows in green color which indicates memory decrease. why would it show the opposite?

  3. when I opened the snapshot#3 I see DummyPojo dead objects. Shouldn't it be collected by the forced GC?
    C#内存分配、GC、死对象的内存诊断不匹配。

答案1

得分: 3

我对VS内存分析器不是特别熟悉,所以其中一些内容可能是猜测的。

首先,重要的是要记住要监视的内存类型。图表上显示的是进程内存,我会认为这意味着由进程从操作系统分配的所有内存。当您分配更多对象时,这些内存显然需要增加,但它不会直接与托管堆的大小相关,即实际使用的内存。垃圾收集器可以过度分配内存以减少它从操作系统请求内存的次数,并且只有在确定一段时间内不需要再次使用内存时,它才会将内存释放回操作系统。我主要使用dotMemory,它的图表分为每一代 + LOH + 非托管内存,这样更容易看到垃圾收集的影响。

与一些评论相反,GC.Collect应该会阻塞,直到收集完成:
> 使用此方法尝试回收所有不可访问的内存。它对所有代执行阻塞垃圾回收。

但是,您声明了一个终结器,这意味着所有对象在被收集时都将被放入终结器队列。因此,即使在被“收集”后,它们仍然会保持部分存活状态。终结器的目的是确保非托管资源被回收,而这应该是罕见的情况。我强烈建议在进行任何GC调查时移除终结器。GC本身已经足够复杂,不需要涉及终结器队列和所有相关的复杂性。

还请注意,只要行为不发生更改,编译器都可以进行任何优化。由于内存分配被视为不涉及“行为”,所以编译器可以选择移除整个循环。

英文:

I'm not super familiar with the VS memory profiler, so some of this will be conjecture.

First of all, it is important to keep in mind what type of memory you are monitoring. The graph says Process Memory, which I would assume means all the memory allocated from the OS by the process. This memory obviously needs to increase when you allocate more objects, but it will not directly correlate with the size of the managed heap, i.e. the actual memory you are using. The GC can over allocate memory to reduce the number of times it needs to request memory from the OS, and it will not release memory back to the OS unless it is sure it will not need it again for a while. I have mostly used dotMemory, and its graph is split into each generation + LOH + unmanaged, making the effect of GCs easier to see.

Contrary to some comments GC.Collect should block until the collection is complete:
> Use this method to try to reclaim all memory that is inaccessible. It performs a blocking garbage collection of all generations.

But you are declaring a finalizer, this will mean that all objects will be put on the finalizer queue when they are collected. Because of this they will remain kind of half alive even after they are "collected". Finalizers are meant to ensure unmanaged resources are collected, and this should be rare. I would really recommend removing the finalizer when doing any kind of investigation into the GC. The GC is difficult enough to understand without involving the finalizer queue and all the complexity it entails.

Also note that the compiler is allowed to do any optimization as long as the behavior is unchanged. Since memory allocations are not seen as "behavior", it would be allowed to just remove the entire loop.

答案2

得分: 1

任何带有终结器的对象(未明确禁用其终结器的对象)将在与GC.Collect()调用异步进行终结。要等待终结器完成并GC回收所有内存,您必须执行完整的步骤:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

确保几乎从不实际调用终结器非常重要;它们是避免保持不受管理的内容的最终备份措施,不是您轻易使用的东西。大多数情况下,您希望将终结器保持尽可能简单和小(请注意.NET本身实际上尽量避免使用终结器,而是将它们隐藏在对象后面,例如SafeHandles;当然,一旦这些对象被处理,它们的终结器将被“注销”)。

当然,仅仅因为对象被回收并不一定意味着内存已被释放供其他进程使用。不会为释放少于一兆字节的内存而执行这种昂贵的操作。您可以使用GC.GetTotalMemory跟踪当前正在使用的GC内存量。总的来说,不要期望像在C语言中处理非托管内存一样处理托管内存。

英文:

Any object with a finalizer (that didn't explicitly disable its finalization) will be finalized asynchronously with respect to the GC.Collect() call. To wait for the finalizers to finish and the GC to reclaim all the memory, you have to do the full dance:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

It's rather important to ensure finalizers are almost never actually called; they're a final backup measure to avoid holding on to unmanaged stuff, not something you should use lightly. Most of the time, you want to keep the finalizers as simple and small as possible (note how .NET itself actually avoid using finalizers as much as possible, and instead hides them behind objects such as SafeHandles; and of course, once those are disposed, their finalizers are "unregistered").

Of course, just because the objects are collected doesn't necessarily mean the memory has been freed for use by other processes. You're not going to see that kind of expensive operation just for freeing less than a megabyte of memory. You can keep track of how much GC memory is currently in use with GC.GetTotalMemory. In general, don't expect to work with managed memory the way you would normally work with unmanaged memory in something like C.

huangapple
  • 本文由 发表于 2023年6月26日 05:53:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/76552560.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定