Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are MakeGenericType / generic types garbage collected?

It is well known in .NET that types are not garbage collected, which means that if you're playing around with f.ex. Reflection.Emit, you have to be careful to unload AppDomains and so on... At least that's how I used to understand how things work.

That made me wonder if generic types are garbage collected, to be more precise: generics created with MakeGenericType, let's say... for example based on user input. :-)

So I constructed the following test case:

public interface IRecursiveClass
{
    int Calculate();
}

public class RecursiveClass1<T> : IRecursiveClass 
                                  where T : IRecursiveClass,new()
{
    public int Calculate()
    {
        return new T().Calculate() + 1;
    }
}
public class RecursiveClass2<T> : IRecursiveClass
                                  where T : IRecursiveClass,new()
{
    public int Calculate()
    {
        return new T().Calculate() + 2;
    }
}

public class TailClass : IRecursiveClass
{
    public int Calculate()
    {
        return 0;
    }
}

class RecursiveGenericsTest
{
    public static int CalculateFromUserInput(string str)
    {
        Type tail = typeof(TailClass);
        foreach (char c in str)
        {
            if (c == 0)
            {
                tail = typeof(RecursiveClass1<>).MakeGenericType(tail);
            }
            else
            {
                tail = typeof(RecursiveClass2<>).MakeGenericType(tail);
            }
        }
        IRecursiveClass cl = (IRecursiveClass)Activator.CreateInstance(tail);
        return cl.Calculate();
    }

    static long MemoryUsage
    {
        get
        {
            GC.Collect(GC.MaxGeneration);
            GC.WaitForFullGCComplete();
            return GC.GetTotalMemory(true);
        }
    }

    static void Main(string[] args)
    {
        long start = MemoryUsage;

        int total = 0;
        for (int i = 0; i < 1000000; ++i)
        {
            StringBuilder sb = new StringBuilder();
            int j = i;
            for (int k = 0; k < 20; ++k) // fix the recursion depth
            {
                if ((j & 1) == 1)
                {
                    sb.Append('1');
                }
                else
                {
                    sb.Append('0');
                }
                j >>= 1;
            }

            total += CalculateFromUserInput(sb.ToString());

            if ((i % 10000) == 0)
            {
                Console.WriteLine("Current memory usage @ {0}: {1}", 
                                  i, MemoryUsage - start);
            }
        }

        Console.WriteLine("Done and the total is {0}", total);
        Console.WriteLine("Current memory usage: {0}", MemoryUsage - start);

        Console.ReadLine();
    }
}

As you can see, the generic types are defined 'possibly recursive', with a 'tail' class that marks the end of the recursion. And to ensure that GC.TotalMemoryUsage isn't cheating, I also opened Task Manager.

So far so good. Next thing I did was fire this beast up and while I was waiting for an 'Out of memory' ... I noticed that it was - contrary to my expectations - not consuming more memory over time. In fact, it shows a slight drop in memory consumption in time.

Can someone please explain this? Are generic types actually collected by the GC? And if so... are there also Reflection.Emit cases that are garbage collected?

like image 855
atlaste Avatar asked Apr 18 '13 15:04

atlaste


1 Answers

To answer your first question:

Generic constructions of types are not collected.

However, if you construct C<string> and C<object>, the CLR actually generates the code for the methods only once; since reference to string and reference to object are guaranteed to be the same size, it can do so safely. It's pretty clever. If you construct C<int> and C<double> though, the code for the methods gets generated twice, once for each construction. (Assuming that the code for the methods is generated at all of course; methods are jitted on demand; that's why its called jitting.)

To demonstrate that generic types are not collected, instead create a generic type

class C<T> { public static readonly T Big = new T[10000]; }

C<object> and C<string> share any code generated for the methods, but each one gets its own static fields, and those fields will live forever. The more types you construct, the more memory will be filled up with those big arrays.

And now you know why those types cannot be collected; we have no way of knowing if someone is going to try to access a member of one of those arrays at any time in the future. Since we don't know when the last array access is going to be, they have to live forever, and therefore the type that contains it has to live forever too.


To answer your second question: Is there a way to make dynamically-emitted assemblies that are collected?

Yes. The documentation is here:

http://msdn.microsoft.com/en-us/library/dd554932.aspx

like image 119
Eric Lippert Avatar answered Nov 20 '22 09:11

Eric Lippert