C#的LOH上都是大对象吗?

关于 CLR 的 GC堆,相信大家都知道有 SOH(小对象堆) 和 LOH(大对象堆),而且也知道它们的分割线是 85000byte,当然这是一个默认值,也可以根据具体情况修改,这里要提醒一点的是,LOH 上都是大于 85000byte 的对象吗?这是一个很有意思的问题,具体是不是,可以用 windbg 看一看便知,刚好手里有一个待分析的dump。.

首先用 !eeheap -gc 找到 segment 上的 LOH 逻辑边界。

0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x0000016ba0930298
generation 1 starts at 0x0000016ba08e60f0
generation 2 starts at 0x0000016ba0151000
ephemeral segment allocation context: none
         segment             begin         allocated              size
0000016ba0150000  0000016ba0151000  0000016ba0cc7a20  0xb76a20(12020256)
Large object heap starts at 0x0000016bb0151000
         segment             begin         allocated              size
0000016bb0150000  0000016bb0151000  0000016bb0221bc8  0xd0bc8(854984)
Total Size:              Size: 0xc475e8 (12875240) bytes.
------------------------------
GC Heap Size:            Size: 0xc475e8 (12875240) bytes.

从输出看,当前 LOH 区间段为 0000016bb0151000 0000016bb0221bc8 ,接下来用 !dumpheap 把这个区间段的所有内容给导出来。

0:000> !dumpheap 0000016bb0151000  0000016bb0221bc8
         Address               MT     Size
0000016bb0151000 0000016b9e4d7a70       24 Free
0000016bb0151018 0000016b9e4d7a70       30 Free
0000016bb0151038 00007ffac5185e70     9744     
0000016bb0153648 0000016b9e4d7a70       30 Free
0000016bb0153668 00007ffac5185e70     1048     
0000016bb0153a80 0000016b9e4d7a70       30 Free
0000016bb0153aa0 00007ffac5185e70     8184     
0000016bb0155a98 0000016b9e4d7a70       30 Free
0000016bb0155ab8 00007ffac5185e70    16344     
0000016bb0159a90 0000016b9e4d7a70       30 Free
0000016bb0159ab0 00007ffac5185e70    32664     
0000016bb0161a48 0000016b9e4d7a70       30 Free
0000016bb0161a68 00007ffac5185e70     2072     
0000016bb0162280 0000016b9e4d7a70       30 Free
0000016bb01622a0 00007ffac5185e70     4120     
0000016bb01632b8 0000016b9e4d7a70       30 Free
0000016bb01632d8 00007ffac5185e70    65304     
0000016bb01731f0 0000016b9e4d7a70       30 Free
0000016bb0173210 00007ffac5185e70    32664     
0000016bb017b1a8 0000016b9e4d7a70       30 Free
0000016bb017b1c8 00007ffac5185e70    16408     
0000016bb017f1e0 0000016b9e4d7a70       30 Free
0000016bb017f200 00007ffac5196c08   319368     
0000016bb01cd188 0000016b9e4d7a70      390 Free
0000016bb01cd310 00007ffac5185e70     8216     
0000016bb01cf328 0000016b9e4d7a70       30 Free
0000016bb01cf348 00007ffac5185e70    16344     
0000016bb01d3320 0000016b9e4d7a70   191118 Free
0000016bb0201db0 00007ffac5185e70   130584     

Statistics:
              MT    Count    TotalSize Class Name
0000016b9e4d7a70       15       191892      Free
00007ffac5196c08        1       319368 System.Int64[]
00007ffac5185e70       13       343696 System.Object[]
Total 29 objects

从 Statistics 列看,当前有 Free, Int64[] 和 Object[] 三大类对象,下面简要分析下。

  1. Free

这个表示 空间块,简单来说就是 segment 段上对象和对象之间的间隙,这个间隙大概分为两种,要么是被GC标记为Free的废对象,要么是挑选到合适的free块后所剩余下来的空间。

CLR 内部使用一个 FreeList 进行管理,当 alloc 对象到 LOH 时,会优先从 FreeList 上选择 最佳 free 块给对象,所以这里的 free < 85000byte 情有可原。

  1. Object[]

有很多小于 85000byte 的 object[],这些数组常用于CLR内部目的,比如你所看到的 static ,string驻留池,缓存的反射信息 等等,它们是不能被 GC 所回收的,那如何实现呢?这就需要用到句柄表,而句柄表的底层又是由 CLR 内部 LargeHeapHandleTable 结构所管理的,接下来随便抽一个。

0:000> !gcroot 0000016bb0153668
HandleTable:
    0000016b9e7317e8 (pinned handle)
    -> 0000016bb0153668 System.Object[]

Found 1 unique roots (run '!GCRoot -all' to see all roots).

0:000> !mdt -e:2 -count:10 0000016bb0153668
0000016bb0153668 (System.Object[], Elements: 128)
[0] 0000016ba0151420 
[1] 0000016ba0152828 "StationDemo"
[2] 0000016ba0152858 "Do not open more"
[3] 0000016ba0152898 "Program"
[4] 0000016ba01528c0 "程序异常:"
[5] 0000016ba01528e8 ",###+ "
[6] 0000016ba0152910 "Program exception:"
[7] 0000016ba0156c98 "Security Exception (ControlAppDomain LinkDemand) while trying to register Shutdown handler with the AppDomain. LoggerManager.Shutdown() will not be called automatically when the AppDomain exits. It must be called programmatically."
[8] 0000016ba0156e80 "log4net.RepositorySelector"
[9] 0000016ba0156ed0 "Exception while resolving RepositorySelector Type ["
expand next 10 items   expand all 128 items   increase depth

上面这些输出看样子就是一些 驻留池字符串

接下来的一个问题是还有其他的小于 85000byte 的情况吗?还真有,比如 32bit 下的 double[1000] 就属于大对象,不可思议把,为了验证,来段代码看看吧。

    public class Program
    {
        public static void Main()
        {
            double[] nums = new double[1000];

            for (int i = 0; i < nums.Length; i++)
            {
                nums[i] = i;
            }

            Console.WriteLine("添加结束!");

            Console.ReadLine();
        }
    }

接下来用 windbg 看一下。

0:000> !DumpObj /d 039d1020
Name:        System.Double[]
MethodTable: 05cb4b08
EEClass:     05cb4aac
Size:        8012(0x1f4c) bytes
Array:       Rank 1, Number of elements 1000, Type Double (Print Array)
Fields:
None
0:000> !objsize 039d1020
sizeof(039D1020) = 8012 (0x1f4c) bytes (System.Double[])
0:000> !gcwhere 039d1020
Address    Gen   Heap   segment    begin      allocated   size
039D1020   3      0     039D0000   039D1000   039D2F70    0x1f4c(8012)

可以很清楚的看到,当前的 double[] 大小才 8k 就上了 LOH,有点意思,对吧。