.Net托管堆布局

.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

加载堆

主要是供CLR内部使用,作为承载程序的元数据。

  1. HighFrequencyHeap
    存放CLR高频使用的内部数据,比如MethodTable,MethodDesc.
  1. LowFrequencyHeap
    存放CLR低频使用的内部数据,比如EEClass,ClassLoader.
  1. StringLiteralMap
    字符串驻留池:https://www.cnblogs.com/lmy5215006/p/18494483
  1. StubHeap
    函数入口的代码堆
  2. CodeHeap
    JIT编译代码使用的内部堆,比如生成IL。
  3. VirtualCallStubHeap
    虚方法调用的内部堆

眼见为实

.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

托管堆

大家的老朋友了,不做过多解释,由GC统一管理的内存堆.一个.NET程序中所有的Domain都会共用一个托管堆

  1. SOH
    略略略
  2. LOH
    略略略
  3. POH
    固定对象专属的堆,比如非托管线程访问托管对象,就需要把对象固定起来,避免被GC回收造成非托管代码的访问违例.

眼见为实

.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

冻结堆

.NET8推出来的一个新堆,用来存放永远不会被GC管理的永生对象,比如string 字面量。
简单来说,就是一个对象你都永远不会释放了,还放在托管堆就是浪费了。不如单独拎出来存。

眼见为实

.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

上述所说的各种堆,只是一个逻辑上的概念。作为内存的物理承载。由堆段(Heap Seg-ment)实现.
简单来说,段是托管堆的物理表示。

.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

眼见为实

.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

SOH小对象堆

堆只是一个抽象的概念,在物理上的表现形式为内存段,作为CLR细化堆的一种管理单位。多个段组成了堆。

.NET8之前的段结构

在.NET 8 之前,段分为SOH,LOH,POH 三个段。
对于SOH段有点特殊,因为段上面还有分代逻辑。包含0代和1代的对象只会分配在新分配的内存段上(临时段),剩下的每个段都是2代的段
.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP
可以看到,代只是一个逻辑概念,并没有独立的段空间。0,1,2代共享段空间。

.NET8的段结构

到了.NET 8,代已经不是一个逻辑概念,而是一个物理概念。
每个代都有了自己独立的段空间。
.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

代机制

每当GC触发时,所有对象(非固定)都会进行升代,直到gen2为止。

  1. obj对象刚创建,为0代
    内存地址为0x00000263ee009528,0x01fb08000028>0x000001fb080b71e0>01fb080b9068 说明obj放在0代里
    .NET Core 堆结构(Heap)底层原理浅谈-LMLPHP
  2. 第一次GC,obj升为1代
    内存地址在1代空间范围内
    .NET Core 堆结构(Heap)底层原理浅谈-LMLPHP
  3. 第二次GC,obj升为2代
    内存地址在2代空间范围内
    .NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

代边界

细心的朋友会发现一个盲点,就是obj刚刚创建的时候,0代内存起始点为0263ee000028,升为1代后,1代内存起始点也变为了0263ee000028,2代也同样。
这就引申出另一个概念,GC升代,不是简单的copy对象从0代到1代。而是移动代的边界。
每次GC触发时,代边界指针会在多个“地址段”上迁移,通过这种逻辑操作,达到性能的最高,可以观察上面的 Allocated 区,一会给了 0gen,一会又给了 1gen,一会又给了 2gen

LOH大对象堆

大对象堆存储所有>=85000byte的对象,但也是有例外。LOH堆上对象管理相对宽松,没有“代”机制,默认情况下也不会压缩。
.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

例外1-32位环境下的double[]

        static void Main(string[] args)
        {
            double[] array1 = new double[999];
            Console.WriteLine(GC.GetGeneration(array1));

            double[] array2 = new double[1000];
            Console.WriteLine(GC.GetGeneration(array2));

            double[,] array3 = new double[32,32];
            Console.WriteLine(GC.GetGeneration(array3));

            long[] array4 = new long[1000];
            Console.WriteLine(GC.GetGeneration(array4));

            Debugger.Break();
            Console.ReadKey();
        }

.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

这里有个很奇怪的现象,在32位环境下,array2的大小= 4b+4+4+1000*8=8012byte. 远远<=85000byte. 为什么被分配到了LOH堆?
这主要跟内存对齐有关,double的未对齐访问非常昂贵,远远超过long,ulong,int。这对于64位环境来说不是问题,总是对SOH与LOH使用8byte对齐。但对于4字节对齐的32位环境。这就是个大问题了.
所以CLR开发团队决定将阈值大于1000的double放入LOH堆(LOH堆总是8byte对齐)。避免了double未对齐访问的巨大成本

例外2-StringInter与静态成员以及元数据

https://www.cnblogs.com/lmy5215006/p/18515971
参考此文,在.NET5之前没有POH堆,所以CLR内部使用的三个数组也会进入LOH堆。
三个数组分别为

  1. static对象的object[]
  2. 字符串池 object[]
  3. 元数据 RuntimeType object[]

POH堆

POH堆解决了什么问题?
从.NET5开始,CLR团队给pinned的对象单独放入一个段中,这样pinned对象不会和普通对象混在一起。导致大量细小Free空间。从而降低托管堆碎片化,也降低了代降级的频次。

如何使用POH堆?

在.NET 8中,将对象放入POH堆是一种“有意为之”行为,必须调用 GC 类提供的 AllocateArray 和 AllocateUninitializedArray 方法并设置 pinned=true
.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

FOH

FOH堆解决了什么问题?
在.NET8中,如果一个对象在创建的时候,就明确知道是“永生”对象,那就没必要纳入托管堆的管理范围,只会徒增GC的工作量。因此干脆把对象放在托管堆之外,来提高性能
.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

常见的例子就是字符串的字面量(literal)

static对象布局

静态的基元类型(short,int,long) ,它的值本身并不存放在托管堆上。而是存放在Domain中的高频堆中
.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

静态的引用类型则不同。真正的对象存放在托管堆上,再由POH中一个object[]持有,最后被高频堆中的m_pGCStatics所管理
.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

眼见为实:静态基元类型分配在高频堆上?

    internal class Program
    {
        static long age = 10086;
        static void Main(string[] args)
        {
            age = 12;
            Console.WriteLine("done. " + age);
            Debugger.Break();
        }
    }

.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP
通过汇编得知,static a的地址为00007ff9a618e4a8
.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP
观察高频堆地址可以发现,00007FF9A6180000<00007ff9a618e4a8<00007FF9A6190000 。明显属于高频堆

眼见为实:静态引用类型分配在哪?

    internal class Program
    {
        public static Person person = new Person();

        static void Main(string[] args)
        {
            var num = person.age;

            Console.WriteLine(num);

            Debugger.Break();
        }
    }

    public class Person
    {
        public int age = 12;
    }
  1. 使用!gcwhere命令来查看person对象属于0代中,说明对象本身分配在托管堆
    .NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

  2. 使用!gcroot命令查看它的引用根,发现它被一个object[]所持有
    .NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

  3. 再查看object[]的所属代,可以看到该对象属于POH堆
    .NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

  4. bp coreclr!JIT_GetSharedNonGCStaticBase_Helper 下断点来获取 DomainLocalModule 实例
    .NET Core 堆结构(Heap)底层原理浅谈-LMLPHP
    注意,这里我重新运行了一遍,所以object[]内存地址有变

字符串驻留池布局

关于字符串的不可变性,参考此文:https://www.cnblogs.com/lmy5215006/p/18494483

.NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

在.NET8之前,字符串驻留与静态引用类型处理模式无差别。
.NET 8加入FOH堆之后,会将编译期间就能确定的字符串放入FOH堆,以便提高GC性能。

眼见为实

        static void Main(string[] args)
        {
            var str1 = "hello FOH";//编译期间能确定
            var str2 = Console.ReadLine();
            string.Intern(str2);//运行期间才能确定

            Console.WriteLine($"str1={str1},str2={str2}");
            Debugger.Break();
        }
  1. 编译期间能确定的,直接加入了FOH
    .NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

  2. 运行期间确定,与静态引用类型处理流程一致
    .NET Core 堆结构(Heap)底层原理浅谈-LMLPHP
    .NET Core 堆结构(Heap)底层原理浅谈-LMLPHP

12-11 15:36