请不要在JDK7及以上用Json-lib了

【Json-lib 介绍】java

Json-lib 是之前 Java 经常使用的一个 Json 库,最后的版本是 2.4,分别提供了 JDK 1.3 和 1.5 的支持,最后更新时间是 2010年12月14日。虽然已经不少年不维护了,但在搜索引擎上搜索 "Java Json" 等相关的关键词发现好像一直还有人在介绍和使用这个库。项目官网是 http://json-lib.sourceforge.net/apache

 

【一句话结论】json

Json-lib 在经过字符串解析每个 Json 对象时,会对当前解析位置到字符串末尾进行 substring 操做,因为 JDK7 及以上的 substring 会完整拷贝截取后的内容,因此当遇到较大的 Json 数据而且含有较多对象时,会进行大量的字符数组复制操做,致使了大量的 CPU 和内存消耗,甚至严重的 Full GC 问题。数组

 

【问题分析】缓存

某天发现线上生产服务器有很多 Full GC 问题,排查发现产生 Full GC 时某个老接口量会上涨,但这个接口除了解析 Json 外就是将解析后的数据存储到了缓存中,遂怀疑跟接口请求参数大小有关,打日志发现确实有比通常请求大得多的 Json 数据,但也只有 1MB 左右。为了简化这个问题,编写以下的性能测试代码。服务器

 1 package net.mayswind;
 2 
 3 import net.sf.json.JSONObject;
 4 import org.apache.commons.io.FileUtils;
 5 
 6 import java.io.File;
 7 
 8 
 9 public class JsonLibBenchmark {
10     public static void main(String[] args) throws Exception {
11         String data = FileUtils.readFileToString(new File("Z:\\data.json"));
12         benchmark(data, 5);
13     }
14 
15     private static void benchmark(String data, int count) {
16         long startTime = System.currentTimeMillis();
17 
18         for (int i = 0; i < count; i++) {
19             JSONObject root = JSONObject.fromObject(data);
20         }
21 
22         long elapsedTime = System.currentTimeMillis() - startTime;
23         System.out.println(String.format("count=%d, elapsed time=%d ms, avg cost=%f ms", count, elapsedTime, (double) elapsedTime / count));
24     }
25 }
View Code

上述代码执行后平均每次解析须要 7秒左右才能完成,以下图所示。ide

测试用的 Json 文件,“...” 处省略了 34,018 个相同内容,整个 Json 数据中包含了 3万多个 Json 对象,实际测试的数据以下图所示。性能

{
    "data":
    [
        {
            "foo": 0123456789,
            "bar": 1234567890
        },
        {
            "foo": 0123456789,
            "bar": 1234567890
        },
        ...
    ]
}

使用 Java Mission Control 记录执行的状况,以下图所示,能够看到分配了大量 char[] 数组。测试

翻看相关源码,其中 JSONObject._fromJSONTokener 方法主要内容以下所示。能够看到其在代码一开始就匹配是否为 "null" 开头。this

private static JSONObject _fromJSONTokener(JSONTokener tokener, JsonConfig jsonConfig) {
    try {
        if (tokener.matches("null.*")) {
            fireObjectStartEvent(jsonConfig);
            fireObjectEndEvent(jsonConfig);
            return new JSONObject(true);
        } else if (tokener.nextClean() != '{') {
            throw tokener.syntaxError("A JSONObject text must begin with '{'");
        } else {
            fireObjectStartEvent(jsonConfig);
            Collection exclusions = jsonConfig.getMergedExcludes();
            PropertyFilter jsonPropertyFilter = jsonConfig.getJsonPropertyFilter();
            JSONObject jsonObject = new JSONObject();
...

而 matches 方法更是直接用 substring 截取当前位置到末尾的字符串,而后进行正则匹配。

public boolean matches(String pattern) {
    String str = this.mySource.substring(this.myIndex);
    return RegexpUtils.getMatcher(pattern).matches(str);
}

字符串 substring 会传入字符数组、起始位置和截取长度建立一个新的 String 对象。

public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

在 JDK7 及以上,调用该构造方法时在最后一行会复制一遍截取后的数据,这也是致使整个问题的关键所在了。

public String(char value[], int offset, int count) {
    if (offset < 0) {
        throw new StringIndexOutOfBoundsException(offset);
    }
    if (count <= 0) {
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        if (offset <= value.length) {
            this.value = "".value;
            return;
        }
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > value.length - count) {
        throw new StringIndexOutOfBoundsException(offset + count);
    }
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}
相关文章
相关标签/搜索