12

在 .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 案例?

4

2 回答 2

20

要回答您的第一个问题:

不收集类型的通用结构。

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

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

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

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

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


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

是的。文档在这里:

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

于 2013-04-18T16:18:07.723 回答
0

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

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

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

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

于 2019-03-04T19:52:51.720 回答