天天看點

請不要在 JDK 7+ 中使用這個 JSON 包了!

Json-lib 介紹

Json-lib 是以前 Java 常用的一個 Json 庫,最後的版本是 2.4,分别提供了 JDK 1.3 和 1.5 的支援,最後更新時間是 2010年12月14日。

雖然已經很多年不維護了,但在搜尋引擎上搜尋 "Java Json"等相關的關鍵詞發現好像一直還有人在介紹和使用這個庫。

項目官網是:

http://json-lib.sourceforge.net/

一句話結論

Json-lib 在通過字元串解析每一個 Json 對象時,會對目前解析位置到字元串末尾進行 substring 操作。

由于 JDK7 及以上的 substring 會完整拷貝截取後的内容,是以當遇到較大的 Json 資料并且含有較多對象時,會進行大量的字元數組複制操作,導緻了大量的 CPU 和記憶體消耗,甚至嚴重的 Full GC 問題。

問題分析

某天發現線上生産伺服器有不少 Full GC 問題,排查發現産生 Full GC 時某個老接口量會上漲,但這個接口除了解析 Json外就是将解析後的資料存儲到了緩存中。

遂懷疑跟接口請求參數大小有關,打日志發現确實有比一般請求大得多的 Json 資料,但也隻有 1MB 左右。為了簡化這個問題,編寫如下的性能測試代碼。

package net.mayswind;

import net.sf.json.JSONObject;
import org.apache.commons.io.FileUtils;

import java.io.File;

public class JsonLibBenchmark {
    public static void main(String[] args) throws Exception {
        String data = FileUtils.readFileToString(new File("Z:\\data.json"));
        benchmark(data, 5);
    }

    private static void benchmark(String data, int count) {
        long startTime = System.currentTimeMillis();

        for (int i = 0; i < count; i++) {
            JSONObject root = JSONObject.fromObject(data);
        }

        long elapsedTime = System.currentTimeMillis() - startTime;
        System.out.println(String.format("count=%d, elapsed time=%d ms, avg cost=%f ms", count, elapsedTime, (double) elapsedTime / count));
    }
}      

上述代碼執行後平均每次解析需要 7秒左右才能完成,如下圖所示。

請不要在 JDK 7+ 中使用這個 JSON 包了!

測試用的 Json 檔案,“...” 處省略了 34,018 個相同内容,整個 Json 資料中包含了 3萬多個 Json 對象,實際測試的資料如下圖所示。

{
    "data":
    [
        {
            "foo": 0123456789,
            "bar": 1234567890
        },
        {
            "foo": 0123456789,
            "bar": 1234567890
        },
        ...
    ]
}      
請不要在 JDK 7+ 中使用這個 JSON 包了!

使用 Java Mission Control 記錄執行的情況,如下圖所示,可以看到配置設定了大量 char[] 數組。

請不要在 JDK 7+ 中使用這個 JSON 包了!

翻看相關源碼,其中 JSONObject._fromJSONTokener 方法主要内容如下所示。可以看到其在代碼一開始就比對是否為 "null" 開頭。

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);
}