8

I have tried to come up with the simplest code to reproduce what I am seeing. The full program is below, but I will describe it here. Suppose I have class named ListData that just has some properties. Then suppose I have a MyList class that has a member List<ListData> m_list. Suppose m_list gets initialized in the MyList constructor.

In the main method I simply create one of these MyList objects, add a few ListData to it, then let it go out of scope. I take a snapshot in dotMemory after the ListData have been added, then I take another snapshot after the MyList object goes out of scope.

In dotMemory I can see that the MyList object has been reclaimed as expected. I also see that the two ListData objects that I created also got reclaimed as expected.

What I do not understand is why is there a ListData[] that survived? Here is a screen shot of this: Screenshot of this in dotMemory

I open survived objects on the newest snapshot for the ListData[] then I view Key Retention Paths, this is what I see.

Key Retention Paths

I am new to .NET memory management and I created this sample app to help me explore it. I downloaded the trial version of JetBrains dotMemory version 4.3. I am using Visual Studio 2013 Professional. I have to learn memory management so I can fix the memory issues we have at work.

Here is the full program that can be used to reproduce this. It is just a quick and dirty app but it will get the thing I am asking about if you profile it.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1
{

    class ListData
    {
        public ListData(string name, int n) { Name = name; Num = n; }
        public string Name { get; private set; }
        public int Num { get; private set; }
    }

    class MyList
    {

        public MyList()
        {
            m = new List<ListData>();
        }

        public void AddString(ListData d)
        {
            m.Add(d);
        }

        private List<ListData> m;

    }


    class Program
    {
        static void Main(string[] args)
        {
            {
                MyList l = new MyList();
                bool bRunning = true;
                while (bRunning)
                {
                    Console.WriteLine("a or q");
                    string input = Console.ReadLine();
                    switch (input)
                    {
                        case "a":
                            {
                                Console.WriteLine("Name: ");
                                string strName = Console.ReadLine();
                                Console.WriteLine("Num: ");
                                string strNum = Console.ReadLine();
                                l.AddString(new ListData(strName, Convert.ToInt32(strNum)));
                                break;
                            }
                        case "q":
                            {
                                bRunning = false;
                                break;
                            }
                    }
                }
            }

            Console.WriteLine("good bye");
            Console.ReadLine();
        }
    }
}

Steps:

  1. Build above code in release.
  2. In dotMemory, select to profile a standalone app.
  3. Browse to the release exe.
  4. Select the option to start collecting allocation data immediately.
  5. Click Run.
  6. Take a snapshot immediately and name it "before". This is before any ListData have been added.
  7. In the app, type a and add two ListData.
  8. In dotMemory, take another snapshot and name it "added 2" because we added two ListData.
  9. In the app, type q to quit (the MyList will go out of scope). Before typing Enter again to exit the app, go take another snapshot in dotMemory. Name it "out of scope".
  10. In the app, type Enter to close the app.
  11. In dotMemory, compare the "added 2" and the "out of scope" snapshots. Group by namespace. You will see the ListData[] that I am referring to.

Notice that the MyList and the two ListData objects did get garbage collected but the ListData[] did not. Why is there a ListData[] hanging around? How can I make it get garbage collected?

cchampion
  • 7,607
  • 11
  • 41
  • 51

1 Answers1

3

Why is there a ListData[] hanging around? How can I make it get garbage collected?

If you look at the "Creation Stack Trace" inside dotMemory, you'll see:

dotMemory StackTrace

This shows you that the empty ListData[0] instance was created via the static constructor of List<T>. If you look at the source, you'll see this:

static readonly T[]  _emptyArray = new T[0];    

List<T> initializes a default, empty array to optimize the avoid such an allocation each time you create a new List<T>. This is the default constructor:

public List() 
{
    _items = _emptyArray;
}

Only one you use List<T>.Add, will it resize the array.

static members are referenced from the "High Frequency Heap", which are created once for each AppDomain in your application. The pinned object[] you're seeing is actually the location where all static instances are stored.

Since the instance is static, it will remain in memory for the lifetime of your application.

Community
  • 1
  • 1
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • Yuval, thank you for the reply. I am looking over your answer to understand and I will let you know if I have questions. – cchampion Jul 06 '15 at 12:19
  • Ok to make sure I understand correctly allow me to explain in my own words and please correct me if I am wrong. What I am seeing is that static _emptyArray being left behind. Since _emptyArray is static it will always be a root for the empty array and therefore it will never get garbage collected. – cchampion Jul 07 '15 at 01:17
  • To help convince myself that the static empty array is what I am seeing I decided to create my own MyList class that does the same thing. And indeed, when I init with a static empty array I can see it get left over in dotMem. When I take the static off it no longer gets left behind. – cchampion Jul 07 '15 at 01:24
  • My only remaining question (assuming my other comments are true) is: what makes up the 12 bytes of this ListData[] (actually the static _emptyArray)? I suppose it is the size of the reference plus the overhead required by the CLR to maintain the object? – cchampion Jul 07 '15 at 01:31
  • @cchampion - In a x86 process - 4 bytes object word header, 4 bytes for method table pointer, 4 bytes for the length of the array. 24 bytes on x64 – Yuval Itzchakov Jul 07 '15 at 04:54
  • @cchampion what version of dotMemory you are using? Since version 4.1 dotMemory should show name of static fields like this one https://www.jetbrains.com/dotmemory/whatsnew/ – Roman Belov Jul 11 '15 at 19:45
  • @RomanBelov hmm yeah according to that it should tell the name of the variable that is a static root. Is that what you're referring to? The screen shots I posted here are 4.3. It is the trial but that shouldn't matter. Not sure why it didn't show the name of the root. – cchampion Jul 12 '15 at 11:07