在 .net 的内存分配中,有四种逻辑区域,Gen 0/1/2 三代是针对小对象的,还专门有个 LOH 大对象堆,里面放的是“大”对象。因为大对象在内存中复制来复制去成本很高,因此特别给它安排了 LOH,LOH 回收的时候只标记和清除废弃的对象,不会进行压缩,也就是说,不会在 LOH 中挪动大对象。因此如果中间部分大对象被回收了,这个段中间就有孔洞,这跟小对象堆是不同的,小对象堆每次回收都会压缩,全都缩在一起(pin住了的除外)。
.net 团队经过大量测试,找到了大小对象的分界点,为 85000 字节,>= 85000 bytes 的就是大对象。大对象一般是各种数组,除了机器生成的代码,正常的对象也到不了那么大。但 double 数组是个特例,如果 double 数组超过 1000 个元素,就认为它是大对象了,据说是因为 double 是 8 bytes 的,要 8 字节对齐,所以特殊对待。但没找到资料说 long 是怎么处理的。现在 clr 开源了,有空可以去看看具体是怎么处理。
大对象 gc 有两个特点:
如何避免频繁 gc LOH 呢?我想到的有下面几个办法:
//在栈上直接分配数组
unsafe
{
Char* pc = stackalloc Char[20000];
}
//或构造一个 struct,将数组嵌入到 struct,由于 struct 是分配在栈上的,也实现了栈上分配数组
unsafe struct CharArray
{
public fixed Char Characters[20];
}
在实践中,大对象经常是比较长的字符串,例如 xml、生成的 html、以及传来传去的 json 等,这都是需要注意的地方。譬如小心选择靠谱的 json 序列化方案,限制用户上传的内容长度或将长度分块,流式处理数据以避免分配过多内存等。