You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#(.Net6.0)使用Newtonsoft.Json解析大JSON文件如何降低内存占用

超大JSON行文件解析内存优化方案

问题背景

  • 需求:将多行独立JSON存储的大文件解析为二维数组结构,现有功能已开发完成,但运行时内存占用达到源文件大小的10倍
  • 测试场景:sample.json共包含100000行数据,每行JSON字段不固定,文件大小为500MB时,现有代码运行内存占用约5GB
  • 技术栈:Newtonsoft.Json、.NET 6.0
  • 文件格式:为JSON Lines格式,每行是一个独立的JSON对象,样例内容如下:
{Field1:0,Field2:1,Field2:3}
{Field1:0,Field5:1,Field6:3}
{Field1:0,Field7:1,Field9:3}
{Field1:0,Field13:1,Field50:3,Field57:3}
...

现有实现代码

JSON读取逻辑

static void Read()
{
    List<Dictionary<string, string>> rows = new List<Dictionary<string, string>>();
    string path = @"D:\small.json";
   
    using (FileStream fsRead = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    using (BufferedStream bsRead = new BufferedStream(fsRead))
    using (StreamReader srRead = new StreamReader(bsRead))
    {
        string? line;
        while ((line = srRead.ReadLine()) != null)
        {
            JObject jsonObject = JObject.Parse(line);
            MakeRowData(jsonObject, out var row);

            rows.Add(row);
        }
    }
}

行数据构造逻辑

private static  void MakeRowData(JObject jsonData, out Dictionary<string, string> row)
{
    Dictionary<string, string> output = new Dictionary<string, string>();

    foreach (var item in jsonData)
    {
        int childSize = 0;

        if (item.Value != null)
        {
            childSize = item.Value.Children().Count();

            /// 有子节点则递归展开
            if (childSize > 0)
            {
                ExploreChild(item.Value, ref output);
            }
            /// 无子节点直接写入
            else
            {
                string str = item.Value.ToString();
                output[item.Key] = str ?? "";
            }
        }
    }
    row = output;
}

private static void ExploreChild(JToken jToken, ref Dictionary<string, string> row)
{
    foreach (var item in jToken)
    {
        int childSize = item.Children().Count();

        /// 有子节点则递归展开
        if (childSize > 0)
        {
            ExploreChild(item,  ref row);
        }
        /// 无子节点直接写入
        else
        {
            string path = jToken.Path.Replace('[', '(').Replace(']', ')');
            string str = jToken.First.ToString();
            row[path] = str?? "";
        }
    }
}

内存占用过高的核心原因

  • 全量加载:所有解析完成的行数据全部存入List<Dictionary<string, string>>全程驻留内存,是最大的开销来源
  • 解析冗余:JObject.Parse会为每行JSON构造完整的JToken对象树,所有值都转为引用类型存储,中间临时对象多,内存开销远大于原始文本
  • 字符串重复:每行的相同字段名都会生成新的字符串实例,没有复用;递归过程中反复生成Path字符串、执行Replace操作,产生大量冗余字符串
  • 逻辑冗余:反复调用Children().Count()遍历子节点判断嵌套层级,产生不必要的遍历开销;Dictionary未指定初始容量,扩容时会产生双倍临时内存拷贝
  • 逻辑bug:ExploreChild方法中遍历子节点时,取值和取路径用的是父级jToken而非当前遍历的item,嵌套场景下会出现值重复、取值错误的问题,也会产生无效赋值开销

可落地优化方案

  • 优先改为流式处理:如果业务场景允许,不要把所有行全量存在内存中,每解析完一行就直接执行后续业务逻辑(比如写入数据库、导出文件、投递下游),处理完立即释放当前行的内存,这一步可以降低80%以上的内存占用。如果确实需要全量数据驻留内存,先扫描全文件收集所有出现过的字段名做统一映射,每行用string[]按列索引存储,替换Dictionary结构,内存占用可以降低50%以上。
  • 替换JObject解析为逐Token读取:不要用JObject.Parse生成完整对象树,改用JsonTextReader逐Token读取每行JSON内容,在读取过程中直接拼接键路径、提取值,跳过中间JObject/JToken对象的生成,这一步可以减少60%左右的解析临时内存。核心实现示例:
Dictionary<string, string> output = new Dictionary<string, string>(16); // 指定初始容量避免扩容
StringBuilder pathBuilder = new StringBuilder();
using (var jsonReader = new JsonTextReader(new StringReader(line)))
{
    while (jsonReader.Read())
    {
        switch (jsonReader.TokenType)
        {
            case JsonToken.PropertyName:
                // 维护当前属性路径,嵌套场景直接拼接,不需要事后Replace
                pathBuilder.Clear();
                pathBuilder.Append(jsonReader.Value.ToString());
                break;
            case JsonToken.String:
            case JsonToken.Integer:
            case JsonToken.Float:
            case JsonToken.Boolean:
            case JsonToken.Date:
            case JsonToken.Null:
                // 直接取值写入,不生成JToken对象
                string key = pathBuilder.ToString();
                // 复用字符串实例,避免重复生成相同键名
                key = string.IsInterned(key) ?? string.Intern(key);
                output[key] = jsonReader.Value?.ToString() ?? "";
                break;
            // 嵌套对象、数组的路径拼接可在这里维护栈结构实现
        }
    }
}
  • 减少字符串冗余分配:所有字段名用string.Intern做驻留复用,避免10万行数据生成几十万份重复的键名字符串;路径拼接用StringBuilder动态维护,不要每次递归全量生成Path字符串再执行Replace操作。
  • 可选替换序列化库:.NET 6环境下System.Text.Json的只读逐Token解析内存占用比Newtonsoft.Json低30%-50%,如果没有强依赖Newtonsoft的特殊特性,可以直接替换进一步降低内存。
  • 及时释放临时资源:如果必须全量加载数据,可以在每解析完1000行时手动触发一次GC回收临时内存,避免大量临时对象在GC堆二代驻留抬高内存占用。

内容的提问来源于stack exchange,提问作者YS R

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 23:06:22