kryo 各数据类型的序列化编码机制(揭晓为什么高效的原理)
用过 dubbo 的开发人员,在选取序列化时都会根据“经验”来选 kryo 为序列化框架,其原因是序列化协议非常高效,超过 java 原生序列化协议、hessian2 协议,那 kryo 为什么高效呢?
序列化协议,所谓的高效,通常应该从两方面考虑:
- 序列化后的二进制序列大小。
- 序列化、反序列化的速率。
> 本节将重点探讨,kryo在减少序列化化二进制流上做的努力。
序列化:将各种数据类型(基本类型、包装类型、对象、数组、集合)等序列化为 byte 数组的过程。
反序列化:将 byte 数组转换为各种数据类型(基本类型、包装类型、对象、数组、集合)。
java 中定义的数据类型所对应的序列化器 在Kryo 的构造函数中构造,其代码截图:
接下来将详细介绍java常用的数据类型的序列化机制,即Kryo是如何编码二进制流。
1、DefaultSerializers$IntSerializer
int类型序列化
static public class IntSerializer extends Serializer<integer> { { setImmutable(true); } public void write (Kryo kryo, Output output, Integer object) { output.writeInt(object, false); } public Integer read (Kryo kryo, Input input, Class<integer> type) { return input.readInt(false); } }
1.1 Integer ---> byte[] (序列化)
Output#writeInt
public int writeInt (int value, boolean optimizePositive) throws KryoException { // @1 return writeVarInt(value, optimizePositive); // @2 }
代码@1:boolean optimizePositive,是否优化绝对值。如果 optimizePositive: false,则会对value进行移位运算,如果是正数,则存放的值为原值的两倍,如果是负数的话,存放的值为绝对值的两倍减去一,其算法为:value = (value << 1) ^ (value >> 31),在反序列化时,通过该算法恢复原值:((result >>> 1) ^ -(result & 1))。
代码@2:调用writeVarInt,采用变长编码来存储int而不是固定4字节。
Output#writeVarInt
public int writeVarInt (int value, boolean optimizePositive) throws KryoException { if (!optimizePositive) value = (value << 1) ^ (value >> 31); if (value >>> 7 == 0) { // @1 require(1); buffer[position++] = (byte)value; return 1; } if (value >>> 14 == 0) { // @2 require(2); buffer[position++] = (byte)((value & 0x7F) | 0x80); buffer[position++] = (byte)(value >>> 7); return 2; } if (value >>> 21 == 0) { require(3); buffer[position++] = (byte)((value & 0x7F) | 0x80); buffer[position++] = (byte)(value >>> 7 | 0x80); buffer[position++] = (byte)(value >>> 14); return 3; } if (value >>> 28 == 0) { require(4); buffer[position++] = (byte)((value & 0x7F) | 0x80); buffer[position++] = (byte)(value >>> 7 | 0x80); buffer[position++] = (byte)(value >>> 14 | 0x80); buffer[position++] = (byte)(value >>> 21); return 4; } require(5); buffer[position++] = (byte)((value & 0x7F) | 0x80); buffer[position++] = (byte)(value >>> 7 | 0x80); buffer[position++] = (byte)(value >>> 14 | 0x80); buffer[position++] = (byte)(value >>> 21 | 0x80); buffer[position++] = (byte)(value >>> 28); return 5; }
其思想是采取变长字节来存储 int 类型的数据,int 在 java 是固定 4 字节,由于在应用中,一般使用的 int 数据都不会很大,4 个字节中,存在高位字节全是存储 0 的情况,故 kryo 为了减少在序列化流中的大小,尽量按需分配,kryo 采用 1 - 5 个字节来存储 int 数据,为什么 int 类型在 JAVA 中最多 4 个字节,为什么变长 int 可能需要 5 个字节才能存储呢?这与变长字节需要标志位有关,下文根据代码来推测 kryo 关于 int 序列化 byte 数组的编码规则。
代码@1:value >>> 7 == 0 ,一个数字,无符号右移(高位补0) 7 位后为 0,说明该数字只占一个字节,并且高两位必须为 0,也就是该数字的范围在 0-127(2^7 -1), 对于字节的高位,低位的说明如下:
如果该值范围为 0-127 则使用 1 个字节存储 int 即可。在操作缓存区时 buffer[position++] = (byte)value,需要向 Output 的缓存区申请 1 个字节的空间,然后进行赋值,并返回本次申请的存储空间,对于 require 方法在 Byte[]、String 序列化时重点讲解,包含缓存区的扩容,Output 与输出流结合使用时的相关机制。
代码@2:value >>> 14 == 0,如果数字的范围在 0 到 2^14-1 范围之间,则需要两个字节存储,这里为什么是 14,其主要原因是,对于一个字节中的 8 位,kryo 需要将高位用来当标记位,用来 标识是否还需要读取下一个字节。1:表示需要,0:表示不需要,也就是一个数据的结束。在变长 int 存储过中,一个字节 8 位 kryo 可用来存储数字有效位为 7 位 。
举例演示一下: kryo 两字节能存储的数据的特点是高字节中前两位为 0,例如: 0011 1011 0 010 1001 其存储方式为 buffer[0] = 先存储最后字节的低 7 位,010 1001 ,然后第一位之前,加 1,表示还需要申请第二个字节来存储。此时buffer[0] = 1010 1001 buffer[1] = 存储 011 1011 0(这个0是原第一个字节未存储的部分) ,此时buffer[1]的8位中的高位为0,表示存储结束。
下图展示了kryo用2个字节存储一个int类型的数据的示意图。
同理,用3个字节可以表示2^21 -1。 kryo使用变长字节(1-5)个字节来存储int类型(java中固定占4字节)。
1.2 int反序列化(byte[] ---> int)
反序列化就是根据上述编码规则,将 byte[] 序列化为 int 数字。 buffer[0] = 低位,buffer[1] 高位, 具体解码实现为:Input#readVarInt
/** Reads a 1-5 byte int. It is guaranteed that a varible length encoding will be used. */ public int readVarInt (boolean optimizePositive) throws KryoException { if (require(1) < 5) return readInt_slow(optimizePositive); int b = buffer[position++]; int result = b & 0x7F; if ((b & 0x80) != 0) { byte[] buffer = this.buffer; b = buffer[position++]; result |= (b & 0x7F) << 7; if ((b & 0x80) != 0) { b = buffer[position++]; result |= (b & 0x7F) << 14; if ((b & 0x80) != 0) { b = buffer[position++]; result |= (b & 0x7F) << 21; if ((b & 0x80) != 0) { b = buffer[position++]; result |= (b & 0x7F) << 28; } } } } return optimizePositive ? result : ((result >>> 1) ^ -(result & 1)); }
Input#require(count)返回的是缓存区剩余字节数(可读)。其实现思路是,一个一个字节的读取,读到第一个字节后,首先提取有效存储位的数据,buffer[ 0 ] & 0x7F,然后判断高位是否为1,如果不为1,直接返回,如果为1,则继续读取第二位buffer[1],同样首先提取有效数据位(低7位),然后对这数据向左移7位,在与buffer[0] 进行或运算。也就是,varint的存放是小端序列,越先读到的位,在整个int序列中越靠近低位。
2、String序列化
其实现类 DefaultSerializers$StringSerializer。
static public class StringSerializer extends Serializer<string> { { setImmutable(true); setAcceptsNull(true); // @1 } public void write (Kryo kryo, Output output, String object) { output.writeString(object); } public String read (Kryo kryo, Input input, Class<string> type) { return input.readString(); } }
代码@1:String 位不可变、允许为空,也就是序列化时需要考虑 String s = null 的情况。
2.1 序列化 (String ----> byte[])
Output#writeString
public void writeString (String value) throws KryoException { if (value == null) { // @1 writeByte(0x80); // 0 means null, bit 8 means UTF8. return; } int charCount = value.length(); if (charCount == 0) { // @2 writeByte(1 | 0x80); // 1 means empty string, bit 8 means UTF8. return; } // Detect ASCII. boolean ascii = false; if (charCount > 1 && charCount < 64) { // @3 ascii = true; for (int i = 0; i < charCount; i++) { int c = value.charAt(i); if (c > 127) { ascii = false; break; } } } if (ascii) { // @4 if (capacity - position < charCount) writeAscii_slow(value, charCount); else { value.getBytes(0, charCount, buffer, position); position += charCount; } buffer[position - 1] |= 0x80; } else { writeUtf8Length(charCount + 1); // @5 int charIndex = 0; if (capacity - position >= charCount) { // @6 // Try to write 8 bit chars. byte[] buffer = this.buffer; int position = this.position; for (; charIndex < charCount; charIndex++) { int c = value.charAt(charIndex); if (c > 127) break; buffer[position++] = (byte)c; } this.position = position; } if (charIndex < charCount) writeString_slow(value, charCount, charIndex); // @7 } }
首先对字符串编码成字节序列,通常采用的编码方式为 length:具体内容,通常的做法,表示字符串序列长度为固定字节,例如 4 位,那 kryo 是如何来表示的呢?请看下文分析。
代码@1:如果字符串为 null,采用一个字节来表示长度,长度为 0,并且该字节的高位填充 1,表示字符串使用 UTF-8 编码,null 字符串的最终表示为:1000 0000。
代码@2:空字符串表示,长度用 1 来表示,同样高位使用 1 填充表示字符串使用 UTF-8 编码,空字符串最终表示为:1000 0001。注:长度为 1 表示空字符串。
代码@3:如果字符长度大于 1 并且小于 64,依次检查字符,如果其 ascii 小于 127,则认为可以用 ascii 来表示单个字符,不能超过 127 的原因是,其中字节的高一位需要表示编码,0 表示 ascii,当用 ascii 编码来表示字符串是,第高 2 位需要用来表示是否结束标记。
代码@4:如果使用 ascii 编码,则单个字符,使用一个字节表示,高 1 位表示编码标记为,高 2 位表示是否结束标记。
代码@5:按照 UTF-8 编码,写入其长度,用变长 int(varint) 写入字符串长度,具体实现如下:
Output#writeUtf8Length
private void writeUtf8Length (int value) { if (value >>> 6 == 0) { require(1); buffer[position++] = (byte)(value | 0x80); // Set bit 8. } else if (value >>> 13 == 0) { require(2); byte[] buffer = this.buffer; buffer[position++] = (byte)(value | 0x40 | 0x80); // Set bit 7 and 8. buffer[position++] = (byte)(value >>> 6); } else if (value >>> 20 == 0) { require(3); byte[] buffer = this.buffer; buffer[position++] = (byte)(value | 0x40 | 0x80); // Set bit 7 and 8. buffer[position++] = (byte)((value >>> 6) | 0x80); // Set bit 8. buffer[position++] = (byte)(value >>> 13); } else if (value >>> 27 == 0) { require(4); byte[] buffer = this.buffer; buffer[position++] = (byte)(value | 0x40 | 0x80); // Set bit 7 and 8. buffer[position++] = (byte)((value >>> 6) | 0x80); // Set bit 8. buffer[position++] = (byte)((value >>> 13) | 0x80); // Set bit 8. buffer[position++] = (byte)(value >>> 20); } else { require(5); byte[] buffer = this.buffer; buffer[position++] = (byte)(value | 0x40 | 0x80); // Set bit 7 and 8. buffer[position++] = (byte)((value >>> 6) | 0x80); // Set bit 8. buffer[position++] = (byte)((value >>> 13) | 0x80); // Set bit 8. buffer[position++] = (byte)((value >>> 20) | 0x80); // Set bit 8. buffer[position++] = (byte)(value >>> 27); } }
用来表示字符串长度的编码规则(int),第 8 位(高位)表示字符串的编码,第 7 位(高位)表示是否还需要读取下一个字节,也就是结束标记,1 表示未结束,0 表示结束。一个字节共 8 位,只有低 6 位用来存放数据,varint 采取的是小端序列。
代码@6:如果当前缓存区有足够的空间,先尝试将字符串中单字节数据写入到 buffer 中,碰到第一个非单字节字符时,结束。
代码@7:将剩余空间写入缓存区,其实现方法:Output#writeString_slow(value, charCount, charIndex)
Output#writeString_slow
private void writeString_slow (CharSequence value, int charCount, int charIndex) { for (; charIndex < charCount; charIndex++) { // @1 if (position == capacity) require(Math.min(, charCount - charIndex)); // @2 int c = value.charAt(charIndex); // @3 if (c <= 0x007F) { // @4 buffer[position++] = (byte)c; } else if (c > 0x07FF) { // @5 buffer[position++] = (byte)(0xE0 | c >> 12 & 0x0F); require(2); buffer[position++] = (byte)(0x80 | c >> 6 & 0x3F); buffer[position++] = (byte)(0x80 | c & 0x3F); } else { // @6 buffer[position++] = (byte)(0xC0 | c >> 6 & 0x1F); require(1); buffer[position++] = (byte)(0x80 | c & 0x3F); } } }
代码@1:循环遍历字符的字符。
代码@2:如果当前缓存区已经写满,尝试申请(capacity 与 charCount - charIndex )的最小值,这里无需担心字符不是单字节申请 charCount - charIndex 空间不足的问题,后面我们会详细分析 require 方法,字节不够时会触发缓存区扩容或刷写到流中,再重复利用缓存区。
代码@3:int c = value.charAt(charIndex); 将字符类型转换为 int 类型,一个中文字符对应一个 int 数字,这是因为 java 使用 unicode 编码,每个字符占用 2 个字节,char 向 int 类型转换,就是将 2 字节的字节编码,转换成对应的二进制,然后用 10 进制表示的数字。
代码@4:如果值小于等 0x7F(127),直接存储在 1 个字节中,此时高位 4 个字节的范围在(0-7).]。
代码@5:如果值大于 0x07FF(二进制 0000 0111 1111 1111),第一个大于 0x7F 的值为(0000 1000 0000 0000), 即 2^12,数据有效位至少 12 位,使用 3 字节来存储,具体存储方式为:
1)buffer[0] :buffer[position++] = (byte)(0xE0 | c >> 12 & 0x0F); 首先将 c 右移 12 位再与 0x0F 进行与操作,其意义就是先提取 c 的第 16-13(4位的值),并与 0xE0 取或,最终的值为 0xE (16-13) 位的值,从 Input 读取字符串可以看出,是根据 0xE0 作为存储该字符需要 3 个字节的依据,并且只取 16-13 位的值作为其高位的有效位,也就是说字符编码的值,不会超过 0XFFFF,也就是两个字节(正好与java unicode编码吻合)。
2)buffer[1]:存储第 12-7(共6位),c >> 6 & 0x3F,然后与 0X80 进行或,高位设置为 1,表示 UTF-8 编码,其实再反序列化时,这个高位设置为 1,未有实际作用。
3)buffer[2]:存储第 6-1(共6位),0x80 | c & 0x3F,同样高位置 1。
2.2 字符串反序列化 (byte[] ----> String)
在讲解反序列化时,总结一下String序列化的编码规则
String 序列化规则:String 序列化的整体结构为 length + 内容,注意,这里的 length 不是内容字节的长度,而是 String 字符的长度。
- 如果是 null,则用 1 个字节表示,其二进制为 1000 0000。
- 如果是""空字符串,则用 1 个字节表示,其二进制为 1000 0001。
- 如果字符长度大于 1 且小于 64,并且字符全是 ascii 字符(小等于127),则每个字符用一个字节表示,最后一个字节的高位置 1,表示 String字符的结束。【优化点,如果是 ascii 字符,编码时不需要使用 length+内容的方式,而是直接写入内容】
- 如果不满足上述条件,则需要使用 length + 内容的方式。
-
用一个变长int写入字符的长度,每一字节,高两位分别为 编码标记(1:utf8)、是否结束标记(1:否;0:结束)
2)将内容用utf-8编码写入字节序列中,utf8,用变长字节(1-3)个字节表示一个字符(英文、中文)。每一个字节,使用6为,高两位为标志位。【16位】
- 3字节的存储为 【4位】 + 【6位】 + 【6位】,根据第一个字节高4位判断得出 需要几个字节来存储一个字符。
其反序列化的入口为 Input#readString,就是按照上述规则进行解析即可,就不深入探讨了,有兴趣的话,可以自己去指定地方查阅。
3、boolean类型序列化
实现类为 DefaultSerializers$BooleanSerializer,序列化:使用 1 个字节存储 boolean 类型,如果为 true,则写入 1,否则写入 0。
4、byte类型序列化
实现类为:DefaultSerializers$ByteSerializer,序列化:直接将 byte 写入字节流中即可。
5、char类型序列化
实现类为:DefaultSerializers$CharSerializer
Output#writeChar
/** Writes a 2 byte char. Uses BIG_ENDIAN byte order. */ public void writeChar (char value) throws KryoException { require(2); buffer[position++] = (byte)(value >>> 8); buffer[position++] = (byte)value; }
序列化:char 在 java 中使用 2 字节存储(unicode), kryo 在序列化时,按大端字节的顺序,将 char 写入字节流
6、short类型序列化
实现类为 DefaultSerializers$ShortSerializer Output#writeShort
/** Writes a 2 byte short. Uses BIG_ENDIAN byte order. */ public void writeShort (int value) throws KryoException { require(2); buffer[position++] = (byte)(value >>> 8); buffer[position++] = (byte)value; }
序列化:与char类型序列化一样,采用大端字节顺序存储。
7、long类型序列化
实现类为:DefaultSerializers$LongSerializer
Output#writeLong
public int writeLong (long value, boolean optimizePositive) throws KryoException { return writeVarLong(value, optimizePositive); }
序列化:采取变长字节(1-9)位来存储 long,其编码规则与 int 变长类型一致,每个字节的高位用来表示是否结束,1:表示还需要继续读取下一个字节,0:表示结束。
8、float类型序列化
实现类为:DefaultSerializers$FloatSerializer
/** Writes a 4 byte float. */ public void writeFloat (float value) throws KryoException { writeInt(Float.floatToIntBits(value)); } /** Writes a 4 byte int. Uses BIG_ENDIAN byte order. */ public void writeInt (int value) throws KryoException { require(4); byte[] buffer = this.buffer; buffer[position++] = (byte)(value >> 24); buffer[position++] = (byte)(value >> 16); buffer[position++] = (byte)(value >> 8); buffer[position++] = (byte)value; }
序列化:首先将 float 按照 IEEE 754 编码标准,转换为 int 类型,然后按大端序列,使用固定长度 4 字节来存储 float,这里之所以不使用变长字节来存储 float,是因为使用 Float.floatToIntBits(value) 产生的值,比较大,基本都需要使用 4 字才能存储,如果使用变长字节,则需要 5 字节,反而消耗的存储空间更大。
9、DefaultSerializers$DoubleSerializer
Output#writeDouble 序列化:首先将 Double 按照 IEEE 754 编码标准转换为 Long,然后才去固定 8 字节存储。 到目前为止,介绍了8种基本类型(boolean、byte、char、short、int、float、long、double)和 String 类型的序列化与反序列化。
10、BigInteger序列化实现类为:DefaultSerializers$BigIntegerSerializer
/** Writes an 8 byte double. */ public void writeDouble (double value) throws KryoException { writeLong(Double.doubleToLongBits(value)); } /** Writes an 8 byte long. Uses BIG_ENDIAN byte order. */ public void writeLong (long value) throws KryoException { require(8); byte[] buffer = this.buffer; buffer[position++] = (byte)(value >>> 56); buffer[position++] = (byte)(value >>> 48); buffer[position++] = (byte)(value >>> 40); buffer[position++] = (byte)(value >>> 32); buffer[position++] = (byte)(value >>> 24); buffer[position++] = (byte)(value >>> 16); buffer[position++] = (byte)(value >>> 8); buffer[position++] = (byte)value; }
BigInteger 序列化实现,整体格式与 String 类型一样,由 length + 内容构成。
- 如果为 null,则写入一个字节,其值为 0,表示长度为 0。
- 如果为 BigInteger.ZERO,则长度写入 2,随后再写入 1 个字节的内容,字节内容为 0,表示 ZERO。
- 将 BigInteger 转换成 byte[] 数组,首先写入长度 =( byte数组长度 + 1),然后写入 byte 数组的内容即可。
11、BigDecimal序列化
实现类为:DefaultSerializers$BigDecimalSerializer
BigDecimal 的序列化与 BigInteger 一样,首先是通过 BigDecimal#unscaledValue 方法返回对应的 BigInteger,然后序列化,在反序列化时通过 BigInteger 创建对应的 BigDecimal 即可。
12、Class实例序列化
实现类为:DefaultSerializers$ClassSerializer
public void write (Kryo kryo, Output output, Class object) { kryo.writeClass(output, object); // @1 output.writeByte((object != null && object.isPrimitive()) ? 1 : 0); // @2 }
代码@1:调用 Kryo 的 writeClass 方法序列化 Class 实例。 代码@2:写入是否是包装类型(针对8种基本类型)。
接下来我们重点分析Kryo#writeClass
public Registration writeClass (Output output, Class type) { if (output == null) throw new IllegalArgumentException("output cannot be null."); try { return classResolver.writeClass(output, type); // @1 } finally { if (depth == 0 && autoReset) reset(); // @2 } }
代码@1:首先调用 ClassResolver.wreteClass 方法。 代码@2:完成一次写入后,需要重置 Kryo 中的临时数据结构,这也就是 kryo 实例非线程安全的原因,其中几个重要的数据结构会再 ClassResolver.writeClass 中详细说明。
DefaultClassResolver#writeClass
public Registration writeClass (Output output, Class type) { if (type == null) { // @1 if (TRACE || (DEBUG && kryo.getDepth() == 1)) log("Write", null); output.writeVarInt(Kryo.NULL, true); return null; } Registration registration = kryo.getRegistration(type); // @2 if (registration.getId() == NAME) // @3 writeName(output, type, registration); else { if (TRACE) trace("kryo", "Write class " + registration.getId() + ": " + className(type)); output.writeVarInt(registration.getId() + 2, true); // @4 } return registration; }
代码@1:如果 type 为 null,则存储 Kryo.NULL(0),使用变长 int 来存储,0 在变长 int 中占用 1 个字节。
代码@2:根据 type 从 kryo 获取类注册信息,如果有调用 kryo#public Registration register (Class type)方法,则会返回其注册关系。
代码@3:如果不存在注册关系,则需要将类型的全名写入。
代码@4:如果存在注册关系,则 registration.getId() 将不等于 Kryo.NAME(-1),则将(registration.getId() + 2)使用变长 int 写入字节流即可。
从这里看出,如果将类预先注册到 kryo 中,序列化字节流将变的更小,所谓的 kryo 类注册机制就是将字符串的类全路径名替换为数字,但数字的分配与注册顺序相关,所有,如果要使用类注册机制,必须在 kryo 对象创建时首先注册,确保注册顺序一致。
接下来重点分析一下 writeName 方法
DefaultClassResolver#writeName
protected void writeName (Output output, Class type, Registration registration) { output.writeVarInt(NAME + 2, true); // @1 if (classToNameId != null) { // @2 int nameId = classToNameId.get(type, -1); / if (nameId != -1) { // if (TRACE) trace("kryo", "Write class name reference " + nameId + ": " + className(type)); output.writeVarInt(nameId, true); return; } } // Only write the class name the first time encountered in object graph. if (TRACE) trace("kryo", "Write class name: " + className(type)); int nameId = nextNameId++; // @3 if (classToNameId == null) classToNameId = new IdentityObjectIntMap(); // @4 classToNameId.put(type, nameId); // @5 output.writeVarInt(nameId, true); // @6 output.writeString(type.getName()); // @7 }
代码@1:由于是要写入类的全路径名,故首先使用变长 int 编码写入一个标记,表示是存储的类名,而不是一个 ID。其标志位为 NAME + 2 = 1。存储 0 表示 null。
代码@2:如果 classToNameId 不为空(IdentityObjectIntMap< Class>),根据 type 获取 nameId,如果不为空并且从缓存中能获取到 nameId,则直接写入 nameId,而不是写入类名,这里指在一次序列化过程中,同一个类名例如(cn.uce.test.Test)只写入一次,其他级联(重复)出现时,为其分配一个 ID,进行缓存,具体可以从下面的代码中得知其意图。
代码@3:首先分配一全局递增的 nameId。
代码@4:如果 classToNameId 为空,则创建一个实例。
代码@5:将 type 与 nameId 进行缓存。
代码@6:写入 nameId。 代码@7:写入 type 的全路径名。
注意 Kryo#writeClass,一次序列化 Class 实例后会调用 reset 方法,最终会清除本次 classToNameId ,classToNameId 并不能做一个全据的缓存的主要原因是,在不同的 JVM 虚拟机中,同一个class type 对应的 nameId 不一定相同,故无法实现共存,只能是作为一个优化,在一次类序列化中,如果存在同一个类型,则第一个写入类全路径名,后面出现的则使用 id(int) 来存储,节省空间。
为了加深上述理解,我们再来看一下 Class 实例的反序列化:
DefaultClassResolver#readClass
public Registration readClass (Input input) { int classID = input.readVarInt(true); // @1 switch (classID) { case Kryo.NULL: // @2 if (TRACE || (DEBUG && kryo.getDepth() == 1)) log("Read", null); return null; case NAME + 2: // Offset for NAME and NULL. // @3 return readName(input); } if (classID == memoizedClassId) return memoizedClassIdValue; Registration registration = idToRegistration.get(classID - 2); if (registration == null) throw new KryoException("Encountered unregistered class ID: " + (classID - 2)); if (TRACE) trace("kryo", "Read class " + (classID - 2) + ": " + className(registration.getType())); memoizedClassId = classID; memoizedClassIdValue = registration; return registration; }
代码@1:首先读取一个变长 int。
代码@2:如果为 Kryo.NULL 表示为 null,直接返回 null 即可。
代码@3:如果为NAME + 2 则表示为存储的是类的全路径名,则调用 readName 解析类的名字。
代码@4:如果不为上述值,说明存储的是类型对应的ID值,也就是使用了类注册机制。 之所以 idToRegistration.get(classID - 2),是因为在存储时就是 nameId + 2。因为,0(代表null),1:代表按类全路径名存储,nameId 是从 3 开始存储。 接下来再重点看一下 readName 的实现:
DefaultClassResolver#readName
protected Registration readName (Input input) { int nameId = input.readVarInt(true); if (nameIdToClass == null) nameIdToClass = new IntMap(); Class type = nameIdToClass.get(nameId); if (type == null) { // Only read the class name the first time encountered in object graph. String className = input.readString(); type = getTypeByName(className); if (type == null) { try { type = Class.forName(className, false, kryo.getClassLoader()); } catch (ClassNotFoundException ex) { if (WARN) warn("kryo", "Unable to load class " + className + " with kryo's ClassLoader. Retrying with current.."); try { type = Class.forName(className); } catch (ClassNotFoundException e) { throw new KryoException("Unable to find class: " + className, ex); } } if (nameToClass == null) nameToClass = new ObjectMap(); nameToClass.put(className, type); } nameIdToClass.put(nameId, type); if (TRACE) trace("kryo", "Read class name: " + className); } else { if (TRACE) trace("kryo", "Read class name reference " + nameId + ": " + className(type)); } return kryo.getRegistration(type); }
首先读取类的 id,因为在序列化类时,如果序列化字符串时,首先先用变长 int 存储类型的 nameId,然后再序列化类的全路径名,这样在一次反序列化时,第一次序列化时,将全列的全路径使用 Class.forName 实例化对象后,然后存储在局部方法缓存中(IntMap)中,在这一次序列化时再碰到同类型时,则根据 id 则可以找到对象。
Class实例序列化总结:
Class实例序列化需求:序列化类的全路径名,反序列化时根据 Class.forName 生成对应的实例。
kryo序列化Class实例的编码规则:
-
如果为 null,用变长 int,实际使用 1 个字节,存储值为 0。
-
如果该类通过类注册机制注册到 kryo 时,则序列化 (nameId + 2),用变长 int 存储。
-
如果该类未通过类注册机制注册到 kryo,在一次序列化过程中(包含级联)时,类型第一次出现时,会分配一个 nameId,将 nameId + type 全路径序列化,后续再出现该类型,则只序列化 nameId 即可。
13、DefaultSerializers$DateSerializer
java.Util.Date、java.sql.Date等序列化时,只需序列化 Date#getTime() 返回的 long 类型,反序列化时根据 long 类型创建对应的实例即可。long 类型的编码使用变长 long 格式进行序列化。
14、枚举类型Enum序列化
实现类为:DefaultSerializers$EnumSerializer
static public class EnumSerializer extends Serializer<enum> { { setImmutable(true); setAcceptsNull(true); } private Object[] enumConstants; public EnumSerializer (Class<!--? extends Enum--> type) { enumConstants = type.getEnumConstants(); if (enumConstants == null) throw new IllegalArgumentException("The type must be an enum: " + type); } public void write (Kryo kryo, Output output, Enum object) { if (object == null) { output.writeVarInt(NULL, true); return; } output.writeVarInt(object.ordinal() + 1, true); } public Enum read (Kryo kryo, Input input, Class<enum> type) { int ordinal = input.readVarInt(true); if (ordinal == NULL) return null; ordinal--; if (ordinal < 0 || ordinal > enumConstants.length - 1) throw new KryoException("Invalid ordinal for enum \"" + type.getName() + "\": " + ordinal); Object constant = enumConstants[ordinal]; return (Enum)constant; } }
枚举类型序列化(支持null):
- 如果为null,则使用变长int,实际用一个字节存储0。
- 如果不为null,使用变长int,存储object.ordinal()+1,也就是序列化该值在枚举类型常量数组中的下标,由于0代表为空,则下标从1开始。
在反序列化时,通过Enum.class.getEnumConstants()获取枚举类型的常量数组,然后从二进制流中获取下标即可。-
15、EnumSet 类型序列化
实现类为:DefaultSerializers$EnumSetSerializer
static public class EnumSetSerializer extends Serializer<enumset> { public void write (Kryo kryo, Output output, EnumSet object) { Serializer serializer; if (object.isEmpty()) { // @1 EnumSet tmp = EnumSet.complementOf(object); // @2 if (tmp.isEmpty()) throw new KryoException("An EnumSet must have a defined Enum to be serialized."); serializer = kryo.writeClass(output, tmp.iterator().next().getClass()).getSerializer(); // @3 } else { serializer = kryo.writeClass(output, object.iterator().next().getClass()).getSerializer(); } output.writeInt(object.size(), true); // @4 for (Object element : object) // @5 serializer.write(kryo, output, element); } public EnumSet read (Kryo kryo, Input input, Class<enumset> type) { Registration registration = kryo.readClass(input); EnumSet object = EnumSet.noneOf(registration.getType()); Serializer serializer = registration.getSerializer(); int length = input.readInt(true); for (int i = 0; i < length; i++) object.add(serializer.read(kryo, input, null)); return object; } public EnumSet copy (Kryo kryo, EnumSet original) { return EnumSet.copyOf(original); } }
EnumSet 是一个专为枚举设计的集合类,EnumSet 中的所有元素都必须是指定枚举类型的枚举值。在序列化 EnumSet 时,需要将 EnumSet 中存储的枚举类型进行序列化,然后再序列每一个枚举值。
序列化过程:
代码@1:如果序列化的 EnumSet 为空,则通过代码 EnumSet.complementOf 方法创建一个其元素类型与指定 EnumSet 里元素类型相同的 EnumSet 集合,新 EnumSet 集合包含原 EnumSet 集合所不包含的、此类枚举类剩下的枚举值(即新 EnumSet 集合和原 EnumSet 集合的集合元素加起来是该枚举类的所有枚举值)。-
代码@3:首先序列化EnumSet中的枚举类型Class实例,并获取枚举类型对应的序列器。
代码@4:序列化EnumSet中元素的个数。
代码@5:逐一序列化EnumSet中元素(一个个枚举值)。
16、StringBuffer序列化
实现类为DefaultSerializers$StringBufferSerializer,序列化:与 String 序列化一致。
17、StringBuilder序列化
实现类为DefaultSerializers$StringBuilderSerializer,序列化:与 String 序列化一致。
18、TreeMap序列化
实现类为:DefaultSerializers$TreeMapSerializer
static public class TreeMapSerializer extends MapSerializer { public void write (Kryo kryo, Output output, Map map) { TreeMap treeMap = (TreeMap)map; kryo.writeClassAndObject(output, treeMap.comparator()); super.write(kryo, output, map); } // ...省略部分代码 }
TreeMap的序列,首先,先序列化 TreeMap 的比较器,然后再序列化 TreeMap 中的数据。
序列化数据请看 MapSerializer MapSerializer#write
public void write (Kryo kryo, Output output, Map map) { int length = map.size(); output.writeInt(length, true); Serializer keySerializer = this.keySerializer; if (keyGenericType != null) { if (keySerializer == null) keySerializer = kryo.getSerializer(keyGenericType); keyGenericType = null; } Serializer valueSerializer = this.valueSerializer; if (valueGenericType != null) { if (valueSerializer == null) valueSerializer = kryo.getSerializer(valueGenericType); valueGenericType = null; } for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) { Entry entry = (Entry)iter.next(); if (keySerializer != null) { if (keysCanBeNull) kryo.writeObjectOrNull(output, entry.getKey(), keySerializer); else kryo.writeObject(output, entry.getKey(), keySerializer); } else kryo.writeClassAndObject(output, entry.getKey()); if (valueSerializer != null) { if (valuesCanBeNull) kryo.writeObjectOrNull(output, entry.getValue(), valueSerializer); else kryo.writeObject(output, entry.getValue(), valueSerializer); } else kryo.writeClassAndObject(output, entry.getValue()); } }
其序列化方法就是遍历 Map 中的元素,调用 Kryo#writeClassAndObject 进行序列化,Kryo#writeClassAndObject 涉及到 Kryo 整个序列化流程,将在下节介绍。
本节就讲述到这里了,本节详细分析了 Kryo 对各种数据类型的序列化机制,其再降低序列化大小方面做了如下优化:-
-
Kryo序列化的“对象”是数据以及少量元信息,这和 JAVA 默认的序列化的本质区别,java 默认的序列化的目的是语言层面的,将类、对象的所有信息都序列化了,也就是就算是不加载 class 的定义,也能根据序列化后的信息动态构建类的所有信息。而 Kryo反序列化时,必须能加载类的定义,这样 Kryo 能节省大量的字节空间。
-
使用变长 int、变长 long 存储 int、long 类型,大大节省空间。-
-
元数据(字符串类型)使用缓存机制,重复出现的字符串使用 int 来存储,节省存储空间。
-
字符串类型使用UTF-8存储,但会使用ascii码进一步优化空间。
下一篇将重点分析 Kryo 序列化的过程,其入口函数:Kryo#writeClassAndObject。
最后,亲爱的读者朋友们,以上就是本文的全部内容了,Kryo序列化为什么这么高效是否已Get,欢迎留言讨论。原创不易,莫要白票,请你为本文点赞个吧,这将是我写作更多优质文章的最强动力。
如果觉得文章对你有点帮助,请扫描如下二维码,第一时间阅读最新推文,回复【源码】,将获得成体系剖析JAVA系主流中间件的源码分析专栏。 </enumset></enumset></enum></enum></string></string></integer></integer>
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
【一周】jQuery更新 | Git 15周年 | Qt付费更新?| iPhone上运行Linux | 树莓派销量猛增
回顾一周社区热门资讯 第【六十九】期:20200411-20200417 点击相应标题,跳转阅读全文 Android 推出虚拟盲文键盘 TalkBack,无需外接设备 Linux FAT 文件系统预读缺陷,补丁提升 7 倍性能 Linux 内核现在已经增加了对 exFAT 的支持,同时它也没有放弃维护原有 FAT 文件系统驱动,甚至现在从邮件列表上看,FAT 性能方面可能会有大幅提升。 KDE 社区称 Qt 公司正考虑仅面向付费用户提供新版本 “在新版发布后长达一年的时间里,开源用户将无法使用它——除非成为付费客户。” WSL 1 运行 Ubuntu 20.04 将会出现问题 问题来自glibc 2.31 中的补丁,该补丁以类似于 UNIX 的方式实现了基于 CLOCK_REALTIME 的nanosleep() 库调用。在 NT 内核上模拟 UNIX 系统时钟非常棘手。WSL 1 实现了最流行的基于时钟的系统调用,但并非全部都实现,并且没有将 CLOCK_REALTIME 支持构建到 nanosleep 中。 Git 诞生 15 周年 Chrome OS 开始使用 PWA 替代部分 ...
- 下一篇
使用helm-controller-简化容器云平台应用商店的开发开发手册
熟练的阅读并且进行相应的开发,大概需要对以下知识的了解: k8s rest api 模型需要了解。 http协议需要熟练掌握。 openapi规范了解。 linux熟练掌握。 helm的理论架构了解。 k8s crd 的理解,controller概念掌握,rbac权限模型的理解。 helm私库index.yaml的理解与使用。 core dns的配置与使用。 整体结构图 helm是啥 helm是k8s的包管理器,也是k8s平台复杂应用部署事实上的标准。包含了应用打包,部署,升级,回滚,卸载等符合生命周期管理的功能。 架构变动 helm从v2到v3的版本升级,移除了重要的一个组件tiller,整体架构更简洁。 helm架构在云管理平台开发中的不足 helm至今为止,官方仍然没有ga版的api。chart的下载,部署,升级,卸载,全部依赖cli。在多集群环境下cli很难满足平台的业务要求。 通过查看github issue,社区大概有两种解决思路: 封装cli成api。这种方式仍然存在每个集群需要通过ssh或者ansible的方式部署helm二进制文件到master节点上,给底层部署工作添...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果