反射.Emit动态类型内存膨胀问题

12
使用C# 3.5,我正在尝试使用反射发射在运行时生成动态类型。我使用了Microsoft的Dynamic Query Library示例创建了一个类生成器。一切正常,但我的问题是,100个生成的类型会使内存使用量增加25MB左右。这是完全不可接受的内存配置文件,因为最终我希望支持在内存中生成数十万个类型。

内存分析显示,内存显然由各种System.Reflection.Emit类型和方法占用,但我无法弄清楚原因。我没有找到其他人谈论这个问题,所以我希望这个社区中的某些人知道我做错了什么,或者这是否是预期行为。

以下是虚构的示例:

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;

namespace SmallRelfectExample
{
    class Program
    {
        static void Main(string[] args)
        {
            int typeCount = 100;
            int propCount = 100;
            Random rand = new Random();
            Type dynType = null;
            SlimClassFactory scf = new SlimClassFactory();
            for (int i = 0; i < typeCount; i++)
            {
                List<DynamicProperty> dpl = new List<DynamicProperty>(propCount);
                for (int j = 0; j < propCount; j++)
                {
                    dpl.Add(new DynamicProperty("Key" + rand.Next().ToString(), typeof(String)));
                }
                dynType = scf.CreateDynamicClass(dpl.ToArray(), i);
                //Optionally do something with the type here
            }
            Console.WriteLine("SmallRelfectExample: {0} Types generated.", typeCount);
            Console.ReadLine();
        }
    }
    public class SlimClassFactory
    {
        private readonly ModuleBuilder module;
        public SlimClassFactory()
        {
            AssemblyName name = new AssemblyName("DynamicClasses");
            AssemblyBuilder assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run);
            module = assembly.DefineDynamicModule("Module");

        }
        public Type CreateDynamicClass(DynamicProperty[] properties, int Id)
        {
            string typeName = "DynamicClass" + Id.ToString();
            TypeBuilder tb = module.DefineType(typeName, TypeAttributes.Class |
                TypeAttributes.Public, typeof(DynamicClass));
            FieldInfo[] fields = GenerateProperties(tb, properties);
            GenerateEquals(tb, fields);
            GenerateGetHashCode(tb, fields);
            Type result = tb.CreateType();
            return result;
        }
        static FieldInfo[] GenerateProperties(TypeBuilder tb, DynamicProperty[] properties)
        {
            FieldInfo[] fields = new FieldBuilder[properties.Length];
            for (int i = 0; i < properties.Length; i++)
            {
                DynamicProperty dp = properties[i];
                FieldBuilder fb = tb.DefineField("_" + dp.Name, dp.Type, FieldAttributes.Private);
                PropertyBuilder pb = tb.DefineProperty(dp.Name, PropertyAttributes.HasDefault, dp.Type, null);
                MethodBuilder mbGet = tb.DefineMethod("get_" + dp.Name,
                    MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
                    dp.Type, Type.EmptyTypes);
                ILGenerator genGet = mbGet.GetILGenerator();
                genGet.Emit(OpCodes.Ldarg_0);
                genGet.Emit(OpCodes.Ldfld, fb);
                genGet.Emit(OpCodes.Ret);
                MethodBuilder mbSet = tb.DefineMethod("set_" + dp.Name,
                    MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
                    null, new Type[] { dp.Type });
                ILGenerator genSet = mbSet.GetILGenerator();
                genSet.Emit(OpCodes.Ldarg_0);
                genSet.Emit(OpCodes.Ldarg_1);
                genSet.Emit(OpCodes.Stfld, fb);
                genSet.Emit(OpCodes.Ret);
                pb.SetGetMethod(mbGet);
                pb.SetSetMethod(mbSet);
                fields[i] = fb;
            }
            return fields;
        }
        static void GenerateEquals(TypeBuilder tb, FieldInfo[] fields)
        {
            MethodBuilder mb = tb.DefineMethod("Equals",
                MethodAttributes.Public | MethodAttributes.ReuseSlot |
                MethodAttributes.Virtual | MethodAttributes.HideBySig,
                typeof(bool), new Type[] { typeof(object) });
            ILGenerator gen = mb.GetILGenerator();
            LocalBuilder other = gen.DeclareLocal(tb);
            Label next = gen.DefineLabel();
            gen.Emit(OpCodes.Ldarg_1);
            gen.Emit(OpCodes.Isinst, tb);
            gen.Emit(OpCodes.Stloc, other);
            gen.Emit(OpCodes.Ldloc, other);
            gen.Emit(OpCodes.Brtrue_S, next);
            gen.Emit(OpCodes.Ldc_I4_0);
            gen.Emit(OpCodes.Ret);
            gen.MarkLabel(next);
            foreach (FieldInfo field in fields)
            {
                Type ft = field.FieldType;
                Type ct = typeof(EqualityComparer<>).MakeGenericType(ft);
                next = gen.DefineLabel();
                gen.EmitCall(OpCodes.Call, ct.GetMethod("get_Default"), null);
                gen.Emit(OpCodes.Ldarg_0);
                gen.Emit(OpCodes.Ldfld, field);
                gen.Emit(OpCodes.Ldloc, other);
                gen.Emit(OpCodes.Ldfld, field);
                gen.EmitCall(OpCodes.Callvirt, ct.GetMethod("Equals", new Type[] { ft, ft }), null);
                gen.Emit(OpCodes.Brtrue_S, next);
                gen.Emit(OpCodes.Ldc_I4_0);
                gen.Emit(OpCodes.Ret);
                gen.MarkLabel(next);
            }
            gen.Emit(OpCodes.Ldc_I4_1);
            gen.Emit(OpCodes.Ret);
        }
        static void GenerateGetHashCode(TypeBuilder tb, FieldInfo[] fields)
        {
            MethodBuilder mb = tb.DefineMethod("GetHashCode",
                MethodAttributes.Public | MethodAttributes.ReuseSlot |
                MethodAttributes.Virtual | MethodAttributes.HideBySig,
                typeof(int), Type.EmptyTypes);
            ILGenerator gen = mb.GetILGenerator();
            gen.Emit(OpCodes.Ldc_I4_0);
            foreach (FieldInfo field in fields)
            {
                Type ft = field.FieldType;
                Type ct = typeof(EqualityComparer<>).MakeGenericType(ft);
                gen.EmitCall(OpCodes.Call, ct.GetMethod("get_Default"), null);
                gen.Emit(OpCodes.Ldarg_0);
                gen.Emit(OpCodes.Ldfld, field);
                gen.EmitCall(OpCodes.Callvirt, ct.GetMethod("GetHashCode", new Type[] { ft }), null);
                gen.Emit(OpCodes.Xor);
            }
            gen.Emit(OpCodes.Ret);
        }
    }
    public abstract class DynamicClass
    {
        public override string ToString()
        {
            PropertyInfo[] props = GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
            StringBuilder sb = new StringBuilder();
            sb.Append("{");
            for (int i = 0; i < props.Length; i++)
            {
                if (i > 0) sb.Append(", ");
                sb.Append(props[i].Name);
                sb.Append("=");
                sb.Append(props[i].GetValue(this, null));
            }
            sb.Append("}");
            return sb.ToString();
        }
    }
    public class DynamicProperty
    {
        private readonly string name;
        private readonly Type type;

        public DynamicProperty(string name, Type type)
        {
            if (name == null) throw new ArgumentNullException("name");
            if (type == null) throw new ArgumentNullException("type");
            this.name = name;
            this.type = type;
        }

        public string Name
        {
            get { return name; }
        }

        public Type Type
        {
            get { return type; }
        }
    }
}

FYI,这在.NET 4.0中实际上更糟糕。 - Firestrand
您可以使用CCI或Mono.Cecil来获得更好的性能。 - leppie
@leppie 谢谢你的 CCI 建议。我正在研究使用它。我不太喜欢以我现在这种方式使用反射来解决问题。 - Firestrand
@leppie FYI,CCI目前不支持将代码发射到动态程序集中。我想我可以进行持久化、加载和变异,但这对我所在的环境有一些负面影响。 - Firestrand
我认为内部动态程序集也是如此,但这也是我的一个关注点。 - leppie
4个回答

5

很遗憾,在ModuleBuilder中有一个静态字段持有内存,这将永远不会被GC回收。我现在想不起来它包含什么字段和内容了,但是可以从WinDbg中的SOS中看到。

好消息是,.NET 4支持可GC的动态程序集:)


1
@leppie 原来程序集中的每个动态类型都保留了对 ModuleBuilder 和随后使用的 TypeBuilder 的引用,用于解决类型名称冲突。如果您确保没有名称冲突,则可以清除 TypeBuilder 列表并释放所有内存,而不会产生任何负面影响。(至少目前是这样) - Firestrand
1
@leppie 我怎样可以在 ModuleBuilder 的私有变量 m__TypeBuilderList 上调用 "Clear" 方法? - Renatto Machado

4

看起来这是System.Reflection.Emit中的一个实际内存泄漏问题。 下面是新的解决方案,我通过使用反射和手动处理dispose来清除了大部分内存使用。我使用扩展方法在某些类型上添加了Dispose方法。这并不能清理所有东西,但代码展示了如何做到这一点。我正在采用另一种方式来获得所需的结果。对于那些想要了解如何操作的人,这里有代码。

在原始示例中,在生成类型后,您将调用tb.Dispose()来释放TypeBuilder实例。以下是扩展方法,请记住这并不能清理所有东西,但可以释放大部分内存。此代码也未经过速度优化。有加速使用的反射的方法,这只是一个示例。 风险自负。

  public static void Dispose(this TypeBuilder tb)
        {
            if (tb == null)
                return;
            Type tbType = typeof(TypeBuilder);
            FieldInfo tbMbList = tbType.GetField("m_listMethods", BindingFlags.Instance | BindingFlags.NonPublic); //List<MethodBuilder>
            FieldInfo tbDecType = tbType.GetField("m_DeclaringType", BindingFlags.Instance | BindingFlags.NonPublic);//TypeBuilder
            FieldInfo tbGenType = tbType.GetField("m_genTypeDef", BindingFlags.Instance | BindingFlags.NonPublic);//TypeBuilder
            FieldInfo tbDeclMeth = tbType.GetField("m_declMeth", BindingFlags.Instance | BindingFlags.NonPublic);//MethodBuilder
            FieldInfo tbMbCurMeth = tbType.GetField("m_currentMethod", BindingFlags.Instance | BindingFlags.NonPublic);//MethodBuilder
            FieldInfo tbMod = tbType.GetField("m_module", BindingFlags.Instance | BindingFlags.NonPublic);//ModuleBuilder
            FieldInfo tbGenTypeParArr = tbType.GetField("m_inst", BindingFlags.Instance | BindingFlags.NonPublic); //GenericTypeParameterBuilder[] 

            TypeBuilder tempDecType = tbDecType.GetValue(tb) as TypeBuilder;
            tempDecType.Dispose();
            tbDecType.SetValue(tb, null);
            tempDecType = tbGenType.GetValue(tb) as TypeBuilder;
            tempDecType.Dispose();
            tbDecType.SetValue(tb, null);

            MethodBuilder tempMeth = tbDeclMeth.GetValue(tb) as MethodBuilder;
            tempMeth.Dispose();
            tbDeclMeth.SetValue(tb,null);
            tempMeth = tbMbCurMeth.GetValue(tb) as MethodBuilder;
            tempMeth.Dispose();
            tbMbCurMeth.SetValue(tb, null);

            ArrayList mbList = tbMbList.GetValue(tb) as ArrayList;
            for (int i = 0; i < mbList.Count; i++)
            {
                tempMeth = mbList[i] as MethodBuilder;
                tempMeth.Dispose();
                mbList[i] = null;
            }
            tbMbList.SetValue(tb, null);

            ModuleBuilder tempMod = tbMod.GetValue(tb) as ModuleBuilder;
            tempMod.Dispose();
            tbMod.SetValue(tb, null);

            tbGenTypeParArr.SetValue(tb, null);
        }
        public static void Dispose(this MethodBuilder mb)
        {
            if (mb == null)
                return;
            Type mbType = typeof(MethodBuilder);
            FieldInfo mbILGen = mbType.GetField("m_ilGenerator", BindingFlags.Instance | BindingFlags.NonPublic);
            //FieldInfo mbIAttr = mbType.GetField("m_iAttributes", BindingFlags.Instance | BindingFlags.NonPublic);
            FieldInfo mbMod = mbType.GetField("m_module", BindingFlags.Instance | BindingFlags.NonPublic); //ModuleBuilder 
            FieldInfo mbContType = mbType.GetField("m_containingType", BindingFlags.Instance | BindingFlags.NonPublic);
            FieldInfo mbLocSigHelp = mbType.GetField("m_localSignature", BindingFlags.Instance | BindingFlags.NonPublic);//SignatureHelper
            FieldInfo mbSigHelp = mbType.GetField("m_signature", BindingFlags.Instance | BindingFlags.NonPublic);//SignatureHelper

            ILGenerator tempIlGen = mbILGen.GetValue(mb) as ILGenerator;
            tempIlGen.Dispose();
            SignatureHelper tempmbSigHelp = mbLocSigHelp.GetValue(mb) as SignatureHelper;
            tempmbSigHelp.Dispose();
            tempmbSigHelp = mbSigHelp.GetValue(mb) as SignatureHelper;
            tempmbSigHelp.Dispose();

            ModuleBuilder tempMod = mbMod.GetValue(mb) as ModuleBuilder;
            tempMod.Dispose();
            mbMod.SetValue(mb, null);

            mbILGen.SetValue(mb, null);
            mbContType.SetValue(mb, null);
            mbLocSigHelp.SetValue(mb, null);
            mbSigHelp.SetValue(mb, null);
            mbMod.SetValue(mb, null);
        }
        public static void Dispose(this SignatureHelper sh)
        {
            if (sh == null)
                return;
            Type shType = typeof(SignatureHelper);
            FieldInfo shModule = shType.GetField("m_module", BindingFlags.Instance | BindingFlags.NonPublic);
            //FieldInfo shSig = shType.GetField("m_signature", BindingFlags.Instance | BindingFlags.NonPublic);
            shModule.SetValue(sh, null);
            //shSig.SetValue(sh, null);
        }
        public static void Dispose(this ILGenerator ilGen)
        {
            if (ilGen == null)
                return;
            Type ilGenType = typeof(ILGenerator);
            FieldInfo ilSigHelp = ilGenType.GetField("m_localSignature", BindingFlags.Instance | BindingFlags.NonPublic);//SignatureHelper
            SignatureHelper sigTemp = ilSigHelp.GetValue(ilGen) as SignatureHelper;
            sigTemp.Dispose();
            ilSigHelp.SetValue(ilGen, null);
        }
        public static void Dispose(this ModuleBuilder modBuild)
        {
            if (modBuild == null)
                return;
            Type modBuildType = typeof(ModuleBuilder);
            FieldInfo modBuildModData = modBuildType.GetField("m__moduleData", BindingFlags.Instance | BindingFlags.NonPublic |BindingFlags.FlattenHierarchy );
            FieldInfo modTypeBuildList = modBuildType.GetField("m__TypeBuilderList", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);

            ArrayList modTypeList = modTypeBuildList.GetValue(modBuild) as ArrayList;
            if(modTypeList != null)
            {
                for (int i = 0; i < modTypeList.Count; i++)
                {
                    TypeBuilder tb = modTypeList[i] as TypeBuilder;
                    tb.Dispose();
                    modTypeList = null;
                }
                modTypeBuildList.SetValue(modBuild, null);
            }
            modBuildModData.SetValue(modBuild, null);
        }
编辑 找到了实际原因:似乎动态程序集中创建的每个类型都会持有对ModuleBuilder(在Type.Module中)的引用,而ModuleBuilder又会保留TypeBuilder对象列表。每次添加类型时,都会扫描此列表以检查名称冲突。如果您在类型生成过程中使用HashSet来确保不会出现名称冲突,则可以在生成Type后调用m__TypeBuilderList上的Clear私有变量,而没有任何负面影响(目前为止)。

我如何在ModuleBuilder的私有变量m__TypeBuilderList上调用“Clear”? - Renatto Machado
在.NET 4.0中,这个变成了一个名为m_TypeBuilderDictDictionary<string, Type>(其中Type实际上是TypeBuilder)。 - Pieter van Ginkel

2

首先,我注意到的是你每次迭代都创建一个新的工厂,因此也会创建一个新的AssemblyBuilder。你能否重复使用该工厂(在同一动态程序集中创建多个类型)?


1
非常好的发现,实际上没有任何区别。我会在这里立即更新代码。 - Firestrand

1

无论您现在遇到的实际问题是什么,我强烈建议您不要采用当前的方法。Reflection.Emit并不是为支持创建数十万个类型而设计的(例如,请参见this connect issue,尽管这个特定问题可能只适用于将它们全部放入单个动态程序集中的情况)。您为什么需要创建那么多类型?


谢谢提供这个链接。我会在Connect上将此问题记录下来,因为我一直无法找到解决方案。需要支持多个客户端连接到应用程序并在运行时创建新类型的原因是,我需要能够将这些类型持久化并从数据库中检索出来。不幸的是,由于内存问题,我必须寻找其他解决方案来实现这一点。 - Firestrand
@Firestrand - 我能理解在运行时动态创建某些类型的需求,但是一百万个类型实在是太多了。例如,mscorlib 只有几千个总数。 - kvb
真实的上限是几十万,但在测试中,即使只有一千个也会受到内存泄漏的限制。 - Firestrand

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接