C#静态方法和实例方法的内存分配测试
在模块化开发过程中,当非Code first场景下,直接与数据库交互进行DML操作时,模板内置DML语句哪种代码形式效率更高呢?
首先我们通过内存分析:
假设,找到一个数据表,将表模块化出来后,并配置当前class DML方法。
是直接将DML写在对象内?还是在class中设置静态方法?或者扩展出来?哪种更节约内存呢?于是,就有了下面的模拟测试:
- 单纯Class(不加DML方法,也不扩展)
- Class扩展DML方法
- Class中内置实例DML方法
- Class中内置静态DML方法
- 模拟500000个对象,查询内存大小以及内存中对象形式。
根据网络牛人解释如下:
方法 (Method) 是一种类型定义,所以,它被存放在 Type Object 上,Type Object 是一个被分配在托管堆上的特殊类型,在同一个 AppDomain 中,每一个类型,都对应一个全局的 Type Object。每个引用类型的实例,都包含一个指向它的直接类型的 Type Object 的指针,每个 Type Object 也存在类似的指针,用来标识它的直接父类型的 Type Object。
当调用静态方法时,CLR 会根据方法调用去寻找其对应的 Type Object,然后,把方法 JIT,JIT 之后的方法是本机代码,可以直接运行,然后,这部分代码被加载进入内存,方法的参数被加载进入当前执行栈,原来的执行上下文地址也被记录到执行栈;方法开始执行,执行完后,执行栈中的返回地址被读出,然后 CLR 利用本机跳转指令,跳转到该返回至继续执行。
当调用实例方法时,CLR 会根据实例的 Type Object 指针找到对应的 Type Object,然后,把方法 JIT,JIT 之后的方法是本机代码,可以直接运行,然后,这部分代码被加载进入内存,该实例对象,以及方法的参数被加载进入当前执行栈 (实例对象永远是第一个参数,即 arg0,利用 ldarg0 指令进行读取),原来的执行上下文地址也被记录到执行栈;方法开始执行,执行完后,执行栈中的返回地址被读出,然后 CLR 利用本机跳转指令,跳转到该返回至继续执行。
如果方法已经被 JIT 过,则不会被第二次 JIT。
方法在 IL 中是以字节流的形式存在的,所以,它仍然会占据内存。
方法 JIT 之后会被驻留在该进程的地址空间里面,因此,它也会在运行时占据内存。
方法的元数据存放在程序集 MethodRef 以及 MethodDef 表中。
定义在值类型上的实例方法就比较麻烦了,大家有兴趣可以想想它怎么执行的。因为值类型没有 Type Object 指针。
如果值类型实现一个接口,在执行接口的方法实现的时候就更加麻烦了,大家也可以想想,欢迎讨论!
最后,
大家都以为" 静态方法在堆上分配内 存,实例方法在堆栈上"
这句话完全不靠谱,不要被迷惑了。。。只要提到方法,它就一定在 Type Object 上,也就是被分配在托管堆上。
代码:
static void size() { GC.Collect(); GC.WaitForFullGCComplete(); long start = GC.GetTotalMemory(true); List<ServerUser2> u2=new List<ServerUser2>(); for (int i = 0; i < 500000; i++) { u2.Add(new ServerUser2($"User{i}", true, false, false)); } GC.Collect(); GC.WaitForFullGCComplete(); long end = GC.GetTotalMemory(true); long size = end - start; Console.WriteLine($"纯对象大小:{size}"); //////////////////////// GC.Collect(); GC.WaitForFullGCComplete(); start = GC.GetTotalMemory(true); List<ServerUser3> u3 = new List<ServerUser3>(); for (int i = 0; i < 500000; i++) { u3.Add(new ServerUser3($"User{i}", true, false, false)); } GC.Collect(); GC.WaitForFullGCComplete(); end = GC.GetTotalMemory(true); size = end - start; Console.WriteLine($"扩展对象大小:{size}"); //////////////////////// GC.Collect(); GC.WaitForFullGCComplete(); start = GC.GetTotalMemory(true); List<ServerUser4> u4 = new List<ServerUser4>(); for (int i = 0; i < 500000; i++) { u4.Add(new ServerUser4($"User{i}", true, false, false)); } GC.Collect(); GC.WaitForFullGCComplete(); end = GC.GetTotalMemory(true); size = end - start; Console.WriteLine($"普通方法对象大小:{size}"); //////////////////////// GC.Collect(); GC.WaitForFullGCComplete(); start = GC.GetTotalMemory(true); List<ServerUser5> u5 = new List<ServerUser5>(); for (int i = 0; i < 500000; i++) { u5.Add(new ServerUser5($"User{i}", true, false, false)); } GC.Collect(); GC.WaitForFullGCComplete(); end = GC.GetTotalMemory(true); size = end - start; Console.WriteLine($"静态方法对象大小:{size}"); Console.WriteLine($"断点分析对象"); }
public class ServerUser2 { [DisplayName("User")] public string User { get; set; } [DisplayName("Admin")] public bool Admin { get; set; } [DisplayName("Export")] public bool Export { get; set; } [DisplayName("Impersonate")] public bool Impersonate { get; set; } public ServerUser2() { } public ServerUser2(string pUser, bool pAdmin, bool pExport, bool pImpersonate) { User = pUser; Admin = pAdmin; Export = pExport; Impersonate = pImpersonate; } } public class ServerUser3 { [DisplayName("User")] public string User { get; set; } [DisplayName("Admin")] public bool Admin { get; set; } [DisplayName("Export")] public bool Export { get; set; } [DisplayName("Impersonate")] public bool Impersonate { get; set; } public ServerUser3() { } public ServerUser3(string pUser, bool pAdmin, bool pExport, bool pImpersonate) { User = pUser; Admin = pAdmin; Export = pExport; Impersonate = pImpersonate; } } public class ServerUser4 { [DisplayName("User")] public string User { get; set; } [DisplayName("Admin")] public bool Admin { get; set; } [DisplayName("Export")] public bool Export { get; set; } [DisplayName("Impersonate")] public bool Impersonate { get; set; } public ServerUser4() { } public ServerUser4(string pUser, bool pAdmin, bool pExport, bool pImpersonate) { User = pUser; Admin = pAdmin; Export = pExport; Impersonate = pImpersonate; } public bool Insert(DbContext db) { return db.Database.ExecuteSqlCommand($"INSERT INTO [Server].[ServerUser] ( [User], [Admin], [Export], [Impersonate]) VALUES ( N'{User}', N'{Admin}', N'{Export}', N'{Impersonate}');") > 0; } } public class ServerUser5 { [DisplayName("User")] public string User { get; set; } [DisplayName("Admin")] public bool Admin { get; set; } [DisplayName("Export")] public bool Export { get; set; } [DisplayName("Impersonate")] public bool Impersonate { get; set; } public ServerUser5() { } public ServerUser5(string pUser, bool pAdmin, bool pExport, bool pImpersonate) { User = pUser; Admin = pAdmin; Export = pExport; Impersonate = pImpersonate; } public static bool Insert(ServerUser5 item,DbContext db) { return db.Database.ExecuteSqlCommand($"INSERT INTO [Server].[ServerUser] ( [User], [Admin], [Export], [Impersonate]) VALUES ( N'{item.User}', N'{item.Admin}', N'{item.Export}', N'{item.Impersonate}');") > 0; } } public static class ServerUserDbo { public static bool Insert(this ServerUser3 item, DbContext db) { return db.Database.ExecuteSqlCommand($"INSERT INTO [Server].[ServerUser] ( [User], [Admin], [Export], [Impersonate]) VALUES ( N'{item.User}', N'{item.Admin}', N'{item.Export}', N'{item.Impersonate}');") > 0; } }
结论:
所以,我们基本上可以得出结论,相同对象数据的情况下,对象方法的实现并不会占用内存地址,我们可以理解为它在被分配到堆后,实际上就是一个栈的指针地址。只要是相同类型的类(可以是不同实例),调用的方法地址都是相同的,所以不需要为实例再分配内存。
那么方法的实现方式上一般推荐如何做呢?
为了后期更好的维护,建议扩展出来,这样,不管做任何的操作,都可以更好地复用。只是可读性不是特别好。
但是如果写成实例方法,可能Class的代码行就多了,不方便维护。