概述
Java对象我们都很熟悉,上来一顿操作猛如虎new 一个。但是说实话真要是说说类的创建过程及这个类的生命周期之类更深些的东西。估计回答起来就不是太顺畅了,当然个人感觉啊,大牛勿喷。下面咱们就聊聊java类的生命周期是如何运转的。
当编写完一个java的文件后,经过编译会生成一个后缀名为 .class的文件(字节码文件),这种字节码文件是需要在java虚拟机中运行的。java类的生命周期就是指一个 .class文件从加载到卸载的全过程。一个java类的完整的生命周期会经历加载、连接(验证、准备、解析)、初始化、使用、和卸载五个阶段,当然存在加载或者连接之后没有被初始化就直接被使用的情况,咱们这里说的是一个完整的生命周期。具体生命周期示意图如下:
装载
装载(Loading)就是把类的 .class文件读入Java虚拟机中; 而连接(Linking)就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去 。连接阶段分为三个子步骤——验证(Verification)、准备(Preparation)和解析(Resolution)。 验证步骤确保了Java类型数据格式正确并且适于Java虚拟机使用。而准备步骤则负责为该类型分配它所需的内存、比如为它的类变量分配内存。解析步骤则负责把常量池中的符号引用转换为直接引用。
加载、 验证、准备和初始化这四个阶段的顺序是确定的,类的加载过程必须按照这种顺序接部就班地开始,而解析则不一定(所以上图仅供正常生命周期参考),它在某些情况下可以在初始化阶段之后再开始, 这是为了支持 Java语言的运行时绑定 (也称为动态绑定或晩期绑定)。
连接
连接主要作用是把已读入的类型数据合并到虚拟机的运行时状态中。
1)验证:确保java类型的数据格式正确并且适用于jvm使用;验证作为连接阶段的第一步,这一步的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证主要内容:
- 文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求;
- 元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。第二阶段,保证不存在不符合 Java 语言规范的元数据信息;
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上;
- 符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用。
可以考虑使用 -Xverify:none
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
2)准备:为该类分配内存;就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认的初值是这样的:
- 基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0;
- 引用类型的默认值为null;
- 常量的默认值为我们程序中设定的值,比如我们在程序中定义final static int i = 100,则准备阶段中 i 的初值就是100。
3)解析:把常量池中的符号引用转换为直接引用;那么什么是符号引用,什么又是直接引用呢?FOR EXAMPLE:我们要找一个人,我们现有的信息是这个人的身份证号是1234567890。只有这个信息我们显然找不到这个人,但是通过公安局的身份系统,我们输入 1234567890这个号之后,就会得到它的全部信息:比如安徽省黄山市余暇村18号张三,通过这个信息我们就能找到这个人了。这里,123456790就好比是一个符号引用,而安徽省黄山市余暇村18号张三就是直接引用。在内存中也是一样,比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164 就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接 口名、字段名、方法名转换为具体的内存地址。常规解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
初始化
每个类和接口在首次主动使用时初始化。为类变量赋予正确的初始值。
1)如果类存在直接超类,且直接超类没有被初始化,先初始化直接超类;
2)如果类存在初始化方法,就执行此方法;
- 创建类的新实例
- 调用类中声明的静态方法
- 操作类或者接口中声明的非常量静态字段
- 调用Java API中特定的反射方法
- 初始化一个类的子类
- 指定一个类作为jvm启动时的初始化类
只有六种动作被认为是主动使用,其他使用类的方式叫做被动引用,而被动引用不会触发类的初始化。具体可以参考下代码列举几种主动使用的情况:
public class Test1 {
/**
* 主动引用引起类的初始化的第四种情况就是运行Test1的main方法时
* 导致Test1初始化,这一点很好理解,就不特别演示了。
* 本代码演示了前三种情况,以下代码都会引起InitClass的初始化,
* 但由于初始化只会进行一次,运行时请将注解去掉,依次运行查看结果。
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception{
// 主动引用引起类的初始化一: new对象、读取或设置类的静态变量、调用类的静态方法。
// new InitClass();
// InitClass.a = "";
// String a = InitClass.a;
// InitClass.method();
// 主动引用引起类的初始化二:通过反射实例化对象、读取或设置类的静态变量、调用类的静态方法。
// Class cls = InitClass.class;
// cls.newInstance();
// Field f = cls.getDeclaredField("a");
// f.get(null);
// f.set(null, "s");
// Method md = cls.getDeclaredMethod("method");
// md.invoke(null, null);
// 主动引用引起类的初始化三:实例化子类,引起父类初始化。
// new SubInitClass();
}
}
class InitClass{
static {
System.out.println("初始化InitClass");
}
public static String a = null;
public static void method(){}
}
class SubInitClass extends InitClass{}
使用
类的使用包括主动引用和被动引用,主动引用在初始化中已经说过了,下面我们主要来说一下被动引用:
- 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
- 定义类数组,不会引起类的初始化。
- 引用类的常量,不会引起类的初始化。
父类子类静态代码的执行加载顺序问题请参考:
使用就离不开类的实例化,实例化一个类大概有四种途径:
1)new操作符;
2)调用Class或者Java.lang.reflect.Constructor对象的newInstance()方法;
3)调用任何现有对象的Clone()方法;
4)通过java.io.ObjectInputStream类的getObject()方法反序列化;
隐含实例化的几种途径:
1)保存命令行参数的String对象;
2)和类装载相关,jvm装载的每一个类型,会暗中实例化一个Class对象来代表这个类型;
3)和类装载相关,当jvm装载了在常量池中包含CONSTANT_String_info入口类的时候,会创建新的String对象的实例来表示这些常量字符串;
4)通过执行包含字符串连接操作符的表达式产生对象;
实例化步骤
1)在堆中为保存对象的实例变量分配内存;
2)为实例变量初始化为默认的初始值;
3)为实例变量赋正确的初始值,有三种技术完成赋值:
- 如果对象是clone() 创建的,jvm把原实例变量中的值拷贝到新对象中;
- 如果是通过ObjectInputStream类的readObject()调用反序列化的,jvm从输入流中读取的值来初始化实例变量;
- jvm调用对象的实例化方法把对象的实例变量初始化为正确的初始值;
卸载
jvm实现必须具有某种自动堆存储管理策略,大部分是使用垃圾收集器。如果类声明了 void finalize()方法,垃圾收集器在释放实例内存前会执行这个方法。垃圾收集器自动调用的finalize()方法抛出的任何异常都将被忽略。
从jvm中卸载类型,很多情况,jvm中类的生命周期和对象的生命周期很相似。jvm如何判断动态装载的类型是否仍然被程序使用,其判断方式和判断对象是否仍然被使用很相似。在类使用完之后,如果满足下面的情况,类就会被卸载:
- 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。如果使用启动类装载器装载的类型永远都是可触及的,所以永远不会被卸载。只有使用用户定义的类装载器装载的类型才会变成不可触及,才会被卸载。