C#的内存分配探索 agile Posted on Jun 18 2023 c# > 本文由 [简悦 SimpRead](http://ksria.com/simpread/) 转码, 原文地址 [www.cnblogs.com](https://www.cnblogs.com/moran-amos/p/14312650.html) * C# 的内存分配探索 * C++ 中默认的 operator new 底层调用的是 malloc,在分配内存时会带上上下 cookie 用于内存回收和内存碎片合并,现在到了 C# 中,因为 C# 有独有的垃圾回收机制,那我就比较好奇 C# 中对于对象内存的排列是怎么样的,还有没有上下 cookie 存在呢?所以我进行了下面的测试 ``` public class MyClassInt { private long a; } public class MyClassLong { private int a; } static class Program { for (int i = 0; i < 3; i++) { printMemory(new MyClassInt()); } Console.WriteLine(); for (int i = 0; i < 3; i++) { printMemory(new MyClassLong()); } public static void printMemory(object o) // 获取引用类型的内存地址方法 { GCHandle h = GCHandle.Alloc(o, GCHandleType.WeakTrackResurrection); IntPtr addr = GCHandle.ToIntPtr(h); Console.WriteLine("0x" + addr.ToString("X")); } /* 输出 0xF010F8 0xF010F4 0xF010F0 0xF010EC 0xF010E8 0xF010E4 */ } ``` 起初对于 MyClassInt 的输出我很惊讶,因为 int 的 size 刚好是 4,我本来认为是 C# 对于内存分配器进行了特殊的管理,比如类似 gunc 的 pool_alloc 这样的设计,去掉了 cookie,使得对象的大小就是纯粹的 size 大小间隔,结果想想不太对劲,还是不能靠猜,在试试其他的,一试就试出来了。。。换成了 long 以后按推理方式来说应该是间隔 8,才是纯粹的 size 大小,结果一看并不是,还是 4...fuck,所以其实这个 C# 中通过 new 内存并没有带上对象的地址,所以可以看出来打印的应该是引用的地址段,每个地址段 4 字节,没有 cookie,至于指针指向的地方有没有 cookie?不得而知,不过这里可以看出这这这样的设计和 C# 的垃圾回收器有很大关系。且还有个好玩的点,不同类型的内存地址也是连续的,用的是一同一个地址段。接下来再进行一个测试: ``` for (int i = 0; i < 100000; i++) { printMemory(new MyClassInt()); } /* 其他代码不变 0x12F10F8 0x12F10F4 0x12F10F0 ... 0x12F1000 0x12F14FC 0x12F14F8 0x12F14F4 ... 0x12F1400 0x12F15FC 0x12F15F8 ... 0x12FFF08 0x12FFF04 0x12FFF00 0x31E10FC 0x31E10F8 0x31E10F4 ... 0x31EFF00 0x31F10FC ... 0x31FFF00 0x57E10FC */ ``` 这里我想知道一次分配器分配的大小是多少,不够的时候是什么时候要新的内存块?所以我进行了这个测试,测试下来结果我总结了一下: * 首先先是开始的时候总是以 XXXXF8 开始的,十进制是 248 * 然后新分配的内存会递减这个内存段,直到变成 00 * 变成 00 后会去要当前这个地址段前三位(F10)后最近的可用的地址段,比如当前是 F10 就去看看 F11 可用吗,不行就找 F12,依次,比如例中找到了 F10 用完找到了 F14,F14 用完找 F15 依次内推 * 找到后分配的大小是从 FC 开始,252,对比于 256 差了 4,很可能用了这个 4 记录了这个内存块用于 GC 的信息,比如这个内存块的引用计数。 * 然后当倒数第五到第三这两位也用完后(变为 FFF),那就会分配新的地址段,也是递增搜寻。所以新地址是 31E,默认从 E10FC 开始(921836),结束是 FFF00(1048320),可用 126484,每个对象为 4 的话就是 31621 个对对象 * 继续探索,如果想要继续往下的话就得去深挖看看 GCHandle.Alloc 的分配 * 如果深入研究`GCHandle.Alloc`,您会看到它调用了一个本地方法`InternalAlloc`: ``` [System.Security.SecurityCritical] // auto-generated [MethodImplAttribute(MethodImplOptions.InternalCall)] [ResourceExposure(ResourceScope.None)] internal static extern IntPtr InternalAlloc(Object value, GCHandleType type); ``` 其中 InternalAlloc 是 CLR 公共库的代码,其中就有这里核心的一个部分,CLR 垃圾回收器 其中 InternalAlloc 的核心就是这一句:`hnd = GetAppDomain()->CreateTypedHandle(objRef, type);` 依次调用`ObjectHandle::CreateTypedHandle`-> `HandleTable::HndCreateHandle`-> `HandleTableCache->TableAllocSingleHandleFromCache`,如果缓存堆中存在则返回,不存在则分配,这里方法调用的时候我已经 new 了这个对象了,所以对象是存在的,存在会返回这个对象的 IntPtr。在托管堆中发生的唯一分配是`IntPtr`,它保存指向表中地址的指针。所以我疯狂 print 的都是这个指针的地址,所以我探究的也是这个 intPtr 的分配策略,当我明白这一点的时候,我就发现,诶嘿,我又可以水一篇 C#GC 探索的文章了,所以我跟到了 CLR 的库,开始研究 CLR 的 GC 原理,看看没有官方的解释和源码来论证我上述的猜想,所以... 下期再见~ __EOF__ C#知识树整理——C#基础 C#中Struct 和Class 的大小