请不要在 JDK 7+ 中使用这个 JSON 包了!

简介: Json-lib 是以前 Java 常用的一个 Json 库,最后的版本是 2.4,分别提供了 JDK 1.3 和 1.5 的支持,最后更新时间是 2010年12月14日。虽然已经很多年不维护了,但在搜索引擎上搜索 "Java 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秒左右才能完成,如下图所示。

image.png

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

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

image.png

使用 Java Mission Control 记录执行的情况,如下图所示,可以看到分配了大量 char[] 数组。

image.png

翻看相关源码,其中 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);
}
相关文章
|
缓存 NoSQL Java
接口幂等该如何设计和实现
本文探讨了程序开发中常见的重复操作问题,如多次点击生成多余订单或支付、RPC调用失败后的重试机制滥用及非法重复请求等。通过接口幂等性设计可有效解决这类问题,确保相同请求多次执行结果一致无副作用。文章详细解释了幂等性的概念及其重要性,并提供了具体的设计与实现方法,包括使用唯一标识符、设计幂等操作、事务处理及缓存策略。此外,还讨论了实现幂等性接口所带来的好处,如并发请求处理、失败请求管理及系统集成等,并提出了验证接口幂等性的策略。通过这些技术和方法的应用,可以显著提升系统的稳定性和用户体验。
367 1
|
人工智能 并行计算 数据中心
NVIDIA智算中心“产品”上市,AI工业革命的iPhone时刻
NVIDIA智算中心“产品”上市,AI工业革命的iPhone时刻
|
SQL Java 数据库连接
MyBatis之动态SQL、#与$的区别和结果映射
MyBatis之动态SQL、#与$的区别和结果映射
278 0
|
Java 程序员 编译器
Java异常处理
Java异常处理
89 1
|
机器学习/深度学习
Lesson 5.3 ROC-AUC 的计算方法、基本原理与核心特性
Lesson 5.3 ROC-AUC 的计算方法、基本原理与核心特性
|
Java 数据库连接 开发者
计量单位及数据校验解读
计量单位及数据校验解读
|
机器人 程序员 编译器
程序员优秀之路:一起来看下这 97 位”砖家“能给出啥编程的好建议?(1)
咱们程序员在接到需求初期,是没办法对整个需求作完全正确评估的!(本瓜以为,由产品需求到技术落地是有着天然的鸿沟的)所以,多数情况下,我们都会在代码迭代过程中面对之前未预想到的问题。
|
关系型数据库 MySQL 数据库管理
MySQL相关子查询
MySQL相关子查询
266 0
MySQL相关子查询
|
网络协议 应用服务中间件 Linux
Nginx的安装与配置(Centos7、云服务器版)
Nginx的安装与配置(Centos7、云服务器版)
456 0
Nginx的安装与配置(Centos7、云服务器版)
|
关系型数据库 MySQL 数据库
mysql导入表时出现的问题,1153
mysql导入表时出现的问题,1153
283 0