Search code examples
c#memorygarbage-collection

How to free up all used RAM in a C# process?


Yet another question about Garbarge Collector. There are many questions and few clear answers about the behavior of the garbage collector in the .net environment. My question is simple and I hope that if someone can answer it clearly that should help a lot of people who will pass by here.

So, here is a simple code (.net core 6.0.101)

var arraySize = int.MaxValue / 4;
var rnd = new Random();
var arrays = new List<byte[]>();

// Initial memory footprint
Console.WriteLine($"[0] Memory: {GC.GetTotalMemory(false) / 1000000:N0} MB");
for (int i = 1; i < 5; i++)
{
    var array = new byte[arraySize];
    // fill the array to be sure that .net doesn't do obscure memory optimizations
    rnd.NextBytes(array);
    arrays.Add(array);
}

Console.WriteLine($"[Before GC] Memory: {GC.GetTotalMemory(true) / 1000000:N0} MB");

arrays.Clear();
arrays = null;

GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true);
GC.WaitForPendingFinalizers();
Console.WriteLine($"[After GC] Memory: {GC.GetTotalMemory(true) / 1000000:N0} MB");

Console.WriteLine("Press a key to exit");
Console.ReadLine();

And now the result.

C:\testsgc>dotnet run
[0] Memory: 0 MB
[Before GC] Memory: 2 147 MB
[After GC] Memory: 536 MB
Press a key to exit

Process memory footprint (Task Manager):

  • Initial memory : 5,6 MB
  • After filling variables : 2 056,5 MB
  • After Garbage Collection : 520,1 MB

At startup, the process takes 5,6 MB or RAM. One array of bytes takes around 500 MB of ram. The list is cleared and assigned to null (not required).

So the question is, how to free up the last array of bytes?

Only a precise and verifiable answer will be marked as resolved.


Solution

  • One of your sub-arrays are still kept in memory when you run the program from a Debug build.

    Let's adjust your program just slightly, add a Console.WriteLine inside your loop like this:

    for (int i = 1; i < 5; i++)
    {
        var array = new byte[arraySize];
        Console.WriteLine($"[In loop] Memory: {GC.GetTotalMemory(true) / 1000000:N0} MB");
    

    the output of your program now becomes:

    > dotnet run
    [0] Memory: 0 MB
    [In loop] Memory: 536 MB
    [In loop] Memory: 1 073 MB
    [In loop] Memory: 1 610 MB
    [In loop] Memory: 2 147 MB
    [Before GC] Memory: 2 147 MB
    [After GC] Memory: 536 MB
    

    As you can see, the first report from the loop matches in size. So most likely one of the arrays have survived garbage collection.

    A typical way to "fix this" is to add another call to GC.Collect after a call to GC.WaitForPendingFinalizers, but this makes no difference here.

    Variables that are no longer used are in fact eligible for garbage collection, but only if you do not run using a debugger and have compiled with optimizations enabled, aka Release-build.

    So if we try to just run the program from a Release-build instead:

    > dotnet run --configuration Release
    [0] Memory: 0 MB
    [Before GC] Memory: 2 147 MB
    [After GC] Memory: 0 MB
    

    While I cannot explain why one of the sub-arrays was still alive and kicking, it seems to be related. I tried setting array to null inside the loop, after adding it to the arrays list, but this did not change anything.