在C#中,大对象堆(LOH,Large Object Heap)是.NET垃圾回收器专门用来存放大型托管对象的一块内存区域。当对象大小达到或超过85000字节时,运行时会将其分配在LOH上,而不是普通的小对象堆。由于LOH上的对象体积大、回收成本高,且默认不进行压缩,长期分配和释放容易造成内存碎片,进而影响程序性能。

LOH到底是什么
LOH全称Large Object Heap,是CLR中区别于Gen0、Gen1、Gen2的一块特殊堆。它的核心特征如下:
- 分配阈值:对象实例大小 >= 85000字节(约83KB)。
- 常见对象:大数组(如byte[]、int[])、大字符串(拼接后超阈值)、大型非托管包装对象。
- 回收方式:LOH对象只在Gen2 GC(即Full GC)时被回收,且传统模式下不压缩,易产生碎片。
可以用一段简单代码观察对象是否进入LOH:
using System;
using System.Runtime;
class Program
{
static void Main()
{
// 分配一个大于85000字节的数组,会进入LOH
byte[] bigArray = new byte[90000];
Console.WriteLine("已分配90KB数组,该对象通常位于LOH");
// 手动触发GC观察(仅演示)
GC.Collect();
}
}
为什么需要避免对象进入LOH
对象进入LOH后,会带来几个明显问题:
- 回收频率低但代价高:只有Full GC才处理,易引发长时间停顿。
- 内存碎片:反复分配释放大对象,LOH出现空洞,可能导致明明有余量却分配失败。
- 性能抖动:Web服务或实时系统中,突然的GC停顿会影响响应。
C#如何避免对象进入LOH
1. 拆分大对象
如果业务允许,将大数组切分为多个小数组,使每个都低于阈值:
using System;
using System.Collections.Generic;
class SplitDemo
{
static List<byte[]> SplitBuffer(int totalSize, int chunkSize = 80000)
{
var list = new List<byte[]>();
for (int i = 0; i < totalSize; i += chunkSize)
{
int size = Math.Min(chunkSize, totalSize - i);
list.Add(new byte[size]); // 每个都小于LOH阈值
}
return list;
}
}
2. 使用对象池复用
对于频繁创建销毁的缓冲区,用ArrayPool<T>复用,避免反复分配:
using System;
using System.Buffers;
class PoolDemo
{
static void Process()
{
var pool = ArrayPool<byte>.Shared;
byte[] buf = pool.Rent(80000); // 可能来自池,不进LOH
try
{
// 使用buf处理数据
}
finally
{
pool.Return(buf);
}
}
}
3. 减少临时大字符串
字符串拼接超阈值也会进LOH,应使用StringBuilder或分块写入:
using System;
using System.Text;
class StrDemo
{
static string BuildLarge(int count)
{
var sb = new StringBuilder();
for (int i = 0; i < count; i++)
{
sb.Append("item"); // 避免直接+拼接产生大对象
}
return sb.ToString();
}
}
4. 使用非连续结构
可用List<byte[]>或Memory<T>代替单一大数组,结合管道处理。
| 策略 | 适用场景 | 效果 |
|---|---|---|
| 拆分对象 | 可分段的数据 | 避开LOH阈值 |
| 对象池 | 临时缓冲区 | 减少分配次数 |
| StringBuilder | 字符串构建 | 降低大字符串产生 |
小结
理解LOH是C#内存调优的基础。通过控制对象大小、复用资源、合理设计数据结构,就能有效避免对象进入大对象堆,减少GC压力和碎片问题,让应用更加稳定高效。