概述
FastJson2
是FastJson
项目的重要晋级,方针是为下一个十年供给一个高功能的JSON
库。依据官方给出的功能来看,比较v1版别,的确有了很大的提高,本篇文章咱们来看下终究做了哪些事情,使得功能有了大幅度的提高。
本篇将选用代码测验
+ 源码阅览
的办法对FastJson2的功能提高做一个较为全面的探索。
一、环境预备
首要,咱们搭建一套用于测验的环境,这儿选用springboot项目,别离创立两个module:fastjson
和fastjson2
。运用两个版别进行比照试验。
代码结构如下所示:
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
,避免造成误差:
2.1 第一次测验
下面正是开始测验:
-
fastjson成果
单次消耗时刻:863ms 单次消耗时刻:444ms 单次消耗时刻:424ms 单次消耗时刻:399ms 单次消耗时刻:384ms 单次消耗时刻:355ms 单次消耗时刻:353ms 单次消耗时刻:363ms ... ... 单次消耗时刻:361ms 单次消耗时刻:356ms 单次消耗时刻:355ms 单次消耗时刻:357ms 单次消耗时刻:351ms 单次消耗时刻:354ms 均匀消耗时刻:366ms
如上所示,除了第一次很慢,第2次变快,到终究根本稳定在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履行时,内存的运用状况,运用如下办法发动:
如上所示的发动放肆会直接翻开jvisualvm的控制面板,挑选Visual GC,终究成果如下所示:
如上所示有几处要点,独自看下:
-
GC次数
如上所示,一共GC了1814次,耗时34.089s,终究一次失利的原因是内存分配失利。
-
Full GC
如上所示,老年代发生了316次GC,耗时27.225s。
经过上面的调查,根本能够确认由于GC导致了fastjson2全体处理时刻变长。
2.3 fastjson的GC表现
咱们能够再看下fastjson傍边的gc是什么样的:
-
GC次数
如上可知,fastjson1中发生了1675次gc,与fastjson2比较少了139次,并且时刻少了11.55s。
经过前面测验的成果,fastjson1均匀时刻366ms,而fastjson2是478ms,别离乘以100次,能够得到如下的时刻差:
(478∗100−366∗100)/1000=11.2(478*100 – 366*100)/1000 = 11.2
与gc时刻差11.55相差无几,那么咱们能够得到一个定论:fastjson2的功能表现,与堆空间的巨细相关!
2.4 第2次试验
咱们好像得到了一个定论,可是如何确认是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行:
供给debug发现终究反常出现在如下代码:
定论:在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特点获取:
界说和初始化此目标的办法如下所示:
//界说【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】:
再回来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联系图如下:
本节首要针对首要流程进行梳理,与上图比照存在部分未讲解流程,感兴趣同学参照源码自行阅览。
整个进程较为杂乱,简略描绘为:运用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目标进行回来:
此目标包括setterFieldReaders
,用于向java目标写入数据。
回到parseObject
下面看如何读取json数据到java目标:
object = objectReader.readObject(reader, 0);
object内部首要是循环遍历fieldReaders
,它内部包括json傍边的特点和目标的set办法:
正是经过这些特点和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联系图:
感兴趣的同学能够参阅上图的内容,结合本文供给的流程,自己盯梢一遍源码。
整个过成简略描绘:底层运用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仍是比较多的,关于需求稳定性的项目仍是不主张测验。具体表现如下:
-
源码阅览难度,这个是我最想吐槽的,悉数源码几乎没有注释信息,读起来仍是比较晦涩的。作者期望读者能够经过PR的办法弥补注释,也期望更多读者参加进来,目前关于Fastjson2的源码阅览文章根本为0。
抛开上述存在的问题,fastjson2的确有不错的功能提高,经过官方供给的测验数据能够看得出来,感兴趣能够本地实测一下。
到此为止关于fastjson2的介绍就结束了,感谢我们的观看。
我个人也是探索着去学习和阅览,关于有些解说或许还存在一些误区和误读,期望爱好阅览源码的朋友们帮助指点出来。本文仅作为我们阅览源码的参阅,期望有更多的fastjson2的源码阅览类文章出现,便于我们一起学习。
我正在参与技术社区创作者签约计划招募活动,点击链接报名投稿。