使用JsonSerializer.DeserializeAsyncEnumerable反序列化1GB文件为何占用2GB+内存?
解决.NET 8中JsonSerializer.DeserializeAsyncEnumerable内存占用过高的问题
1. 确认JSON结构与枚举方式匹配
DeserializeAsyncEnumerable仅针对顶层JSON数组做流式处理,如果你的JSON是行分隔格式(JSON Lines,每行一个对象)或外层有包装对象,它会被迫加载整个文件到内存。
- 正确的顶层数组格式示例:
[ {"Id":1,"Name":"Alice"}, {"Id":2,"Name":"Bob"}, ... ] - 如果是JSON Lines格式,需改用逐行读取+单独反序列化:
await using var stream = File.OpenRead("large.json"); using var reader = new StreamReader(stream, leaveOpen: true); string? line; while ((line = await reader.ReadLineAsync()) != null) { var person = JsonSerializer.Deserialize<Person>(line); // 处理person,处理后立即释放引用 ProcessPerson(person); person = null; // 帮助GC标记回收 }
2. 优化JsonSerializerOptions配置
默认配置可能存在不必要的内存开销,调整以下选项:
var options = new JsonSerializerOptions { // 禁用不必要的元数据保留 IncludeFields = false, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // 减少序列化/反序列化时的内存分配 PropertyNameCaseInsensitive = true, AllowTrailingCommas = true, ReferenceHandler = ReferenceHandler.IgnoreCycles // 避免循环引用导致的内存泄漏 };
使用该配置调用DeserializeAsyncEnumerable:
await using var stream = File.OpenRead("large.json"); await foreach (var person in JsonSerializer.DeserializeAsyncEnumerable<Person>(stream, options)) { ProcessPerson(person); // 不要将person存入集合,处理后立即丢弃引用 }
3. 避免枚举时缓存所有对象
如果你的基准测试代码中存在将所有Person对象添加到List<Person>或其他集合的逻辑,内存必然会飙升到接近文件大小。确保处理单个对象后不保留其引用,仅在必要时临时使用。
4. 手动使用Utf8JsonReader实现极致内存控制
如果DeserializeAsyncEnumerable的封装仍有内存开销,可直接使用Utf8JsonReader逐对象解析,完全控制内存分配:
await using var stream = File.OpenRead("large.json"); using var buffer = MemoryPool<byte>.Shared.Rent(1024 * 1024); // 1MB池化缓冲区 int bytesRead; var reader = new Utf8JsonReader(new ReadOnlySequence<byte>(), isFinalBlock: false, state: default); while ((bytesRead = await stream.ReadAsync(buffer.Memory)) > 0) { var sequence = new ReadOnlySequence<byte>(buffer.Memory.Slice(0, bytesRead)); reader = new Utf8JsonReader(sequence, isFinalBlock: bytesRead == 0, reader.CurrentState); // 跳过数组开始标记 if (reader.TokenType == JsonTokenType.StartArray) reader.Read(); while (reader.TokenType != JsonTokenType.EndArray) { if (reader.TokenType == JsonTokenType.StartObject) { var person = JsonSerializer.Deserialize<Person>(ref reader); ProcessPerson(person); } else { reader.Read(); } } }
这种方式直接复用池化内存,避免中间缓冲区的冗余分配。
5. 基准测试的正确姿势
确保基准测试使用Release模式运行,且禁用BenchmarkDotNet的内存诊断以外的不必要功能。如果使用Debug模式,JIT优化未启用,内存开销会显著增加。
6. 检查数据集生成逻辑
确认你的数据集生成代码没有产生异常大的JSON对象,或意外生成了嵌套过深的结构。单个对象过大也会导致内存占用超出预期。
内容的提问来源于stack exchange,提问作者DeanOfHouseAkin
相关产品推荐
相关产品推荐

