Why does process memory grow in .Net, but managed heap size does not change?

51 Views Asked by At

I am experiencing a weird memory leak in C#, which does not make any sense to me. Here is a simple example that recreates this problem. I have a simple class that can hold an array of bytes:

public class Record
{
    public byte[] Bytes { get; set; }
}

I also created a method that can display the total memory taken by the process in megabytes, and also the size of managed heap memory:

static void ShowMemory()
{
    var managedTotal = Math.Round(GC.GetTotalMemory(true) / (1024.0 * 1024.0), 2);
    var proc = System.Diagnostics.Process.GetCurrentProcess();
    var memoryTotal = Math.Round(proc.PrivateMemorySize64 / (1024.0 * 1024.0), 2);
    proc.Dispose();
    Console.WriteLine($"Total Memory: {memoryTotal} Mb, Managed Memory: {managedTotal} Mb");
}

Now this is the interesting part, here a code snippet that demonstrates the problem:

var list = new List<Record>();
// here I just add some records to the list and allocate Bytes array for each of the records.
for (int i = 0; i < 10000; i++)
{
    list.Add(new Record() { Bytes = new byte[100000]  });
}

// Now here I display the memory. The result I get is:
// Total Memory: 1071.84 Mb, Managed Memory: 954.56 Mb
ShowMemory();

// here I loop through the list again and reallocate the Bytes array
foreach (var record in list)
{
    record.Bytes = new byte[100000];
}

// Now here I display the memory again. The result I get is:
// Total Memory: 1994.46 Mb, Managed Memory: 954.68 Mb
ShowMemory();

Now as you can see the results displayed were:

Total Memory: 1071.84 Mb, Managed Memory: 954.56 Mb
Total Memory: 1994.46 Mb, Managed Memory: 954.68 Mb

I don't understand how is it possible that after I looped through the list again and reallocated the bytes, the size of total memory grew twice, while heap memory remained pretty much the same. Further more if I put this code in a separate method, after it finishes, garbage collector recalims only managed memory while total memory still is very big.

Can some one explain what is happening here? And perhaps how to avoid it? because of this issue, another process that I run, runs out of memory.

For reference I am using .Net 8 running on Ubuntu 22

1

There are 1 best solutions below

1
mtmk On

Not an answer to your question but more a suggestion that you should probably use the pooled memory utilities available in .NET 8.0. e.g.:

for (int i = 0; i < 10000; i++)
{
    list.Add(new Record() { Bytes = ArrayPool<byte>.Shared.Rent(100000)  });
}

ShowMemory();

foreach (var record in list)
{
    ArrayPool<byte>.Shared.Return(record.Bytes);
    record.Bytes = ArrayPool<byte>.Shared.Rent(100000);
}

On my machine this prints:

Total Memory: 1274.11 Mb, Managed Memory: 1250.87 Mb
Total Memory: 1274.38 Mb, Managed Memory: 1250.87 Mb

Of course you'd need to do a little more work than that since Rent() will return potentially a larger block. I'd suggest to look into Memory, Span, MemoryOwner etc.

There are also other helpful types in CommunityToolkit.HighPerformance library.

Edit: I think this is a good answer actually :-P

Can someone explain what is happening here?

In .NET memory management allocations and Garbage Collection work somewhat separately. When an object goes out of scope it doesn't mean GC will return it's memory and it might hang on to it until there is memory pressure.

And perhaps how to avoid it?

You need to used pooled memory so you can avoid GC as much as possible to have a more predictable memory performance. Example above shows how this could affect your memory usage.

You can also force GC which might help if you're looking for a quick solution to buy some time:

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

This is probably a good reference to start with: https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals