在创建一个Java类的对象之前,需要由虚拟机加载该类,然后对该类进行链接和初始化。初始化完成之后,才能创建出该Java类的新的对象实例。对象也有自己的初始化过程,主要通过调用对应Java类的特定构造方法来完成。当不再有引用指向一个对象时,这个对象成为垃圾回收器的候选。对象所占用的内存空间会在合适的时机被垃圾回收器回收。对象终止机制提供了一种方式在对象被回收之前进行清理工作。
当需要复制一个对象时,可以使用clone方法,如果需要将对象状态持久化,可以使用序列化机制来得到一个方便存储的字节流。
1、类的链接
Java虚拟机运行时会在内部维护所有可用Java类的相关信息,链接的过程就是把加载的Java类的字节码中包含的信息与虚拟机的内部信息进行合并,使得Java类的代码可以被执行。
(1)验证
用来确保Java类的字节码表示在结构上是正确的,验证过程可能会导致其他Java类或接口被加载。如果发现字节码格式不正确,会抛出java.lang.VerifyError错误。
(2)准备
这个过程会创建Java类中的静态域,并将这些域的值设为默认值。重要的环节是保证类加载时的类型安全,如果发现类型安全约束被破坏,抛出java.lang.LinkageError错误。
(3)解析
处理所加载的Java类中包含的形式引用。核实所引用的类被正确加载,所调用的方法确实存在。
引用关系处理策略:
A、提前解析:在链接时递归对依赖的所有形式引用都进行解析,缺点是性能较差
B、延迟解析:只在真正需要形式引用的时候才进行解析,解决性能差的问题(OpenJDK采用此方法)
2、类的初始化
执行Java类中的静态代码块和初始化静态域。初始化过程按静态代码块和静态域在代码出现的顺序依次进行。
在当前Java类被初始化之前,它的直接父类也会被初始化,但是该Java类所实现的接口并不会被初始化(即接口被初始化时,它的父接口不会被初始化)。
另外,当访问一个Java类或接口中的静态域时,只有真正声明这个域的类或接口才会被初始化。比如访问了父类的静态域,则只会初始化父类不会初始化子类。
可能造成类被初始化的操作:
(1)创建一个Java类的实例对象
比如Test test = new Test();
(2)调用Java类中的静态方法
比如Test.staticMethod();
(3)为类或接口中的静态域赋值
比如Test.staticField = 10;
(4)访问类或接口中声明的非常量静态域
比如Test.staticNoFinalField
(5)在一个顶层Java类中执行assert语句
(6)调用class和反射api进行反射操作
3、对象的创建与初始化
实际初始化流程是沿着继承层次结构树往上传递,完成部分初始化工作,到达Object类之后,再沿着层次结构树向下,完成其余的初始化工作。
在对象创建之前需要分配内存空间,其大小取决于该类及父类和祖先类包含的所有实例域的数量和类型。空间不足会抛出OutOfMemoryError错误。如果内存分配成功,则把新创建的对象的所有实例域都设为默认值,包括Java类本身声明的及父类声明的。但该对象还不能调用,因为类构造方法还没有被调用。
类构造器调用的三个步骤:
(1)调用父类的构造方法
分显式调用和隐式调用两种,显式调用通过super关键字来完成,如果没有显式调用则由编译器自动生成相关代码。
实际执行流是跳转到父类的构造方法,再沿着继承层次结构树依次往上跳转,直到到达Object类。整个过程是一个递归过程。
(2)初始化类中实例域的值
按照实例域的出现顺序依次初始化。
(3)执行类构造器的其他代码
完成初始化工作。
在编写构造方法时,注意不要在构造方法中调用可以被子类覆写的方法,因为如果子类覆写该方法,那么在初始化过程中调用父类构造方法时,其调用的是子类所覆盖的方法,而此时子类的构造方法中的代码还没有被执行,对象仍处于初始化过程当中,容易出错,特别是用到某些子类变量还没有初始化的时候。
class Parent {
public Parent() {
int average = 30 / getCount();
}
protected int getCount() {
return 4;
}
}
class Child extends Parent {
private int count;
public Child(int count) {
this.count = count;
}
public int getCount() {
return count;
}
}
public class BadConstructor {
public void test() {
Child child = new Child(5);
}
//Exception in thread "main" java.lang.ArithmeticException: / by zero
public static void main(String[] args) {
BadConstructor bc = new BadConstructor();
bc.test();
}
}
4、对象终止
对象被销毁之前要正确释放资源。
A、内存资源:对象实例域所占用的内存空间,由垃圾回收器回收
B、非内存资源:程序运行时申请的其他系统资源,包括打开的文件、套接字连接、数据连接等,需要由程序显式释放。
在Java中对这两种资源的释放操作是分开处理的,对于非内存资源的释放无法以自动的方式来进行,因此引入了对象终止机制finalization来解决非内存资源的释放问题。
(1)finalize方法的基本用法
双重不确定:
A、finalize方法一定在对象的内存空间被垃圾回收器回收之前运行(Java语言规范没明确规定调用时间)
B、垃圾回收器运行的时间也不确定
这种双重不确定可能导致释放资源时产生与时间相关的错误。如果在某个时点上finalize方法碰巧被执行了,那么程序的行为是正确的;如果finalize方法没有被执行,则可能资源没有被正确释放。这种随机错误显然是不能出现在程序当中的。
(2)finalize方法与资源释放
实际的程序当中,不应该依靠对象的finalize方法来进行非内存资源的释放。例如在一个对象的使用过程中打开了多个文件,如果把文件关闭操作放在finalize方法进行,可能会出问题,当程序中出现大量此对象,而这些对象的finalize方法没有被及时调用,导致大量处于打开状态的文件没有被关闭,而操作系统对同时打开的文件数量是有限制的。
正确的做法是程序显式添加释放资源的方法,由对象的使用者负责。或者实现AutoCloseable接口,进行close关闭,或者使用try-with-resources语句来进行释放。
(3)实现正确的finalize方法
finalize方法可能抛出任何类型的异常,由于是由虚拟机直接调用的,抛出的异常将被忽略,对finalize方法的调用马上停止。
先编写当前类的终止逻辑,再通过super.finalize()来调用父类的finalize方法。
为防止子类未调用父类的终止方法,可使用终止器守卫者:finalizer guardian模式:
public class WithFinalizer {
//防止子类未主动调用父类的finalize方法
private final Object guardian = new Object() {
protected void finalize() throws Throwable {
super.finalize();
}
};
}
当WithFinalizer可以被回收时,guardian对象也要被回收,此时finalize方法被执行。
另外,在finalize方法中要避免创建对当前对象的新的引用,否则会逃离GC。
5、对象复制
根据防御式编程(defensive programming)的实践,当方法将一个内部对象的引用传递给调用者的时候,最好返回对象的拷贝,防止对其进行修改。
实现cloneable接口,clone方法实现的是浅拷贝,对当前对象中的所有实例域进行逐一复制,深拷贝需要自己实现,深拷贝要求对当前对象的实例域所引用的可变对象都可以以递归的方式进行复制,所涉及的每个对象都应该实现clonable接口。
另外一种方式是提供复制构造器方法:
public class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() {
return this.name;
}
public String getEmail() {
return this.email;
}
//复制构造器
public User(User user) {
this.name = user.getName();
this.email = user.getEmail();
}
}
6、对象序列化
对象的持久化,涉及自定义存储格式,可以基于xml,json,csv等,或者二进制格式,复制的是保持和读取的时候,有的对象内部结构比较复杂,需要遍历整个对象图。
从简化实现的角度,可以使用java语言内置的对象持久化方式,即对象序列化机制(object serialization),用来在虚拟机中的活动的对象与字节流之间进行转换。
A、序列化:把活动对象的内部状态转换成一个字节流
B、反序列化:从字节流中得到一个可以直接使用的java对象
(1)默认的对象序列化
实现Serializable接口,该接口仅仅是一个标记接口,声明该类的对象可以被序列化。
public void write(User user) throws IOException {
Path path = Paths.get("user.bin");
try (ObjectOutputStream output = new ObjectOutputStream(Files.newOutputStream(path))) {
output.writeObject(user);
}
}
public User readUser() throws IOException, ClassNotFoundException {
Path path = Paths.get("user.bin");
try (ObjectInputStream input = new ObjectInputStream(Files.newInputStream(path))) {
User user = (User) input.readObject();
return user;
}
}
java对象序列化机制会自动遍历整个对象图并依次处理,如果该类没有实现Serializable接口,将抛出NotSerializableException异常,如果该类的对象中的域所引用的对象没有实现该接口,则该域不会出现在序列化之后的字节流中。
可以使用transient关键字声明不被序列化的字段(比如password),或者使用serialPersistentFields声明序列化时要包含的字段(要求名称和类型一致,否则不生效)。
(2)自定义对象序列化
java序列化方法依赖其内部实现,如果内部实现变了,会导致旧版本的字节流无法被转换为新版本的java对象,不符合面向接口编程。
自定义序列化方法需要在要序列化的类中增加writeObject和readObject方法,使用ObjectOutputStream进行序列化时,如果java类提供了该方法,将调用该方法替代默认方法进行写入操作。
具体的一般先调用defaultWrite/ReadObject方法,再进行特殊处理。另外反序列化没有调用构造方法,内部完整性可能被破坏,在readObject里头要确保返回对象之前经过了完整的初始化。
(3)对象替代
反序列化相当于一个构造器,对于有些类要管理自己的实例的话(比如单例,比如扁平化实例域),反序列化会破坏这种机制。可以适用对象替代的方法来解决,writeReplace,readResolve方法,当要序列化的类提供这两种方法的时候,会再对象写入和返回之前调用,替代默认方法产生的对象。
public class Order implements Serializable {
private User user;
private String id;
public Order(String id, User user) {
this.id = id;
this.user = user;
}
public String getId() {
return this.id;
}
private Object writeReplace() throws ObjectStreamException {
return new OrderTO(this);
}
}
(4)版本更新
通过serialVersionUID来判断是否兼容,如果没有显式声明的话,虚拟机会根据java类的各种元素特征计算出一个散列值,默认生成的值会随着java类的更新而变化。
使用自定义序列化机制的话,通常要声明serialVersionUID防止出现虚拟机判定为版本不兼容。
(5)安全性
A、根据信息隐藏原则,尽可能减少序列化结果包含的信息
B、对包含序列化结果的字节流进行保护(加密,解密)
C、从字节流中进行反序列化操作时进行数据完整性验证,可以通过ObjectInputValueValidation进行校验。
(6)使用Externalizable接口
如果希望对序列化的过程进行完全控制的话,可以实现Externalizable接口,提供writeExternal,readExternal方法,要求提供一个无参构造器。