image.png
image.png

概述

FastJson2是FastJson项目的重要升级,目标是为下一个十年提供一个高性能的JSON库。根据官方给出的性能来看,相比v1版本,确实有了很大的提升,本篇文章我们来看下究竟做了哪些事情,使得性能有了大幅度的提升。

本篇将采用代码测试 + 源码阅读的方式对FastJson2的性能提升做一个较为全面的探索。

一、环境准备

首先,我们搭建一套用于测试的环境,这里采用springboot项目,分别创建两个module:fastjson和fastjson2。使用两个版本进行对比试验。

代码结构如下所示:

image.png
image.png

1.1 引入对应依赖

在父pom当中引入一些我们需要使用的公共依赖,这里为了简便,使用了

<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.24</version> </dependency> 

在fastjson当中引入fastjson的依赖:

<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.79</version> </dependency> 

在fastjson2当中引入fastjson2的依赖:

<dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.8</version> </dependency> 

1.2 创建测试类

这里为了方便,直接使用main方法进行测试。

  • 创建类:Student.java

    import lombok.Builder; import lombok.Data; @Data @Builder public class Student { private String name; private Integer age; private String address; public Student(String name, Integer age, String address) { this.name = name; this.age = age; this.address = address;
        }
    }
  • 创建测试main方法:

    /**
    * 定义循环次数
    */ private final static Integer NUM = 100; public static void main(String[] args) { // 总时间 long totalTime = 0L; //初始化学生数据 List<Student> studentList = new ArrayList<>(); // 10w学生 for (int i = 0; i < 100000; i++) {
           studentList.add(Student.builder().name("我犟不过你").age(10).address("黑龙江省哈尔滨市南方区哈尔滨大街267号").build());
       } // 按指定次数循环 for (int i = 0; i < NUM; i++) { // 单次循环开始时间 long startTime = System.currentTimeMillis(); // 遍历学生数据 studentList.forEach(student -> { // 序列化 String s = JSONObject.toJSONString(student); //字符串转回java对象 JSONObject.parseObject(s, Student.class);
           }); // 将学生list序列化,之后转为jsonArray JSONArray jsonArray = JSONArray.parseArray(JSONObject.toJSONString(studentList)); // 将jsonArray转java对象list jsonArray.toJavaList(Student.class); //单次处理时间 long endTime = System.currentTimeMillis(); // 单次耗时 totalTime += (endTime - startTime);
           System.out.println("单次耗费时间:" + (endTime - startTime) + "ms");
       }
       System.out.println("平均耗费时间:" + totalTime / NUM + "ms");
    }

    上述代码在fastjson和fastjson2的测试中基本相同,唯一不同在于在fastjson2当中,jsonArray.toJavaList方法转变成了jsonArray.toList。

二、性能测试

本节将使用上面的代码进行测试。在此之前,我们首先需要针对两个子工程设置相同的堆空间大小128M,以免造成偏差:

image.png
image.png

2.1 第一次测试

下面正是开始测试:

  • fastjson结果

    单次耗费时间:863ms
    单次耗费时间:444ms
    单次耗费时间:424ms
    单次耗费时间:399ms
    单次耗费时间:384ms
    单次耗费时间:355ms
    单次耗费时间:353ms
    单次耗费时间:363ms ... ...
    单次耗费时间:361ms
    单次耗费时间:356ms
    单次耗费时间:355ms
    单次耗费时间:357ms
    单次耗费时间:351ms
    单次耗费时间:354ms
    平均耗费时间:366ms

    如上所示,除了第一次很慢,第二次变快,到最后基本稳定在360毫秒左右,最终的平均耗时是366ms。

  • fastjson2结果

    单次耗费时间:957ms
    单次耗费时间:803ms
    单次耗费时间:468ms
    单次耗费时间:435ms
    单次耗费时间:622ms
    单次耗费时间:409ms
    单次耗费时间:430ms
    ··· ···
    单次耗费时间:400ms
    单次耗费时间:641ms
    单次耗费时间:403ms
    单次耗费时间:398ms
    单次耗费时间:431ms
    单次耗费时间:356ms
    单次耗费时间:362ms
    单次耗费时间:626ms
    单次耗费时间:404ms
    单次耗费时间:395ms
    平均耗费时间:478ms

    如上所示,首次执行慢,逐步变快,但是后面就出现问题了,怎么执行的时间这么不稳定?跨度从390多到640多?这是怎么回事?平均时间也达到了478ms,反而比fastjson还要慢。

2.2 fastjson2慢的原因?

比较熟悉java的应该都能想到一个问题:由于堆空间大小不够,导致频繁发生GC,最终导致处理时间增长?

带着这个推测,我们使用jvisualVM来看下在fastjson2执行时,内存的使用情况,使用如下方式启动:

image.png
image.png

如上所示的启动放肆会直接打开jvisualvm的控制面板,选择Visual GC,最终结果如下所示:


image.png
image.png

如上所示有几处重点,单独看下:

  • GC次数
image.png
image.png
如上所示,总共GC了1814次,耗时34.089s,最后一次失败的原因是内存分配失败。
  • Full GC
image.png
image.png
如上所示,老年代发生了316次GC,耗时27.225s

通过上面的观察,基本可以确定由于GC导致了fastjson2整体处理时间变长

2.3 fastjson的GC表现

我们可以再看下fastjson当中的gc是什么样的:

  • GC次数
image.png
image.png
如上可知,fastjson1中发生了1675次gc,与fastjson2相比少了139次,并且时间少了11.55s。

通过前面测试的结果,fastjson1平均时间366ms,而fastjson2是478ms,分别乘以100次,能够得到如下的时间差:

<math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><annotation encoding="application/x-tex">(478*100 - 366*100)/1000 = 11.2</annotation></semantics></math>(478100366100)/1000=11.2 与gc时间差11.55相差无几,那么我们可以得到一个结论:**fastjson2的性能表现,与堆空间的大小相关!**

2.4 第二次试验

我们似乎得到了一个结论,但是如何确定是fastjson2的那个方法消耗更多的内存空间呢?毕竟我们在测试方法中,调用了很多的方法。

所以我们进一步调小内存,看看是否会有内存溢出呢?

我们将内存调整为64M:

-Xms64m -Xmx64m

运行后发现果然出现了内存溢出,并且明确的指出是堆空间内存溢出:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:265) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231) at java.util.ArrayList.add(ArrayList.java:462) at com.alibaba.fastjson2.JSONReader.read(JSONReader.java:1274) at com.alibaba.fastjson2.JSON.parseArray(JSON.java:1494) at com.alibaba.fastjson2.JSONArray.parseArray(JSONArray.java:1391) at com.wjbgn.fastjson2.test.TestFastJson2.main(TestFastJson2.java:43) 

通过如上的异常堆栈,发现异常出现在测试代码的43行:

image.png
image.png

提供debug发现最终异常出现在如下代码:

image.png
image.png

结论:在toJsonString方法时,发生了内存溢出异常。

2.5 第三次实验

下面我们将内存增大,看看是否能够提升fastjson2的性能。将堆空间大小调整为256M。

  • fastjson

    单次耗费时间:805ms
    单次耗费时间:224ms
    单次耗费时间:235ms
    单次耗费时间:228ms
    单次耗费时间:222ms ... ...
    单次耗费时间:191ms
    单次耗费时间:196ms
    单次耗费时间:193ms
    单次耗费时间:194ms
    单次耗费时间:192ms
    平均耗费时间:198ms

    如上所示,发现随着堆空间增加,fastjson1有较大的性能提升,平均时长在198ms。

  • fastjson2

    单次耗费时间:671ms
    单次耗费时间:496ms
    单次耗费时间:412ms
    单次耗费时间:405ms
    单次耗费时间:315ms
    单次耗费时间:321ms ... ...
    单次耗费时间:337ms
    单次耗费时间:326ms
    平均耗费时间:335ms

    如上所示,结果在335毫秒,随着内存增加,性能有提升,但是仍然没有fastjson1快。

通过如上的实验,我们似乎可以得到如下的结论:在数据量较大时,fastjson的性能还要好于fastjson2!

2.6 第四次试验

本次测试我们要给足够大堆空间,看看这两者的性能表现,此处将堆空间设置成1g:

-Xms1g -Xmx1g
  • fastjson

    单次耗费时间:943ms
    单次耗费时间:252ms
    单次耗费时间:156ms
    单次耗费时间:155ms ... ...
    单次耗费时间:119ms
    单次耗费时间:114ms
    单次耗费时间:108ms
    单次耗费时间:133ms
    单次耗费时间:115ms
    平均耗费时间:133ms

    如上所示,在足够大的内存条件下,fastjson的平均时间达到了133ms。

  • fastjson2

    单次耗费时间:705ms
    单次耗费时间:199ms
    单次耗费时间:172ms ... ...
    单次耗费时间:101ms
    单次耗费时间:124ms
    单次耗费时间:96ms
    平均耗费时间:119ms

    如上所示,fastjson2处理速度首次高于fastjson。

2.7 小结

通过前面的测试,我们能够得到如下的结论:

  • fastjson2相比fastjson确实是有性能提升,但是取决于堆内存的大小。

  • 堆空间小的情况下,fastjson的性能表现优于fastjson2。

  • 在适当的情况先,对jvm进行调优,是对应用程序的性能有影响的

  • 我们需要知道,堆空间并非越大越好,空间越大代表着GC处理时间会越长,其表现为应用响应时间的增加。

三、源码分析

本节将通过阅读源码的方式简单了解fastjson2的原理,主要分为两个方面进行阅读:

  • writer
  • reader

为什么通过这两个方面?

fastjson的核心就是将java对象序列化成json(对应writer),以及将json反序列化成java对象(对应reader)。而且其内部正是通过这样的命名方式去实现的。

3.1 序列化 writer

toJSONString方法

其实所谓的序列化,就是JSONObject.toJSONString的体现,所以我们通过跟踪其源码去发现其原理,注意我写注释的位置。

/**
 * Serialize Java Object to JSON {@link String} with specified {@link JSONReader.Feature}s enabled
 *
 * @param object   Java Object to be serialized into JSON {@link String}
 * @param features features to be enabled in serialization
 */ static String toJSONString(Object object, JSONWriter.Feature... features) { // 初始化 【ObjectWriterProvider】 ,关注【JSONFactory.defaultObjectWriterProvider】 JSONWriter.Context writeContext = new JSONWriter.Context(JSONFactory.defaultObjectWriterProvider, features); boolean pretty = (writeContext.features & JSONWriter.Feature.PrettyFormat.mask) != 0; // 初始化jsonwriter,ObjectWriter会将json数据写入jsonwriter JSONWriterUTF16 jsonWriter = JDKUtils.JVM_VERSION == 8 ? new JSONWriterUTF16JDK8(writeContext) : new JSONWriterUTF16(writeContext); try (JSONWriter writer = pretty ? new JSONWriterPretty(jsonWriter) : jsonWriter) { if (object == null) {
            writer.writeNull();
        } else {
            writer.setRootObject(object);
            Class<?> valueClass = object.getClass(); boolean fieldBased = (writeContext.features & JSONWriter.Feature.FieldBased.mask) != 0; // 获取ObjectWriter ObjectWriter<?> objectWriter = writeContext.provider.getObjectWriter(valueClass, valueClass, fieldBased); // ObjectWriter将数据写入JSONWriter objectWriter.write(writer, object, null, null, 0);
        } return writer.toString();
    }
}

defaultObjectWriterProvider对象

查看JSONFactory.defaultObjectWriterProvider的内容:

public ObjectWriterProvider() {
    init(); // 初始化【ObjectWriterCreator】,用来创建【ObjectWriterProvider】 ObjectWriterCreator creator = null; switch (JSONFactory.CREATOR) { case "reflect": //反射 creator = ObjectWriterCreator.INSTANCE; break; case "lambda": // lambda creator = ObjectWriterCreatorLambda.INSTANCE; break; case "asm": default: try {//asm creator = ObjectWriterCreatorASM.INSTANCE;
            } catch (Throwable ignored) { // ignored } if (creator == null) {
                creator = ObjectWriterCreatorLambda.INSTANCE;
            } break;
    } this.creator = creator;
}

如上所示,我们看到此处初始化了ObjectWriterCreator,其实现方式默认是基于ASM的动态字节码实现。

另外还提供了 反射 和 lambda 的方式。

到此为止已经获取到了ObjectWriterProvider,它的作用是用来获取ObjectWriter的。

getObjectWriter方法

ObjectWriter的作用就是将java对象写入到json当中,所以我们下面开始关注这一行代码的实现:

writeContext.provider.getObjectWriter(valueClass, valueClass, fieldBased);

继续查看getObjectWriter方法,查看关键位置代码:

if (objectWriter == null) { // 获取creator,此处获取的是方法开始时默认的【ObjectWriterCreatorASM】 ObjectWriterCreator creator = getCreator(); if (objectClass == null) {
        objectClass = TypeUtils.getMapping(objectType);
    } // 此处创建ObjectWriter,内部创建【FieldWriter】 objectWriter = creator.createObjectWriter(
            objectClass,
            fieldBased ? JSONWriter.Feature.FieldBased.mask : 0,
            modules
    );
    ObjectWriter previous = fieldBased
            ? cacheFieldBased.putIfAbsent(objectType, objectWriter)
            : cache.putIfAbsent(objectType, objectWriter); if (previous != null) {
        objectWriter = previous;
    }
}

createObjectWriter方法

查看creator.createObjectWriter伪代码:

// 遍历java对象当中的getter方法,获取属性名 BeanUtils.getters(objectClass, method -> {
    ... ...
String fieldName; if (fieldInfo.fieldName == null || fieldInfo.fieldName.isEmpty()) { if (record) {
        fieldName = method.getName();
    } else { // 根据getter获取到属性名称 fieldName = BeanUtils.getterName(method.getName(), beanInfo.namingStrategy);
    }
} else {
    fieldName = fieldInfo.fieldName;
}
    ... ...

在上面的getterName方法获取到对象的属性名,找到属性后,创建对应的【FieldWriter】:

//创建该属性的fieldWriter FieldWriter fieldWriter = createFieldWriter(
        objectClass,
        fieldName,
        fieldInfo.ordinal,
        fieldInfo.features,
        fieldInfo.format,
        fieldInfo.label,
        method,
        writeUsingWriter
); // 将属性名作为key,fieldWriter作为value放入缓存【fieldWriterMap】 FieldWriter origin = fieldWriterMap.putIfAbsent(fieldName, fieldWriter);

循环过所有的getter方法后,会得到一个全部属性的List fieldWriters集合:

fieldWriters = new ArrayList<>(fieldWriterMap.values());

再往后,fastjson2会组装一个动态类:【ObjectWriter_1】,在里面组装能够写入JSONWriter的各种属性和方法,以及get属性获取:

image.png
image.png

定义和初始化此对象的方法如下所示:

//定义【ObjectWriter_1】的属性 genFields(fieldWriters, cw); // 定义【ObjectWriter_1】的方法 genMethodInit(fieldWriters, cw, classNameType); //定义【ObjectWriter_1】获取对象属性的读取方法 genGetFieldReader(
        fieldWriters,
        cw,
        classNameType, new ObjectWriterAdapter(objectClass, null, null, features, fieldWriters)
);

此动态对象的末尾【1】是随数量增长的。

继续向下跟踪到如下方法:

genMethodWrite(objectClass, fieldWriters, cw, classNameType, writerFeatures);

此方法主要的作用是创建【ObjectWrite_1】的write方法,并匹配当前java对象的属性属于哪种类型,使用哪种FieldWriter进行写入。

其内部会轮询所有的属性进行匹配,我们的属性主要是String和Integer,如下:

... ... else if (fieldClass == Integer.class) {
     // 处理Integer属性
    gwInt32(mwc, fieldWriter, OBJECT, i);
} else if (fieldClass == String.class) {
    // 处理String属性
    gwFieldValueString(mwc, fieldWriter, OBJECT, i);
} ... ...
  • Integer 在内部处理时,会在动态对象生成名称是writeInt32的方法。

  • String 内部处理时在动态对象生成方法writeString。

再向下会通过以下方法修改写入不同类型属性的方法名称和描述信息等

genMethodWriteArrayMapping("writeArrayMapping", objectClass, writerFeatures, fieldWriters, cw, classNameType);

能够看到,Integer和String的后续处理方法不同:

  • String

    else if (fieldClass == String.class) {
            methodName = "writeString";
            methodDesc = "(Ljava/lang/String;)V";
        }
  • Integer 则是对象"(Ljava/lang/Object;)V"

到此整个ObjectWriter_1对象就设置完成了,使用反射进行创建:

try {
    Constructor<?> constructor = deserClass.getConstructor(Class.class, String.class, String.class, long.class, List.class); return (ObjectWriter) constructor.newInstance(objectClass, beanInfo.typeKey, beanInfo.typeName, writerFeatures, fieldWriters);
} catch (Throwable e) { throw new JSONException("create objectWriter error, objectType " + objectClass, e);
}

回到toJSONString方法

至此我们已经拿到java对象的属性,并成功创建了【ObjectWriter】:

image.png
image.png

再返回toJSonString方法当中,看看Object的后续操作 拿到的ObjectWriter调用其【write】方法进行数据写入:

objectWriter.write(writer, object, null, null, 0);

我们已经知道不同类型属性使用不同的FieldWriter进行写入:

  • String:我们虽然提到过使用的writeString方法,但是你会发现没有对应的FieldWriter,因为它使用的是JSONWriterUTF16JDK8的writeString(String str)方法,不同版本的jdk有不同的Class。

  • Integr:使用FieldWriterInt32的writeInt32(JSONWriter jsonWriter, int value)进行写入。

关于具体的写入过程就不在介绍了。

小结

官方提供Writer关系图如下:

image.png
image.png

本节主要针对主要流程进行梳理,与上图对比存在部分未讲解流程,感兴趣同学参照源码自行阅读。

整个过程较为复杂,简单描述为:使用ASM动态字节码方式作为基础,通过java对象的getter方法获取对象的属性值,构建动态ObjectWriter对象,针对不同的对象属性,生成不同的写入方法,最终通过反射进行对象创建,最后进行java对象数据的写入。

值得一提的是,ObejctWriter对象是会进行缓存的,有助于性能的提升。

3.2 反序列化 reader

下面来看看反序列化reader的流程。因为大体流程与writer差不多,所以以下内容不做详细讲解了。

parseObject 方法

/**
 * json转换java对象
 *
 * @param text  json字符串
 * @param 需要转换的类
 * @return Class
 */ @SuppressWarnings("unchecked") static <T> T parseObject(String text, Class<T> clazz) { if (text == null || text.isEmpty()) { return null;
    } //创建reader,内部与writer相同,使用ASM动态字节码形式创建creater try (JSONReader reader = JSONReader.of(text)) { // 获取上下文 JSONReader.Context context = reader.context; boolean fieldBased = (context.features & JSONReader.Feature.FieldBased.mask) != 0; // 获取ObjectReader ObjectReader<T> objectReader = context.provider.getObjectReader(clazz, fieldBased);

        T object = objectReader.readObject(reader, 0); if (reader.resolveTasks != null) {
            reader.handleResolveTasks(object);
        } return object;
    }
}

JSONReader.of方法

创建reader对象,

public static JSONReader of(String str) { if (str == null) { throw new NullPointerException();
    } //创建reader的上下文,内部与writer相同,使用ASM动态字节码形式创建creater,包装成context Context context = JSONFactory.createReadContext(); // jdk8以上版本使用下面的字符串处理方式 if (JDKUtils.JVM_VERSION > 8 && JDKUtils.UNSAFE_SUPPORT && str.length() > 1024 * 1024) { try { byte coder = UnsafeUtils.getStringCoder(str); if (coder == 0) { byte[] bytes = UnsafeUtils.getStringValue(str); return new JSONReaderASCII(context, str, bytes, 0, bytes.length);
            }
        } catch (Exception e) { throw new JSONException("unsafe get String.coder error");
        } return new JSONReaderStr(context, str, 0, str.length());
    } // jdk 8 及以下字符串处理 final int length = str.length(); char[] chars; if (JDKUtils.JVM_VERSION == 8) { // jdk8字符串转char chars = JDKUtils.getCharArray(str);
    } else {
        chars = str.toCharArray();
    } // 创建JSONReaderUTF16对象 return new JSONReaderUTF16(context, str, chars, 0, length);
}

getObjectReader方法

与getObjectWriter类似,获取动态的json数据读取对象。关注重点代码:

if (objectReader == null) { // 获取前面创建的creater ObjectReaderCreator creator = getCreator(); // 创建ObjectReader对象,根据java类的类型 objectReader = creator.createObjectReader(objectClass, objectType, fieldBased, modules);
}

createObjectReader方法

关注下面这行代码:

// 创建属性读取对象数组 FieldReader[] fieldReaderArray = createFieldReaders(objectClass, objectType, beanInfo, fieldBased, modules);

继续跟进,发现遍历java对象的setter方法,此时我们应该能够想到,向对象设置值的时候,一定是使用的setter方法:

BeanUtils.setters(objectClass, method -> {
    fieldInfo.init(); // 创建Fieldreader createFieldReader(objectClass, objectType, namingStrategy, orders, fieldInfo, method, fieldReaders, modules);
});

createFieldReader方***获取java对象当中的属性,以及set开头的方法。

处理完对象的属性和set方法后,会生成ObjectReader对象进行返回:

image.png
image.png

此对象包含setterFieldReaders,用于向java对象写入数据。

回到parseObject

下面看如何读取json数据到java对象:

object = objectReader.readObject(reader, 0);

object内部主要是循环遍历fieldReaders,它内部包含json当中的属性和对象的set方法:

image.png
image.png

正是通过这些属性和set方法将json的数据放到java对象当中。

首先将对象的属性和值放到map当中:

valueMap.put(fieldReader.getFieldNameHash(), fieldValue);

通过下面的方法将map转换成java对象:

T object = createInstanceNoneDefaultConstructor(
        valueMap == null ? Collections.emptyMap()
                : valueMap);

内部通过构造器和值去创建一个新的java对象:

return (T) constructor.newInstance(args);

注意:因为这个原因,在java对象当中必须要有一个相应的带有参数的构造器,否则会报错。

到此为止就成功拿到转换后的java对象了。

小结

官方提供的Reader关系图:

image.png
image.png

感兴趣的同学可以参考上图的内容,结合本文提供的流程,自己跟踪一遍源码。

整个过成简单描述:底层使用ASM动态字节码为基础,通过java对象的setter方法去构建动态的ObjectReader对象,最终通过构造器去创建一个新的java对象

四、总结

关于fastjson2的简单测试,以及源码阅读到此就告一段落了。

针对fastjson2有以下几点总结:

  • fastjson2对于fastjson的兼容,可以使用下面的依赖:

    <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.8</version> </dependency> 

    但是官方也不保证100%兼容。

  • 内存占用,通过前面的测试,发现fastjson2有明显占用更大内存的现象,甚至在相同内存条件下,fastjson1可以完美执行,而fastjson2有产生内存溢出的风险。

  • Issues:通过官方的Issues能够发现目前的bug还是比较多的,对于需要稳定性的项目还是不建议尝试。具体表现如下:

image.png
image.png
  • 源码阅读难度,这个是我最想吐槽的,全部源码几乎没有注释信息,读起来还是比较晦涩的。希望读者能够通过PR的方式补充注释,也希望更多读者加入进来,目前关于Fastjson2的源码阅读文章基本为0。
image.png
image.png

抛开上述存在的问题,fastjson2确实有不错的性能提升,通过官方提供的测试数据可以看得出来,感兴趣可以本地实测一下。