在.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();
}
}
正如您所看到的,泛型类型被定义为“可能是递归的”,其中“tail”类标记了递归的结束。为了确保GC.TotalMemoryUsage
没有作弊,我还打开了任务管理器。
到目前为止一切顺利。接下来我做的就是点燃这只野兽,当我在等待“内存不足”时...我注意到它 - 与我的期望相反 - 不随着时间的推移消耗更多的内存。事实上,它显示出内存消耗的轻微下降。
有人可以解释一下吗? GC是否实际收集了泛型类型?如果是这样的话......还有Reflection.Emit案件是垃圾收集的吗?
答案 0 :(得分:19)
回答你的第一个问题:
不收集类型的通用结构。
但是,如果构造C<string>
和C<object>
,CLR实际上只为方法生成代码一次;因为对字符串的引用和对象的引用保证是相同的大小,所以它可以安全地执行。这很聪明。但是,如果构造C<int>
和C<double>
,方法的代码将生成两次,每次构造一次。 (假设方法的代码当然是生成的;方法是按需进行的;这就是为什么它被称为jitting。)
要证明不收集泛型类型,而是创建泛型类型
class C<T> { public static readonly T Big = new T[10000]; }
C<object>
和C<string>
共享为方法生成的所有代码,但每个代码都有自己的静态字段,这些字段将永久存在。你构造的类型越多,用这些大数组填充的内存就越多。
现在你知道为什么这些类型无法收集;我们无法知道某人是否会在将来的任何时间尝试访问其中一个阵列的成员。由于我们不知道最后一个数组访问的时间,它们必须永远存在,因此包含它的类型也必须永远存在。
回答第二个问题:有没有办法制作动态发射的集合?
是。文档在这里:
答案 1 :(得分:0)
对代码共享或代码不共享不利,每次MakeGenericType尝试都会为元数据创建新的内部CLR类,这将消耗内存。类型对象是直接在CLR代码中创建的(不是在托管代码中),每个类型对象仅存在一个实例,因此您可以将它们进行比较以确保引用相等。 CLR本身拥有对其的引用,因此它们无法进行GC处理,但在我的测试中,我确认GC可以移动它们。
编辑:CLR保留的引用可能是弱引用,因此在挖掘RuntimeTypeHandle.cs源之后,我会看到
internal bool IsCollectible()
{
return RuntimeTypeHandle.IsCollectible(GetTypeHandleInternal());
}
考虑到埃里克·利珀特,这很可能是错误的