奇遇记之——读取大文件导致OOM
一、问题背景
我们的目的是从动态生成的json文件中读取信息,暂且叫它foo.json吧。
由于前期开发时,发现生成的foo.json都比较小(50kb左右,大的也不过几百kb不超过1M),因此在解析这个json文件时,就直接把它整个读取到内存中,再通过fastjson对json数据处理。雷就此埋下了。。。
二、案发现场
当同事跟我反映项目出问题了,接口返回的数据不正常。便去看运行日志,发现了这么一条记录:
java.lang.OutOfMemoryError:Java heap space
当时百思不得其解,到底是哪里造成的堆内存异常呢?由于是第一次在企业项目遇到这个问题,抱着大胆猜测小心验证的态度,最后发现居然是由于生成的foo.json太大了(接近600M,这是纯文本呀,大家可以想象下有多少数据)。找到问题原因后,就好解决了。
既然一次性读取,内存遭不住,那就只好分批读取咯。
三、解决方案
- 使用缓存:使用缓存技术提高读取效率,例如将读取到的数据暂时存储到一个缓存区中,待缓存区达到一定大小后再一次性解析缓存中存储的 JSON 数据。
- 使用流式读取:采用流式读取方式,即边读边解析,避免一次性将整个文件全部读入内存。这种方式需要借助 JsonReader 这样支持流式读取的工具类,能够大幅降低内存占用和读取速度。
第一种思路是对的,但是由于是json格式的数据,那么对格式是有要求的。比如某个json对象比较大,缓存区只存了它一部分的内容,这个部分数据想要解析就要另写逻辑了。因此这里重点介绍Gson的JsonReader。
核心API介绍:
beginArray()
:读取一个数组的开始标记[
。endArray()
:读取一个数组的结束标记]
。beginObject()
:读取一个对象的开始标记{
。endObject()
:读取一个对象的结束标记}
。hasNext()
:判断当前位置之后是否还有更多的元素,并返回true
或false
。nextName()
:读取一个JSON对象中的字段名并返回这个字段名的字符串形式,如果这个字段不存在则返回null。nextBoolean()
:读取下一个JSON值并将其解析为布尔类型,如果这个JSON值不是布尔类型则抛出一个IOException。nextDouble()
:读取下一个JSON值并将其解析为双精度浮点数类型,如果这个JSON值不是数字类型则抛出一个IOException。nextString()
:读取下一个JSON值并将其解析为字符串类型。skipValue()
:跳过当前JSON元素,以便在读取无需处理的JSON文件时快速前进。
举例:
foo.json:
{
"version_major": 1,
"version_minor": 27,
"version_patch": 0,
pipelines": [
"build",
"test",
"report"
]
}
JsonReader reader = new JsonReader(new FileReader("foo.json"));
reader.beginObject();
while (reader.hasNext()) {
String key = reader.nextName();
if (key.equals("pipelines")) {
reader.beginArray();
while (reader.hasNext()) {
reader.beginObject();
while (reader.hasNext()) {
System.out.println(reader.nextName());
}
reader.endObject();
}
reader.endArray();
} else {
reader.skipValue();
}
}
reader.endObject();
这样就把pipelines
数组中的元素读取出来啦。当然这只是个示例,如果你的json格式够复杂。那么你会发现代码中while
和if
嵌套的层次就会很深。这个时候采用递归的思路或许可以解决你的问题。
最后:建议大家使用Gson的JsonReader,而不用FastJson的JSONReader。经过测试发现FastJson的JSONReader虽然也是流式读取,但是对内存的占用依然很高。对于我们不想要的数据Gson提供skipValue()
跳过,而FastJson只能通过readObject()
跳过。
转载自:https://juejin.cn/post/7239225588065108029