问题 Reflect.Emit Dynamic Type Memory Blowup


使用C#3.5我试图在运行时使用反射发射生成动态类型。我用了 动态查询库 来自Microsoft的示例创建一个类生成器。一切正常,我的问题是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; }
        }
    }
}

4988
2018-03-23 21:08


起源

仅供参考,这在.NET 4.0中实际上有点差。 - Firestrand
使用CCI或Mono.Cecil可能会有更好的性能。 - leppie
@leppie感谢CCI的建议。我正在考虑使用它。我不得不像我一样使用反射来解决问题。 - Firestrand
@leppie FYI CCI目前不支持发射到动态组件的能力。我想我可以坚持,加载和变异,但这对我正在工作的环境有一些负面的含义。 - Firestrand
我认为内部动态组件也是如此。但这也是我的担忧。 - leppie


答案:


不幸的是,有一个静态字段 ModuleBuilder 抓住记忆,永远不会得到GC。我不记得现在哪个领域及其包含的内容,但这可以在WinDbg的SOS中看到。

好消息是.NET 4支持GC-able动态组件:)


4
2018-03-25 18:23



@leppie事实证明,程序集中的每个动态类型都保留对ModuleBuilder和后续TypeBuilder的引用,后者用于Type name冲突解决。如果确保没有名称冲突,则可以清除TypeBuilder列表并释放所有内存而不会产生任何负面影响。 (至少到目前为止) - Firestrand
@Firestrand:感谢您的信息:) - leppie
@leppie如何在ModuleBuilder私有变量m__TypeBuilderList上调用“Clear”? - Renatto Machado


这似乎是System.Reflection.Emit中的实际内存泄漏。 以下新解决方案 我能够通过使用反射和手动处理过程来消除大部分使用的内存。我使用扩展方法在某些类型上添加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);
        }

编辑 找到实际原因:看起来在动态程序集中创建的每个Type都包含对该引用的引用 ModuleBuilder (在 Type.Module)反过来保留一份清单 TypeBuilder 对象。每次添加类型时都会扫描此列表以检查名称冲突。如果你保持一个 HashSet 超出类型生成例程,以确保您没有得到任何名称冲突,您可以调用清除 ModuleBuilder 私人变量 m__TypeBuilderList 之后 Type 生成没有任何负面影响(迄今为止)


4
2018-03-25 18:07



我如何在ModuleBuilder私有变量m__TypeBuilderList上调用“Clear”? - Renatto Machado
在.NET 4.0中,这变为了 Dictionary<string, Type> (哪里 Type 实际上是 TypeBuilder)与名称 m_TypeBuilderDict。 - Pieter van Ginkel


嗯,我注意到的第一件事是你正在创建一个新工厂,因此是新工厂 AssemblyBuilder,每次迭代。您可以重新使用工厂(在同一动态装配中创建多种类型)吗?


1
2018-03-23 21:24



伟大的捕获,它实际上没有区别。我将在一秒钟内更新代码。 - Firestrand


无论您现在看到的实际问题如何,我强烈建议您不要使用当前的方法。 Reflection.Emit并非旨在支持数十万种类型的创建(例如,见 这个连接问题虽然这个特定的问题可能只适用于你将它们全部放入一个动态组件中)。为什么需要创建那么多类型?


1
2018-03-24 15:14



谢谢你的链接。我会把这个问题放在连接上,因为我一直无法找到解决方案。许多动态类型的原因是我需要支持许多客户端连接到应用程序并在运行时创建新类型的能力,然后我需要能够从数据库中持久化并检索这些类型。不幸的是,有了这个内存问题,我将不得不考虑其他解决方案来解决这个问题。 - Firestrand
@Firestrand - 我可以理解在运行时动态创建一些类型的需要,但是十万种类型 很多。例如,mscorlib只有几千个。 - kvb
真正的几十万是一个上限,但在测试期间,内存泄漏明显甚至一千个会令人望而却步。 - Firestrand