.NET 大对象JSON写入磁盘替代内存 解决海量对象内存溢出问题
核心疑问解答:为什么磁盘存储能解决内存不足问题
你之前的疑惑本质上是默认了「所有要写入磁盘的对象必须同时驻留内存」,这个前提完全不成立。
你现在遇到OOM的核心原因是:所有生成的对象都被List<T>集合持有强引用,GC根本无法回收这些对象占用的内存。单个对象27KB,300万个对象仅实体数据就占约77GB,算上.NET对象头、堆预留空间、集合开销实际占用只会更高,跑到300万时正好触碰到进程内存阈值,GC反复做全量回收却找不到可释放的内存,就会出现程序卡顿、最后直接内存不足崩溃的情况。
而磁盘存储的核心优化逻辑是流式处理+短生命周期绑定:你不需要一次性生成所有对象,也不需要把生成过的对象都留在内存里。同一时间内存里只需要保留当前正在生成、写入/读取的一小批对象,处理完立刻释放引用,GC随时可以回收这部分内存,内存占用会一直稳定在很低的水平,根本不会出现内存撑爆的问题。
可落地的实现方案
- 采用冷热分层的存储架构,不要无限制往内存
List<T>里塞对象- 热数据层:固定预留内存容量存访问频率最高的300万条数据,达到容量阈值后不再往内存集合加新对象,新生成的对象转入冷数据处理逻辑
- 冷数据层:剩余700万条数据存在本地磁盘,不要存成单个大JSON文件(否则随机读取需要从头扫描整个文件,性能极差),采用分块+内存索引的结构:
- 每攒够100010000条冷数据,就序列化成一个独立的块文件,块大小控制在27MB270MB之间,平衡文件数量和单块加载开销
- 内存中只维护轻量索引:每条索引只存数据唯一标识、所属块编号、块内偏移位置,单条索引仅占十几字节,700万条索引总大小不到150MB,完全可以常驻内存
- 访问冷数据时先查索引定位到具体块,只加载对应块到内存反序列化,取到目标对象后立刻释放块的内存引用,不需要加载全量冷数据
- 序列化必须用流式API,不要用全量内存序列化的写法
不要用JsonSerializer.Serialize()先把整个对象/整个集合转成字符串/字节数组再写文件,这种写法会在内存里生成完整的序列化副本,额外占一倍内存。直接用流式写入API往文件流里逐对象写即可,示例代码:// 初始化文件流,直接写磁盘,不需要在内存攒全量数据 using var fileStream = File.OpenWrite($"./cold_blocks/block_{blockId}.dat"); using var jsonWriter = new Utf8JsonWriter(fileStream); jsonWriter.WriteStartArray(); for (int i = 0; i < batchSize; i++) { // 生成单个实体对象 var entity = GenerateEntity(currentOffset + i); // 直接序列化写入流,不需要把对象存入全局List JsonSerializer.Serialize(jsonWriter, entity); // 这里没有任何强引用持有entity,GC随时可以回收这部分内存 } jsonWriter.WriteEndArray(); - 几个立竿见影的优化点
- 项目配置中开启64位平台目标,同时开启GC大对象堆支持,减少大对象分配导致的堆碎片
- 如果对象中存在大量重复字符串值(比如枚举描述、固定分类标签),用字符串池复用字符串实例,避免重复值占内存
- 对冷数据随机访问性能要求高的话,可以用
MemoryMappedFile(内存映射文件)访问块文件,比普通文件流随机读性能高2~5倍,不需要手动处理文件流Seek逻辑 - 不要给27KB大小的业务对象用struct(值类型),大值类型拷贝开销极高,反而会降低性能、增加额外内存占用
常见思路误区
要给对象赋值才能写入,所以写入磁盘的对象必须先存在于内存中
这个表述本身没错,但错在默认要求所有对象同时存在于内存。你完全可以把对象生命周期和处理批次绑定:同一时间内存里只存当前批次的少量对象,处理完就释放,内存占用永远不会随总数据量线性上涨,哪怕总数据量到几千万、几亿都不会出现OOM。
内容的提问来源于stack exchange,提问作者ag925
相关产品推荐
相关产品推荐

