close

點擊關注公眾號,實用技術文章及時了解


概述

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
importlombok.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,以免造成偏差:

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執行時,內存的使用情況,使用如下方式啟動:

如上所示的啟動放肆會直接打開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-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 序列化 writertoJSONString方法

其實所謂的序列化,就是JSONObject.toJSONString的體現,所以我們通過跟蹤其源碼去發現其原理,注意我寫注釋的位置。

/***SerializeJavaObjecttoJSON{@linkString}withspecified{@linkJSONReader.Feature}senabled**@paramobjectJavaObjecttobeserializedintoJSON{@linkString}*@paramfeaturesfeaturestobeenabledinserialization*/staticStringtoJSONString(Objectobject,JSONWriter.Feature...features){//初始化【ObjectWriterProvider】,關注【JSONFactory.defaultObjectWriterProvider】JSONWriter.ContextwriteContext=newJSONWriter.Context(JSONFactory.defaultObjectWriterProvider,features);booleanpretty=(writeContext.features&JSONWriter.Feature.PrettyFormat.mask)!=0;//初始化jsonwriter,ObjectWriter會將json數據寫入jsonwriterJSONWriterUTF16jsonWriter=JDKUtils.JVM_VERSION==8?newJSONWriterUTF16JDK8(writeContext):newJSONWriterUTF16(writeContext);try(JSONWriterwriter=pretty?newJSONWriterPretty(jsonWriter):jsonWriter){if(object==null){writer.writeNull();}else{writer.setRootObject(object);Class<?>valueClass=object.getClass();booleanfieldBased=(writeContext.features&JSONWriter.Feature.FieldBased.mask)!=0;//獲取ObjectWriterObjectWriter<?>objectWriter=writeContext.provider.getObjectWriter(valueClass,valueClass,fieldBased);//ObjectWriter將數據寫入JSONWriterobjectWriter.write(writer,object,null,null,0);}returnwriter.toString();}}defaultObjectWriterProvider對象

查看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方法

ObjectWriter的作用就是將java對象寫入到json當中,所以我們下面開始關注這一行代碼的實現:

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的後續處理方法不同:

String
elseif(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方法

與getObjectWriter類似,獲取動態的json數據讀取對象。關注重點代碼:

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還是比較多的,對於需要穩定性的項目還是不建議嘗試。具體表現如下:

源碼閱讀難度,這個是我最想吐槽的,全部源碼幾乎沒有注釋信息,讀起來還是比較晦澀的。作者希望讀者能夠通過PR的方式補充注釋,也希望更多讀者加入進來,目前關於Fastjson2的源碼閱讀文章基本為0。

拋開上述存在的問題,fastjson2確實有不錯的性能提升,通過官方提供的測試數據可以看得出來,感興趣可以本地實測一下。

https://alibaba.github.io/fastjson2/benchmark_cn

到此為止關於fastjson2的介紹就結束了,感謝大家的觀看。

我個人也是摸索着去學習和閱讀,對於有些解釋可能還存在一些誤區和誤讀,希望愛好閱讀源碼的朋友們幫忙指點出來。本文僅作為大家閱讀源碼的參考,希望有更多的fastjson2的源碼閱讀類文章出現,便於大家一起學習。

來源:juejin.cn/post/7115219049931341854

推薦

Java面試題寶典

技術內卷群,一起來學習!!

PS:因為公眾號平台更改了推送規則,如果不想錯過內容,記得讀完點一下「在看」,加個「星標」,這樣每次新文章推送才會第一時間出現在你的訂閱列表里。點「在看」支持我們吧!

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()