請不要在 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 左右。為了簡化這個問題,編寫如下的性能測試代碼。

<code> 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 }/<code>

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

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

測試用的 Json 文件,“...” 處省略了 34,018 個相同內容,整個 Json 數據中包含了 3萬多個 Json 對象,實際測試的數據如下圖所示。

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

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

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

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

<code>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();.../<code>

而 matches 方法更是直接用 substring 截取當前位置到末尾的字符串,然後進行正則匹配。

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

字符串 substring 會傳入字符數組、起始位置和截取長度創建一個新的 String 對象。

<code>在 JDK7 及以上,調用該構造方法時在最後一行會複製一遍截取後的數據,這也是導致整個問題的關鍵所在了。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);
}/<code>

在 JDK7 及以上,調用該構造方法時在最後一行會複製一遍截取後的數據,這也是導致整個問題的關鍵所在了。

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


分享到:


相關文章: