天天看點

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

Json-lib 在通過字元串解析每一個 Json 對象時,會對目前解析位置到字元串末尾進行 substring 操作,由于 JDK7 及以上的 substring 會完整拷貝截取後的内容,是以當遇到較大的 Json 資料并且含有較多對象時,會進行大量的字元數組複制操作,導緻了大量的 CPU 和記憶體消耗,甚至嚴重的 Full GC 問題。

【Json-lib 介紹】

Json-lib 是以前 Java 常用的一個 Json 庫,最後的版本是 2.4,分别提供了 JDK 1.3 和 1.5 的支援,最後更新時間是 2010年12月14日。雖然已經很多年不維護了,但在搜尋引擎上搜尋 "Java Json" 等相關的關鍵詞發現好像一直還有人在介紹和使用這個庫。項目官網是 http://json-lib.sourceforge.net/。

【一句話結論】

【問題分析】

某天發現線上生産伺服器有不少 Full GC 問題,排查發現産生 Full GC 時某個老接口量會上漲,但這個接口除了解析 Json 外就是将解析後的資料存儲到了緩存中,遂懷疑跟接口請求參數大小有關,打日志發現确實有比一般請求大得多的 Json 資料,但也隻有 1MB 左右。為了簡化這個問題,編寫如下的性能測試代碼。

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

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秒左右才能完成,如下圖所示。

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

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

{
    "data":
    [
        {
            "foo": 0123456789,
            "bar": 1234567890
        },
        {
            "foo": 0123456789,
            "bar": 1234567890
        },
        ...
    ]
}      
請不要在JDK7及以上用Json-lib了

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

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

翻看相關源碼,其中 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);
}      

如果您覺得本文對您有所幫助,不妨點選下方的“推薦”按鈕來支援我!

本文及文章中代碼均基于“署名-非商業性使用-相同方式共享 3.0”,文章歡迎轉載,但請您務必注明文章的作者和出處連結,如有疑問請私信我聯系!