【问题标题】:Are MakeGenericType / generic types garbage collected?是否会收集 MakeGenericType / 泛型类型垃圾?
【发布时间】:2013-04-11 19:57:55
【问题描述】:

众所周知,在 .NET 中类型不会被垃圾收集,这意味着如果您正在使用 f.ex。 Reflection.Emit,你必须小心卸载 AppDomains 等等......至少我以前是这样理解事情是如何工作的。

这让我想知道泛型类型是否被垃圾收集,更准确地说:使用MakeGenericType 创建的泛型,比方说...例如基于用户输入。 :-)

于是我构建了以下测试用例:

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();
    }
}

如您所见,泛型类型被定义为“可能递归”,并带有一个标记递归结束的“尾”类。为了确保GC.TotalMemoryUsage 没有作弊,我还打开了任务管理器。

到目前为止一切顺利。接下来我做的就是启动这个野兽,而我正在等待“内存不足”......我注意到它 - 与我的预期相反 - 没有随着时间的推移消耗更多的内存。事实上,它在时间上显示了内存消耗的轻微下降。

有人可以解释一下吗?泛型类型实际上是由 GC 收集的吗?如果是这样......是否还有 Reflection.Emit 垃圾收集的案例?

【问题讨论】:

标签: c# .net generics types garbage-collection


【解决方案1】:

与代码共享或代码不共享无关,每次 MakeGenericType 尝试都会为元数据创建新的内部 CLR 类,这将消耗内存。类型对象直接在 CLR 代码中创建(而不是在托管代码中),每个类型对象只存在一个实例,因此您可以比较它们的引用相等性。 CLR 本身持有对它的引用,因此它们不能被 GC'ed 但在我的测试中我确认 GC 可以移动它们。

编辑: CLR 持有的引用可能是弱引用,所以在挖掘 RuntimeTypeHandle.cs 源代码后我看到了

internal bool IsCollectible()
{
    return RuntimeTypeHandle.IsCollectible(GetTypeHandleInternal());
}

考虑到 Eric Lippert,这很可能是错误的

【讨论】:

    【解决方案2】:

    回答你的第一个问题:

    不收集类型的通用构造。

    但是,如果您构造C&lt;string&gt;C&lt;object&gt;,CLR 实际上只为方法生成代码一次;由于对字符串的引用和对对象的引用保证大小相同,因此可以安全地这样做。这很聪明。但是,如果您构造 C&lt;int&gt;C&lt;double&gt;,则方法的代码会生成两次,每次构造一次。 (假设方法的代码当然是生成的;方法是按需jitting的;这就是为什么它被称为jitting的原因。)

    为了证明不收集泛型类型,而是创建一个泛型类型

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

    C&lt;object&gt;C&lt;string&gt; 共享为方法生成的任何代码,但每个都有自己的静态字段,这些字段将永远存在。你构造的类型越多,那些大数组就会填满更多的内存。

    现在您知道为什么无法收集这些类型了;我们无法知道将来是否有人会尝试访问其中一个数组的成员。由于我们不知道最后一次访问数组的时间,它们必须永远存在,因此包含它的类型也必须永远存在。


    回答您的第二个问题:有没有办法制作收集的动态发出的程序集?

    是的。文档在这里:

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

    【讨论】:

    • @ErikLippert 哇,埃里克回答得很好,谢谢!我想出了关于静态的事情,这就是我没有在类中添加静态的原因。我仍在考虑“实际上只为方法生成一次代码[..]”这一行。这是否也意味着如果您有两个没有静态的引用类型泛型类,那么内存中只存在一个“类型”? (例如,因为一个类型只是代码并且没有内存,它只会被创建一次)但这也意味着 JIT 优化边界(如果 T 可以是 Foo 和 Bar 并且只有一个代码实例,则内联将是不可能的)?跨度>
    • 如果像string GetTypeName&lt;T&gt;(T param) {return typeof(T).ToString();} 这样的方法的代码副本将被所有类类型T 共享,那么代码如何知道它被调用的是什么类型?这是否以某种方式作为隐藏参数传递?
    • @supercat:有一些机制可以让当前正在执行的构造被秘密传递,但我不知道它是什么。您必须咨询抖动方面的专家。
    • @supercat 是的,使用了隐藏参数。有关更多详细信息,请参阅CLR Generics and code sharing(虽然它是从 2004 年开始的,所以从那时起细节可能会有所变化)。
    • @StefandeBruijn:为 x86 编译,SomeStruct&lt;int&gt;SomeStruct&lt;StringBuilder&gt; 都是值类型,它们都占用四个字节。如果SomeStruct&lt;someClass&gt;SomeStruct&lt;someClass&gt;,则具有T 类型变量的代码必须将T 识别为包含GC 根,但如果它是SomeStruct&lt;int&gt;,则不能识别。也许即使代码是共享的,GC 也可以处理,但如果他们使用不同的代码,GC 元数据似乎会更简单。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-20
    • 2012-07-26
    • 1970-01-01
    • 2016-02-29
    • 1970-01-01
    相关资源
    最近更新 更多