-
[x] 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原理,看看没有官方的解释和源码来论证我上述的猜想,所以...下期再见~
-
-
相关文章
- 01-13Golang中的append是否会帮助nil类型的变量分配内存?
- 01-13动态内存的分配与释放
- 01-13C#中关于内存的存放。
- 01-13动态内存分配的方法实现冒泡排序
- 01-13c – 由于64位进程的内存分配过多,Windows冻结
- 01-13内存分配策略:minor gc前后的几种特殊情况
- 01-13垃圾收集器与内存分配策略---内存的分配与回收
- 01-13了解JVM运行时的内存分配
- 01-13c-在C中为可分配的Fortran分配内存
- 01-13JNA释放Memory对象分配的内存