Java 提供的对象输入流ObjectInputStream和输出流ObjectOutputStream,可以直接把 Java 对象作为可存储的字节数据写入文件,也可以传输到网络上。对于程序员来说,基于 JDK 默认的序列化机制可以避免操作底层的字节数组,从而提高开发效率。Java 序列化的主要目的是网络传输和对象持久化。

# 一、无法跨语言

无法跨语言,是 Java 序列化最致命的问题。对于跨进程的服务调用,服务提供者可能是 Java 意外的其他语言,当我们需要和异构语言交互时,Java 序列化就难以胜任

由于 Java 序列化技术是 Java 语言内部的私有协议,其他语言并不支持,对于用户来说它完全是个黑盒子。对于 Java 序列化后的字节数组,别的语言无法进行反序列化,这就严重阻碍了它的应用。

事实上,目前几乎所有流行的 Java RCP 通信框架,都没有使用 Java 序列化作为编解码框架,原因就在于它无法跨语言,而这些 RPC 框架往往需要支持跨语言调用。

# 二、序列化后的码流太大

【1】通过一个实例看下 Java 序列化后的字节数组大小。如下 UserInfo 对象是实现了序列化接口的对象,并生成了默认的序列号:serialVersionUID = 1L。说明 UserInfo 对象可以通过 JDK 默认的序列化机制进行序列化和反序列化。并创建了一个与之比较码流大小的方法 codeC ,此方法基于 ByteBuffer 的通用二进制编解码技术对 UserInfo 对象进行编码,编码结果也是 byte 数组。

【序列化 ID 问题】:两个客户端 A 和 B 试图通过网络传递对象数据,A 端将对象 C 序列化为二进制数据再传给 B,B 反序列化得到 C。 问题:C 对象的全类路径假设为 com.yintong.UserInfo,在 A 和 B 端都有这么一个类文件,功能代码完全一致。也都实现了 Serializable 接口,但是反序列化时总是提示不成功。 解决:虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。清单 1 中,虽然两个类的功能代码完全一致,但是序列化 ID 不同,他们无法相互序列化和反序列化。

//实现了序列化接口的实例
public class UserInfo implements Serializable{

	private static final long serialVersionUID = 1L;
	
	//用户ID
	private int ID;
	//用户名
	private String name;
	
	//有参构造器
	public UserInfo(int iD, String name) {
		super();
		ID = iD;
		this.name = name;
	}
	
	//根据 buffer缓冲区 获取字节数组
	public byte[] codeC() {
		//定义字节缓冲区
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		//获取name属性的二进制字节流
		byte[] value = this.name.getBytes();
		//用于写入 int 值的相对 put 方法(可选操作)。 
		//将 n 个包含给定 int 值的字节按照当前的字节顺序写入到此缓冲区的当前位置,然后将该位置增加 n。
		buffer.putInt(value.length);
		//存入 二进制值
		buffer.put(value);
		
		//写入一个int值=ID到ByteBuffer中。
		buffer.putInt(this.ID);
		//切换为读
		buffer.flip();
		value = null;
		//remaining 返回剩余的可用长度,次长度为实际读取的数据长度
		byte[] result = new byte[buffer.remaining()];
		//获取buffer中的值到 result
		buffer.get(result);
		return result;
	}
	
	public int getID() {
		return ID;
	}

	public void setID(int iD) {
		ID = iD;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

【2】下面是测试程序,先调用两种编码接口对 UserInfo 进行编码,然后分别打印两者编码后的流大小进行对比。

public class TestUserInfo {

	public static void main(String[] args) throws Exception {
		UserInfo info = new UserInfo(101, "zheng zhao xiang");
		//JDK 序列化流程
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(bos);
		oos.writeObject(info);
		oos.flush();
		oos.close();
		byte[] b = bos.toByteArray();
		System.out.println("JDB 序列化后流的长度:  "+b.length);
		bos.close();
		System.out.println("通过缓冲区 buffer 处理后的流长度:  "+info.codeC().length);
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

【3】测试结果如下:采用 JDK 序列化机制编码后的二进制数组大小是通过缓冲区处理后的 4 倍。

JDB 序列化后流的长度: 97
通过缓冲区buffer 处理后的流长度:24
1
2

# 三、序列化性能太低

【1】下面从序列化的性能角度看下 JDK 的表现如何。将之前的例子进行修改。对 UserInfo 中的 codeC 方法改造如下:

public byte[] codeC(ByteBuffer buffer) {
	//清空缓冲区
	buffer.clear();
	//获取name属性的二进制字节流
	byte[] value = this.name.getBytes();
	//用于写入 int 值的相对 put 方法(可选操作)。 
	//将 n 个包含给定 int 值的字节按照当前的字节顺序写入到此缓冲区的当前位置,然后将该位置增加 n。
	buffer.putInt(value.length);
	//存入 二进制值
	buffer.put(value);
	
	//写入一个int值=ID到ByteBuffer中。
	buffer.putInt(this.ID);
	//切换为读
	buffer.flip();
	value = null;
	//remaining 返回剩余的可用长度,次长度为实际读取的数据长度
	byte[] result = new byte[buffer.remaining()];
	//获取buffer中的值到 result
	buffer.get(result);
	return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

【2】对 Java 序列化和二进制编码分别进行性能测试,编码 100万次,然后统计消耗的总时间:

public class PerformTestUserInfo {
	public static void main(String[] args) throws IOException {
		UserInfo info = new UserInfo(101, "zheng zhao xiang");
		//循环次数
		//JDK 序列化流程
		ByteArrayOutputStream bos = null;
		int loop = 1000000;
		ObjectOutputStream oos = null;
		//写之前获取系统时间
		long startTimeMillis = System.currentTimeMillis();
		for(int i=0; i<loop; i++) {
			bos = new ByteArrayOutputStream();
			oos = new ObjectOutputStream(bos);
			oos.writeObject(info);	
			oos.flush();
			oos.close();
			byte[] b = bos.toByteArray();
			bos.close();
		}
		long endTimeMillis = System.currentTimeMillis();
		System.out.println("JDK 序列化花费的时间:  "+(endTimeMillis - startTimeMillis)+" ms");
		
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		startTimeMillis = System.currentTimeMillis();
		for(int i=0; i<loop; i++) {
			info.codeC(buffer);
		}
		endTimeMillis = System.currentTimeMillis();
		System.out.println("通过缓冲区 buffer花费的时间:  "+(endTimeMillis - startTimeMillis)+" ms");
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

【3】结果展示:结果非常令人惊讶,Java 序列化的性能只有二进制编码的 11% 左右,可见原生序列化的性能很差。

JDB 序列化花费的时间: 1861ms
通过缓冲区buffer花费的时间:210ms
1
2

# 四、结论

无论是序列化后的码流大小,还是序列化的性能,JDK 默认的序列化机制表现都很差。因此,我们通常不会选择 Java 序列化作为远程跨节点调用的编解码框架。而是使用业界提供的很多优秀的编解码框架,它们在克服了 JDK 默认的序列化框架缺点的基础上,还增加了很多亮点。例如:Google 的 Protobuf、Facebook 的 Thrift 和 JBoss 的 Marshalling 等等,我后期都会学习和整理相关的博文。

(adsbygoogle = window.adsbygoogle || []).push({});