閱讀119 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Kryo簡介及代碼閱讀筆記

更新:2012-08-01

版本 2.16長時間運行可能會導致OOM,版本2.18有bug,不能正確序列化map和collection。

真是悲劇,所用的每一個版本都有bug。不過從代碼來看,作者有時的確比較隨便。。測試用例也少。。(比起msgpack少多了)

========================================

Kryo官方網站:https://code.google.com/p/kryo/

優點:

    速度快!見https://github.com/eishay/jvm-serializers/wiki/Staging-Results

    支持互相引用,比如類A引用類B,類B引用類A,可以正確地反序列化。

    支持多個引用同一個對象,比如多個類引用了同一個對象O,隻會保存一份O的數據。

    支持一些有用的注解,如@Tag@Optional

    支持忽略指定的字段。

    支持null

    代碼入侵少

    代碼比較簡法(比起msgpack,少得多)

缺點:

    bug多 2.12,2.14都有bug

    文檔比較少,有些使用方法要看代碼才能理解,最新版2.14bug,不能正確反序列化map類型。

    不是跨語言的解決方案

    貌似每一個類都要注冊下,不然隻能用writeClassAndObject和readClassAndObject函數。

    類要有一個空的構造函數,不然不能序列化。

    (如果這個構造函數裏調用了別的資源,而這個資源沒有初始化,那麼就悲劇了。)

    可以通過實現KryoSerializable接口來避免這個問題。。同樣不能解決這個問題

   Java自帶的則不用調用這個構造函數。

       msgpack同樣有這個問題。


接口:

    KryoSerializable

    KryoCopyable


實現忽略指定的字段

  1. 使用transient關鍵字

  2. 使用Context結合@Optional注解

  3. 使用@Tag注解(很麻煩)

  4. 實現KryoSerializable接口(比較麻煩,相當於手寫代碼)


引用:

可以用setReferences(boolean )函數來設置,默認是打開的。

引用的實現原理:

原本以為要實現引用是個比較麻煩的事,因為一想到引用,頭腦中就出現一個圖。。但在看了代碼後,發現是比較簡單的。

在Kryo類中有一個writtenObjects的ArrayList,記錄已寫入的對象。有一個readObjects來記錄已寫入的對象。

另外有個depth來記錄深度,每寫一個對象時depth++,當寫完時depth--,當depth == 0時,調用reset函數,清空writtenObjects和

比如寫一個大對象,這個對象有很多成員,每一個成員都是一個對象,而成員之間有可能用引用關係(A引用了B,B也引用了A)。

    private int depth, maxDepth = Integer.MAX_VALUE, nextRegisterID;
private final ArrayList writtenObjects = new ArrayList();
private final ArrayList readObjects = new ArrayList();

每當寫一個對象時,都到裏麵去檢查有沒有這個對象,如果有的話,就隻寫一個int即可。這個int是要表明這個對象當前在的位置即可。

因為當反序列化時,可以據讀到的int,正確地從readObjects取回對象。

如果沒有,則在輸出流中寫入writtenObjects的size()+1,再把這個對象放到writtenObjects中。


序列化時,寫入引用的對象在writtenObjects中的位置:

    for (int i = 0, n = writtenObjects.size(); i < n; i++) {
    if (writtenObjects.get(i) == object) {
        if (DEBUG)
            debug("kryo", "Write object reference " + i + ": "
                    + string(object));
        output.writeInt(i + 1, true); // + 1 because 0 means null.
        return true;
    }
}
// Only write the object the first time encountered in object graph.
output.writeInt(writtenObjects.size() + 1, true);
writtenObjects.add(object);

反序列化時,據id從readObjects得到正確的對象:

    if (--id < readObjects.size()) {
    Object object = readObjects.get(id);
    if (DEBUG)
        debug("kryo", "Read object reference " + id + ": "
                + string(object));
    return object;
}



注冊:

貌似每一個類都要注冊下,不然隻能用writeClassAndObject和readClassAndObject函數。

注冊的順序不能亂!!因為是每一個類都有一個id,而這個id是增長的!

可以設置registrationRequired,防止沒有注冊的情況!


注解annotation:

貌似隻有四個:@Optional,@Tag,@DefaultSerializer,@NotNull


實現原理:

每注冊一個類,都有一個id,由一個IntMap的hashMap來保存(TODO,研究下這個東東的實現)


代碼閱讀筆記:

在Kryo類中有以下的成員,簡單來看,就是一些HashMap,用來存放id和Class,Class和id,id和Registration,Class和Registration之間的對應關係

    private final IntMap<Registration> idToRegistration = new IntMap();
private final ObjectMap<Class, Registration> classToRegistration = new ObjectMap();
private final IdentityObjectIntMap<Class> classToNameId = new IdentityObjectIntMap();
private final IntMap<Class> nameIdToClass = new IntMap();
private int nextNameId;  //不斷增長,分新的Class分配一個新的id,即Registration中的id


每一個類對應一個Registration:

    public class Registration {
    private final Class type;
    private final int id;         //這個要注意,每一個類都有一個唯一的id,這個id是從0開始不斷增長的
    private Serializer serializer;
    private ObjectInstantiator instantiator;  //複製對象時候用
}

直接看這個類的成員,就大概能明白它是怎樣回事了。

要注意一點,Registration中的id很重要,可以說是和別的序列化方案相比,高效之處。

在調用Kryo.writeClass(Output output, Class type)函數時,

先查找到這個類的Registration,得到Serializer,再調用write (Kryo kryo, Output output, T object)寫到輸出流中。


如果沒有找到的話,則為這個類生成一個Registration,並放到Kryo類中的對應的HashMap中。

再來說下Serializer

默認是FieldSerializer,在生成Registration中,如果為這個類找不到Serializer(到defaultSerializers中找),

則會構造一個FieldSerializer。

FieldSerializer實際是有一個數組存放了每一個field的信息,當調用write (Kryo kryo, Output output, T object)函數時,則曆遍所有的field,把每一個field寫到輸出流中。

    private CachedField[] fields = new CachedField[0];
                                                     
public class CachedField<X> {
    final Field field;
    Class fieldClass;
    Serializer serializer;
    boolean canBeNull;
    int accessIndex = -1;
}


Kryo有兩種模式,一種是先注冊(regist),再寫對象,即writeObject函數,實際上如果不先注冊,在寫對象時也會注冊,並為class分配一個id。

注意,如果是rpc,則必須兩端都按同樣的順序注冊,否則會出錯,因為必須要明確類對應的唯一id。


另一種是寫類名及對象,即writeClassAndObject函數。

writeClassAndObject函數是先寫入(-1 + 2)(一個約定的數字),再寫入類ID(第一次要先寫-1,再寫類ID + 類名),寫入引用關係(見引用的實現),最後才寫真正的數據)。

注意每一次writeClassAndObject調用後信息都會清空,所以不用擔心和client交互時會出錯。


代碼中其它有意思的地方:

在writeString函數中先判斷是不是ascii即,所有都<127,如果是在寫string的最後一個字符,會執行這個:

    buffer[position - 1] |= 0x80;

不知為何。。

在readString的時候也會判斷:

    if ((b & 0x80) == 0) {
// ascii


是因為這個https://topic.csdn.net/t/20040416/10/2972001.html ?所謂的雙字節的?


為什麼比其它的序列化方案要快?

為每一個類分配一個id

實現了自己的IntMap

代碼中一些取巧的地方:

利用變量memoizedRegistration和memoizedType記錄上一次的調用writeObject函數的Class,則如果兩次寫入同一類型時,可以直接拿到,不再查找HashMap。

這個也許是為什麼在測試中kryo要比其它類庫要快的原因之一。


注意事項:

實現KryoSerializable接口

像下麵這樣實現是錯誤的。

    @Override
public void write(Kryo kryo, Output output) {
    // TODO Auto-generated method stub
    kryo.writeObject(output, this);
}
                             
@Override
public void read(Kryo kryo, Input input) {
    // TODO Auto-generated method stub
    kryo.readObject(input, this.getClass());
}

實際上隻寫入了默認構造函數的內容!

原因是在生成Registration時已在writtenObjects中寫入了這個類,所以在Kryo類中的writtenObjects中已有這個類,所以在調用write函數時,如果是用下麵的代碼,則會以為這個類是已寫過的,所以直接寫了一個1和它的id!!

實際上如果實現了KryoSerializable接口,最終是這個類來調用接口的write函數:KryoSerializableSerializer

正確的寫法是寫入每一個成員,在read函數中把數據讀出,再賦值給成員。



最後更新:2017-04-02 16:47:52

  上一篇:go J2EE中在web.xml異常頁麵跳轉
  下一篇:go sqlite3數據庫使用