kryo是一個(gè)高性能的序列化/反序列化工具,由于其變長(zhǎng)存儲(chǔ)特性并使用了字節(jié)碼生成機(jī)制秦士,擁有較高的運(yùn)行速度和較小的體積隧土。
依賴
引入maven依賴
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.2</version>
</dependency>
需要注意的是,由于kryo使用了較高版本的asm辐脖,可能會(huì)與業(yè)務(wù)現(xiàn)有依賴的asm產(chǎn)生沖突皆愉,這是一個(gè)比較常見的問(wèn)題艇抠。只需將依賴改成:
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo-shaded</artifactId>
<version>4.0.2</version>
</dependency>
記錄類型信息
這算是kryo的一個(gè)特點(diǎn)家淤,可以把對(duì)象信息直接寫到序列化數(shù)據(jù)里瑟由,反序列化的時(shí)候可以精確地找到原始類信息,不會(huì)出錯(cuò)绿鸣,這意味著在寫readxxx方法時(shí)暂氯,無(wú)需傳入Class或Type類信息。
相應(yīng)的擎厢,kryo提供兩種讀寫方式辣吃。記錄類型信息的writeClassAndObject/readClassAndObject方法,以及傳統(tǒng)的writeObject/readObject方法厘惦。
線程安全
kryo的對(duì)象本身不是線程安全的哩簿,所以我們有兩種選擇來(lái)保障線程安全。
使用Threadlocal來(lái)保障線程安全:
private static final ThreadLocal<Kryo> kryoLocal = new ThreadLocal<Kryo>() {
protected Kryo initialValue() {
Kryo kryo = new Kryo();
kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(
new StdInstantiatorStrategy()));
return kryo;
};
};
或者使用kryo提供的pool:
public KryoPool newKryoPool() {
return new KryoPool.Builder(() -> {
final Kryo kryo = new Kryo();
kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(
new StdInstantiatorStrategy()));
return kryo;
}).softReferences().build();
}
實(shí)例化器
在上面注意到kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(new StdInstantiatorStrategy()));
這句話顯示指定了實(shí)例化器羡玛。
在一些依賴了kryo的開源軟件中稼稿,可能由于實(shí)例化器指定的問(wèn)題而拋出空指針異常讳窟。例如hive的某些版本中,默認(rèn)指定了StdInstantiatorStrategy是越。
public static ThreadLocal<Kryo> runtimeSerializationKryo = new ThreadLocal<Kryo>() {
@Override
protected synchronized Kryo initialValue() {
Kryo kryo = new Kryo();
kryo.setClassLoader(Thread.currentThread().getContextClassLoader());
kryo.register(java.sql.Date.class, new SqlDateSerializer());
kryo.register(java.sql.Timestamp.class, new TimestampSerializer());
kryo.register(Path.class, new PathSerializer());
kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
......
return kryo;
};
};
而StdInstantiatorStrategy在是依據(jù)JVM version信息及JVM vendor信息創(chuàng)建對(duì)象的碌上,可以不調(diào)用對(duì)象的任何構(gòu)造方法創(chuàng)建對(duì)象。
那么例如碰到ArrayList這樣的對(duì)象時(shí)候天梧,就會(huì)出問(wèn)題霞丧。觀察一下ArrayList的源碼:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
既然沒(méi)有調(diào)用構(gòu)造器蛹尝,那么這里elementData會(huì)是NULL,那么在調(diào)用類似ensureCapacity方法時(shí)突那,就會(huì)拋出一個(gè)異常愕难。
public void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
&& minCapacity <= DEFAULT_CAPACITY)) {
modCount++;
grow(minCapacity);
}
}
解決方案很簡(jiǎn)單,就如框架中代碼寫的一樣葱弟,顯示指定實(shí)例化器猜丹,首先使用默認(rèn)無(wú)參構(gòu)造策略DefaultInstantiatorStrategy,若創(chuàng)建對(duì)象失敗再采用StdInstantiatorStrategy妖混。
類注冊(cè)
當(dāng)kryo寫一個(gè)對(duì)象的實(shí)例的時(shí)候轮洋,默認(rèn)需要將類的完全限定名稱寫入。將類名一同寫入序列化數(shù)據(jù)中是比較低效的祥楣,所以kryo支持通過(guò)類注冊(cè)進(jìn)行優(yōu)化汉柒。
kryo.register(SomeClassA.class);
kryo.register(SomeClassB.class);
kryo.register(SomeClassC.class);
注冊(cè)會(huì)給每一個(gè)class一個(gè)int類型的Id相關(guān)聯(lián),這顯然比類名稱高效兽间,但同時(shí)要求反序列化的時(shí)候的Id必須與序列化過(guò)程中一致正塌。這意味著注冊(cè)的順序非常重要恤溶。
但是由于現(xiàn)實(shí)原因咒程,同樣的代碼讼育,同樣的Class在不同的機(jī)器上注冊(cè)編號(hào)任然不能保證一致,所以多機(jī)器部署時(shí)候反序列化可能會(huì)出現(xiàn)問(wèn)題饥瓷。
所以kryo默認(rèn)會(huì)禁止類注冊(cè)痹籍,當(dāng)然如果想要打開這個(gè)屬性,可以通過(guò)kryo.setRegistrationRequired(true);
打開刺洒。
循環(huán)引用
這是對(duì)循環(huán)引用的支持吼砂,可以有效防止棧內(nèi)存溢出,kryo默認(rèn)會(huì)打開這個(gè)屬性因俐。當(dāng)你確定不會(huì)有循環(huán)引用發(fā)生的時(shí)候周偎,可以通過(guò)kryo.setReferences(false);
關(guān)閉循環(huán)引用檢測(cè)蓉坎,從而提高一些性能。
可變長(zhǎng)存儲(chǔ)
kryo對(duì)int和long類型都采用了可變長(zhǎng)存儲(chǔ)的機(jī)制蛉艾,以int為例勿侯,一般需要4個(gè)字節(jié)去存儲(chǔ),而對(duì)kryo來(lái)說(shuō)助琐,可以通過(guò)1-5個(gè)變長(zhǎng)字節(jié)去存儲(chǔ)兵钮,從而避免高位都是0的浪費(fèi)舌界。
最多需要5個(gè)字節(jié)存儲(chǔ)是因?yàn)楹铰蓿谧冮L(zhǎng)存儲(chǔ)int過(guò)程中粥血,一個(gè)字節(jié)的8位用來(lái)存儲(chǔ)有效數(shù)字的只有7位酿箭,最高位用于標(biāo)記是否還需讀取下一個(gè)字節(jié),1表示需要缔御,0表示不需要妇蛀。
在對(duì)string的存儲(chǔ)中也有變長(zhǎng)存儲(chǔ)的應(yīng)用,string序列化的整體結(jié)構(gòu)為length+內(nèi)容眷茁,那么length也會(huì)使用變長(zhǎng)int寫入字符的長(zhǎng)度纵诞。
如果使用緩存
在實(shí)際開發(fā)中,class增刪字段是很常見的事情浙芙,但對(duì)于kryo來(lái)說(shuō),確是不支持的纸俭,而如果恰好需要使用緩存南窗,那么這個(gè)問(wèn)題會(huì)被放得更大。
例如一個(gè)對(duì)象使用kryo序列化后女轿,數(shù)據(jù)放入了緩存中壕翩,而這時(shí)候如果這個(gè)對(duì)象增刪了一個(gè)屬性放妈,那么緩存中反序列化的時(shí)候就會(huì)報(bào)錯(cuò)荐操。所以頻繁使用緩存的場(chǎng)景珍策,可以盡量避免kryo。
不過(guò)現(xiàn)在的Kryo提供了兼容性的支持屯耸,使用CompatibleFieldSerializer.class蹭劈,在kryo.writeClassAndObject時(shí)候?qū)懭氲男畔⑷缦?
class name|field length|field1 name|field2 name|field1 value| filed2 value
而在讀入kryo.readClassAndObject時(shí)铺韧,會(huì)先讀入field names,然后匹配當(dāng)前反序列化類的field和順序再構(gòu)造結(jié)果塔逃。
當(dāng)然如果在做好緩存隔離的情況下料仗,這一切都不用在意。