适用对象:后端工程师、数据平台开发者
典型场景:大体积 API 响应(MB/GB 级)、日志/ETL 导入、大模型数据集处理
1. 大 JSON 的痛点
在工程里遇到过这种场景:
- 一次性加载:前端传过来一个 200MB 的 JSON 文件,如果用
JSON.parse()
或 Jackson 的readTree()
,直接就会把整个文件展开到内存。8 核 16G 的服务器瞬间卡主,GC 一直在跑。 - 数组超大:像日志、交易流水、埋点数据,经常是一个 JSON 数组里几百万条对象。内存里 hold 不住,但业务逻辑只需要逐条处理并写库。
- I/O 与 CPU 不匹配:磁盘和网络是流式的,但我们偏要一次性把 JSON 吃干抹净,再开始处理,白白浪费了 pipeline 的机会。
所以,核心问题就是:如何避免一口吃撑”。
2. 懒加载:用到再解析
懒加载的思路很简单:不要提前把整个树结构展开,而是保留游标,等到访问时再解析。
在 Java 里,可以用 Jackson 的 JsonNode
懒绑定特性,或者自定义 Iterator
包装流。
示例:Jackson 的懒解析
1 | ObjectMapper mapper = new ObjectMapper(); |
这样做的好处:
- 单条对象级别的懒解析:不用一次性展开整个数组。
- 内存占用可控:常驻内存只存一条记录。
坏处也明显:如果你需要频繁随机访问(比如第 999999 条对象),就不如一次性载入了。
3. 流式解析:边读边处理
如果懒加载是“访问才算账”,流式解析就是“流水账”,不记本子。
主流 JSON 库大多支持流式 API,比如:
- Jackson Streaming API(
JsonParser
/JsonGenerator
) - Gson Streaming(
JsonReader
) - Python ijson
- Node.js JSONStream
示例:Python 处理 GB 级 JSON
1 | import ijson |
特点:
- 内存里永远只有一条对象。
- 可以直接串到下游(数据库批量写入、消息队列)。
实际测试下来,1GB 的 JSON 文件在 8G 内存机器上几乎没压力,CPU 基本就是序列化/反序列化本身的成本。
4. 工程实践中的取舍
(1)什么时候用懒加载?
- 中等体量 JSON(几十 MB 级):一次性解析略显浪费,但又想保留对象树结构,适合懒加载。
- 有条件访问场景:比如只会访问其中某些字段或部分节点。
(2)什么时候用流式解析?
- 日志/流水/埋点类数据:天然就是“顺序处理”,适合边读边写。
- 超大 JSON(GB 级):唯一可行方案,内存 hold 不住。
(3)性能数据对比
以 500MB 的 JSON 数组(500 万条对象)为例,在一台 16G 内存的机器上:
- 一次性 parse:耗时约 35s,内存峰值 8GB+,GC 频繁。
- 懒加载:耗时约 28s,内存峰值 < 1GB。
- 流式解析:耗时约 25s,内存常驻 ~200MB。
注:数据来自实际测试,因语言/库实现不同会有差异,但趋势一致。
5. 额外技巧
- 切块存储:上游可以把超大数组切成 NDJSON(每行一个对象),这样天然流式友好,Fluent Bit / Spark 都能吃。
- 配合异步/批处理:解析一条→放进队列→消费者写库,比解析完一大坨再批量处理更均衡。
- 错误容忍:流式解析时更容易做到“跳过坏行继续跑”,避免全量失败。
6. 小结
到这里总结下, 大 JSON 不要一口吞,用懒加载减少不必要的展开,用流式解析把顺序处理做成流水线。
这两个思路不是互斥的:读大文件时用流式解析,进入业务层时再懒加载字段,可以把效率和易用性都兼顾。