概述
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.javaimportlombok.Builder;importlombok.Data;@Data@BuilderpublicclassStudent{privateStringname;privateIntegerage;privateStringaddress;publicStudent(Stringname,Integerage,Stringaddress){this.name=name;this.age=age;this.address=address;}}创建测试main方法:/***定义循环次数*/privatefinalstaticIntegerNUM=100;publicstaticvoidmain(String[]args){//总时间longtotalTime=0L;//初始化学生数据List<Student>studentList=newArrayList<>();//10w学生for(inti=0;i<100000;i ){studentList.add(Student.builder().name(“我犟不过你”).age(10).address(“黑龙江省哈尔滨市南方区哈尔滨大街267号”).build());}//按指定次数循环for(inti=0;i<NUM;i ){//单次循环开始时间longstartTime=System.currentTimeMillis();//遍历学生数据studentList.forEach(student->{//序列化Strings=JSONObject.toJSONString(student);//字符串转回java对象JSONObject.parseObject(s,Student.class);});//将学生list序列化,之后转为jsonArrayJSONArrayjsonArray=JSONArray.parseArray(JSONObject.toJSONString(studentList));//将jsonArray转java对象listjsonArray.toJavaList(Student.class);//单次处理时间longendTime=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,以免造成偏差:
下面正是开始测试:
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执行时,内存的使用情况,使用如下方式启动:
如上所示的启动放肆会直接打开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
与gc时间差11.55相差无几,那么我们可以得到一个结论:fastjson2的性能表现,与堆空间的大小相关!
2.4 第二次试验
我们似乎得到了一个结论,但是如何确定是fastjson2的那个方法消耗更多的内存空间呢?毕竟我们在测试方法中,调用了很多的方法。
所以我们进一步调小内存,看看是否会有内存溢出呢?
我们将内存调整为64M:
-Xms64m-Xmx64m
运行后发现果然出现了内存溢出,并且明确的指出是堆空间内存溢出:
Exceptioninthread”main”java.lang.OutOfMemoryError:Javaheapspaceatjava.util.Arrays.copyOf(Arrays.java:3210)atjava.util.Arrays.copyOf(Arrays.java:3181)atjava.util.ArrayList.grow(ArrayList.java:265)atjava.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)atjava.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)atjava.util.ArrayList.add(ArrayList.java:462)atcom.alibaba.fastjson2.JSONReader.read(JSONReader.java:1274)atcom.alibaba.fastjson2.JSON.parseArray(JSON.java:1494)atcom.alibaba.fastjson2.JSONArray.parseArray(JSONArray.java:1391)atcom.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-Xmx1gfastjson单次耗费时间: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 序列化 writertoJSONString方法
其实所谓的序列化,就是JSONObject.toJSONString的体现,所以我们通过跟踪其源码去发现其原理,注意我写注释的位置。
查看JSONFactory.defaultObjectWriterProvider的内容:
publicObjectWriterProvider(){init();//初始化【ObjectWriterCreator】,用来创建【ObjectWriterProvider】ObjectWriterCreatorcreator=null;switch(JSONFactory.CREATOR){case”reflect”://反射creator=ObjectWriterCreator.INSTANCE;break;case”lambda”://lambdacreator=ObjectWriterCreatorLambda.INSTANCE;break;case”asm”:default:try{//asmcreator=ObjectWriterCreatorASM.INSTANCE;}catch(Throwableignored){//ignored}if(creator==null){creator=ObjectWriterCreatorLambda.INSTANCE;}break;}this.creator=creator;}
如上所示,我们看到此处初始化了ObjectWriterCreator,其实现方式默认是基于ASM的动态字节码实现。
另外还提供了 反射 和 lambda 的方式。
到此为止已经获取到了ObjectWriterProvider,它的作用是用来获取ObjectWriter的。
getObjectWriter方法
writeContext.provider.getObjectWriter(valueClass,valueClass,fieldBased);
继续查看getObjectWriter方法,查看关键位置代码:
if(objectWriter==null){//获取creator,此处获取的是方法开始时默认的【ObjectWriterCreatorASM】ObjectWriterCreatorcreator=getCreator();if(objectClass==null){objectClass=TypeUtils.getMapping(objectType);}//此处创建ObjectWriter,内部创建【FieldWriter】objectWriter=creator.createObjectWriter(objectClass,fieldBased?JSONWriter.Feature.FieldBased.mask:0,modules);ObjectWriterprevious=fieldBased?cacheFieldBased.putIfAbsent(objectType,objectWriter):cache.putIfAbsent(objectType,objectWriter);if(previous!=null){objectWriter=previous;}}createObjectWriter方法
查看creator.createObjectWriter伪代码:
//遍历java对象当中的getter方法,获取属性名BeanUtils.getters(objectClass,method->{……StringfieldName;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】:
//创建该属性的fieldWriterFieldWriterfieldWriter=createFieldWriter(objectClass,fieldName,fieldInfo.ordinal,fieldInfo.features,fieldInfo.format,fieldInfo.label,method,writeUsingWriter);//将属性名作为key,fieldWriter作为value放入缓存【fieldWriterMap】FieldWriterorigin=fieldWriterMap.putIfAbsent(fieldName,fieldWriter);
循环过所有的getter方法后,会得到一个全部属性的List fieldWriters集合:
fieldWriters=newArrayList<>(fieldWriterMap.values());
再往后,fastjson2会组装一个动态类:【ObjectWriter_1】,在里面组装能够写入JSONWriter的各种属性和方法,以及get属性获取:
定义和初始化此对象的方法如下所示:
//定义【ObjectWriter_1】的属性genFields(fieldWriters,cw);//定义【ObjectWriter_1】的方法genMethodInit(fieldWriters,cw,classNameType);//定义【ObjectWriter_1】获取对象属性的读取方法genGetFieldReader(fieldWriters,cw,classNameType,newObjectWriterAdapter(objectClass,null,null,features,fieldWriters));
此动态对象的末尾【1】是随数量增长的。
继续向下跟踪到如下方法:
genMethodWrite(objectClass,fieldWriters,cw,classNameType,writerFeatures);
此方法主要的作用是创建【ObjectWrite_1】的write方法,并匹配当前java对象的属性属于哪种类型,使用哪种FieldWriter进行写入。
其内部会轮询所有的属性进行匹配,我们的属性主要是String和Integer,如下:
……elseif(fieldClass==Integer.class){//处理Integer属性gwInt32(mwc,fieldWriter,OBJECT,i);}elseif(fieldClass==String.class){//处理String属性gwFieldValueString(mwc,fieldWriter,OBJECT,i);}……
Integer 在内部处理时,会在动态对象生成名称是writeInt32的方法。
String 内部处理时在动态对象生成方法writeString。
再向下会通过以下方法修改写入不同类型属性的方法名称和描述信息等
genMethodWriteArrayMapping(“writeArrayMapping”,objectClass,writerFeatures,fieldWriters,cw,classNameType);
能够看到,Integer和String的后续处理方法不同:
Stringelseif(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(Throwablee){thrownewJSONException(“createobjectWritererror,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对象**@paramtextjson字符串*@param需要转换的类*@returnClass*/@SuppressWarnings(“unchecked”)static<T>TparseObject(Stringtext,Class<T>clazz){if(text==null||text.isEmpty()){returnnull;}//创建reader,内部与writer相同,使用ASM动态字节码形式创建creatertry(JSONReaderreader=JSONReader.of(text)){//获取上下文JSONReader.Contextcontext=reader.context;booleanfieldBased=(context.features&JSONReader.Feature.FieldBased.mask)!=0;//获取ObjectReaderObjectReader<T>objectReader=context.provider.getObjectReader(clazz,fieldBased);Tobject=objectReader.readObject(reader,0);if(reader.resolveTasks!=null){reader.handleResolveTasks(object);}returnobject;}}JSONReader.of方法
创建reader对象,
publicstaticJSONReaderof(Stringstr){if(str==null){thrownewNullPointerException();}//创建reader的上下文,内部与writer相同,使用ASM动态字节码形式创建creater,包装成contextContextcontext=JSONFactory.createReadContext();//jdk8以上版本使用下面的字符串处理方式if(JDKUtils.JVM_VERSION>8&&JDKUtils.UNSAFE_SUPPORT&&str.length()>1024*1024){try{bytecoder=UnsafeUtils.getStringCoder(str);if(coder==0){byte[]bytes=UnsafeUtils.getStringValue(str);returnnewJSONReaderASCII(context,str,bytes,0,bytes.length);}}catch(Exceptione){thrownewJSONException(“unsafegetString.codererror”);}returnnewJSONReaderStr(context,str,0,str.length());}//jdk8及以下字符串处理finalintlength=str.length();char[]chars;if(JDKUtils.JVM_VERSION==8){//jdk8字符串转charchars=JDKUtils.getCharArray(str);}else{chars=str.toCharArray();}//创建JSONReaderUTF16对象returnnewJSONReaderUTF16(context,str,chars,0,length);}getObjectReader方法
if(objectReader==null){//获取前面创建的createrObjectReaderCreatorcreator=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();//创建FieldreadercreateFieldReader(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对象:
Tobject=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还是比较多的,对于需要稳定性的项目还是不建议尝试。具体表现如下:
抛开上述存在的问题,fastjson2确实有不错的性能提升,通过官方提供的测试数据可以看得出来,感兴趣可以本地实测一下。
https://alibaba.github.io/fastjson2/benchmark_cn
到此为止关于fastjson2的介绍就结束了,感谢大家的观看。
我个人也是摸索着去学习和阅读,对于有些解释可能还存在一些误区和误读,希望爱好阅读源码的朋友们帮忙指点出来。本文仅作为大家阅读源码的参考,希望有更多的fastjson2的源码阅读类文章出现,便于大家一起学习。
推荐
Java面试题宝典
技术内卷群,一起来学习!!