• 欢迎访问开心洋葱网站,在线教程,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站,欢迎加入开心洋葱 QQ群
  • 为方便开心洋葱网用户,开心洋葱官网已经开启复制功能!
  • 欢迎访问开心洋葱网站,手机也能访问哦~欢迎加入开心洋葱多维思维学习平台 QQ群
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏开心洋葱吧~~~~~~~~~~~~~!
  • 由于近期流量激增,小站的ECS没能经的起亲们的访问,本站依然没有盈利,如果各位看如果觉着文字不错,还请看官给小站打个赏~~~~~~~~~~~~~!

再来认识一下 Java 序列化

OC/C/C++ Henry扶苏 1329次浏览 0个评论

前言

在面试中,Java 序列化被问到的几率还是挺高的。所以搜集了 Java 序列化常见的问题,由浅入深的帮助大家进一步学习和理解。

再来认识一下 Java 序列化

序列化基础知识

什么是序列化?

Java 序列化是 JDK 1.1 中引入的特性之一。

总的来说,序列化讲一个 Java 对象所描述的所有内容以文件 IO 的方式 存储传输 的过程。核心作用是对象状态的保存和重建

在这里有两个比较重要的概念:

  • 序列化:把 Java 对象转换为字节码的过程
  • 反序列化:把字节码还原为 Java 对象的过程

再来认识一下 Java 序列化

为什么要序列化 ?

因为 Java 对象是存放在 JVM 的 堆内存 中的,当 JVM 退出的时候,对象也就随之销毁。如果想 持久化 或进行 网络传输 对象数据时,那就必须把对象转为计算机可以识别的字节码。

在以下场景中需要使用到序列化。

  • 持久化数据:文件、数据库、缓存
  • 网络传输:RMI (远程调用 Remote Method Invocation)、RPC

如何实现序列化

在 Java 中,没有关键字可以直接去定义一个所谓的 可持久化 对象。这就需要我们在代码中 显示地 进行序列化和反序列化还原操作。

Serializable 接口

Serializable 接口是一个 标记接口,没有方法或字段。一旦实现了此接口,就标志该类的对象就是可序列化的。

1、定义

再来认识一下 Java 序列化

2、序列化

再来认识一下 Java 序列化

3、反序列化

再来认识一下 Java 序列化

4、结果

再来认识一下 Java 序列化

5、如果不实现 Serializable 接口将无法进行序列化或反序列化

再来认识一下 Java 序列化

Externalizable 接口

Externalizable 继承了 Serializable 接口,还定义了两个抽象方法:writeExternal() 和 readExternal()

如果开发人员使用 Externalizable 来实现序列化和反序列化,必须重写 writeExternal() 和 readExternal() 方法。

因为实现 Externalizable 接口之后,基于 Serializable 接口的默认化序列化机制就会失效。

再来认识一下 Java 序列化

Serializable 和 Externalizable 的区别

SerializableExternalizable
Java 支持比较完整,自动存储必要信息需要开发人员自己完成
所有对象由 Java 统一保存,性能较低开发人员决定哪个对象保存,可以提升速度
保存时占用空间大,性能差部分存储,空间占用可能较少,性能相对高

Java 序列化协议分析

下面这段字节码是保存在本地的字节码文件,接下来准备对这段字节码进行 拆分讲解 (只针对 Serializable)。

以下的字节码定义参考 java.io.ObjectStreamConstants 中的定义,如果有兴趣,找到这个类,里面有详细的定义。

再来认识一下 Java 序列化

  • JDk 序列化的魔数
  • aced STREAM_MAGIC 魔数,用于标识当前文件的头部
  • 0005 STREAM_VERSION 序列化协议版本号
  • 描述对象的类型信息
  • 73 TC_OBJECT 表示序列化的是一个普通 Java 对象 (Object 0x73,String 0x74,Array 0x75)
  • 72 TC_CLASSDESC 表示当前的对象的类型信息
  • 0014 表示类名的长度,这段代码中是 0014 换算过来是 20 个字节
  • 7374 6174 6963 4661 6374 6f72 792e 5065 7273 6f6e 表示类名,即 staticFactory.Person
  • 0000 0000 0000 0001 类名后的 8 个字节是一个长整数,即 serialVersionUID = 1L
  • 02 SC_SERIALIZABLE 标识位,说明这个类实现了 Serializable 接口。
  • 对象的字段表
  • 0002 表示这个对象中有 2 个属性
  • 49 即 I 表示 int,说明这是一个 32 位整数
  • 0003 表示属性名的长度,即 3 字节
  • 6167 65 表示属性 age
  • 4c 即 L,表示引用类型,说明这个属性是某个类型的引用
  • 0004 表示属性名的长度,即 4 字节
  • 6e61 6d65 表示属性 name
  • 74 TC_STRING 表示后面是个字符串
  • 0012 表示字符串长度,即 18 字节
  • 4c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b 即 Ljava/lang/String
  • 父类的描述信息
  • 78 TC_ENDBLOCKDATA 标志所有的字段类型信息描述结束
  • 70 TC_NULL 代表 null,即没有父类
  • 对象的属性值
  • 0000 001e 初始化后的年龄,转换后即 30
  • 74 TC_STRING 表示后面是个字符串
  • 0005 表示字符串长度为 5
  • 4865 6e72 79 初始化之后的姓名,转换后即 Henry

序列化的特性

在实际应用中,有些时候 不能使用默认序列化机制。比如,希望在序列化过程中忽略掉敏感数据。

本段重点讨论 transient 和 static 之间的区别,并讨论每个关键字的作用。

transient 关键字

当我们的一个字段被声明为 transient 后,默认序列化机制就会忽略掉该字段的内容,不会被保存。

static 关键字

序列化仅对特定的变量产生作用,但 static 修饰的变量并不特定于任何对象。因此,静态变量不会参与序列化。

虽然用关键字可以避免序列化,但是当关键字组合使用的时候,也可能会失效。

transient 和 static 的规则

  1. 临时变量在序列化过程中将被忽略。
  2. static 变量不会参与序列化。
  3. 如果在声明本身期间对值进行了初始化,则静态变量将被序列化。
  4. 如果一个变量同时包含 transient 和 static 关键字,并且该值在声明期间被初始化,则它将被序列化。因为在这里 transient 修饰符会被忽略,而 static 修饰符将执行操作。
  5. final 变量将被序列化。
  6. 如果一个变量同时包含 final 和 transient 关键字,那么它就会被序列化。因为在这里 transient 修饰符会被忽略,而 final 修饰符将执行操作。

下面用一段代码验证一些。

1、定义一个实例化类

再来认识一下 Java 序列化

2、序列化

再来认识一下 Java 序列化

3、反序列化

再来认识一下 Java 序列化

4、输出结果

再来认识一下 Java 序列化

重点:

  • One 和 Two 为 null,根据规则 1,使用 tresient 修饰的变量不参与序列化
  • Three 为 null,根据规则 2, static 变量不参与序列化
  • Four 之所以为 V4,根据规则 3,仅在声明期间初始化该值,静态变量才会被序列化
  • Five 为 null,根据规则4,因为它被 static 和 tresient 同时修饰,并且值在生命期间未初始化
  • Six 之所以为 6,根据规则 4,如果同时 static 和 tresient 同时修饰,并且该值在声明期间已初始化,那就会被序列化
  • Seven 是 V7,根据规则 5,用 final 修饰的会被序列化
  • Eight 之所以为 V8,根据规则 6,如果变量同时被 final 和 tresient 修饰,那就会被序列化

serialVersionUID 具体作用是什么?

在序列化中,还有一个特别重要的步骤,需要指定 serialVersionUID 版本号。

如果反序列化使用的 Class 的版本号与序列化时候使用的不一致,则会报异常。

序列化版本号可以随意的指定

如果不指定,JVM 会 自己计算 一个版本号,但随着 Class 的升级,就无法正确反序列化

不指定版本号还有另一个明显隐患,不利于 JVM 间的移植,可能 Class 文件没有更改,但不同 JVM 可能计算的规则不一样,这样也会导致无法反序列化

Java 序列化的缺陷

无法跨平台

现在的系统设计越来越多元化,项目里可能会用多种语言来编写应用程序,比如 Java、C++、Python 同时配合使用。

而 Java 序列化只适用于基于 Java 语言实现的框架。其他语言大部分没有使用 Java 的序列化框架。如果两个基于不同语言编写的应用程序相互通信,那么久无法实现两个应用服务之间的序列化与反序列化。

容易被攻击

对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这意味着,在反序列化字节流的过程中,该方法可以 执行任意类型的代码,这是非常危险的。

对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致 hashCode 方法被调用次数呈次方爆发式增长, 从而引发栈溢出异常。

序列化后的流太大

序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量。

序列化的性能太差

Java 的序列化耗时比较大。序列化的速度也是体现序列化性能的重要指标,如果序列化的速度慢,就会影响网络通信的效率,从而增加系统的响应时间。

序列化的其它问题

单例模式与序列化

首先抛出一个问题,单例模式真的能够实现实例的唯一性吗?

答案是否定的,很多人都知道反射可以 恶意破坏单例模式。其实除了反射以外,使用序列化与反序列化也同样会破坏掉单例。比如下面这个单例:

再来认识一下 Java 序列化

上边这种情况,其实已经破坏掉单例。因为序列化会通过反射调用无参构造器返回一个新的对象,从而破坏了单例模式,解决办法就是 添加 readResolve() 方法,返回指定的对象。

巨人的肩膀

  • 刘超 网络通信优化之序列化:避免使用 Java 序列化 「极客时间」
  • Java 优化


开心洋葱 , 版权所有丨如未注明 , 均为原创丨未经授权请勿修改 , 转载请注明再来认识一下 Java 序列化
喜欢 (0)

您必须 登录 才能发表评论!

加载中……