类的声明周期描述了一个类加载,使用,卸载的整个过程。
类的生命周期-加载阶段
类的加载阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制的方式获取字节码文件。
多种渠道指的是本地文件(磁盘上的字节码文件),动态代理生成(程序运行时使用动态代理生成),通过网络传输的类。
类加载器在加载完类之后,Java虚拟机会将字节码文件中的信息保存到内存的方法区(Java虚拟机中的虚拟概念)中。生成一个InstanceKlass对象,保存类的所有信息。
InstanceKlass保存的类的所有信息包括基本信息,常量池信息,字段,方法,虚方法表(实现多态的基础)等信息。
最后,Java虚拟机会在堆中生成一份与方法区中数据类似的Java.lang.class对象。作用是在Java代码中获取类的信息以及存储静态字段的数据(JDK8及之后)
为什么这样设计呢?
首先我们要知道,instanceKlass是C++编写的对象,我们Java不能直接操作该对象,我们就需要在堆中用Java包装一个该对象来方便我们进行操作。
其次,方法区的instanceKlass对象有一些方法是我们不需要关心的,例如instanceKlass对象中的虚方法表,就是底层实现Java多态时使用的,而对于开发者来说,并不需要开发者去关心或者修改里面的代码。所以,为了优化内存和考虑系统安全,虚拟机复制给堆区的对象并不包含InstanceKlass对象中的所有方法,而是只会把开发者能用上的方法创建出来。
类的声明周期-连接阶段
在加载阶段后,一个类或者接口的信息就会被加载到内存中,接下来就是进入连接阶段。
连接阶段可分为三个部分:验证,准备,解析。
验证部分:验证内容是否满足《Java虚拟机规范》,字节码信息不满足规范并继续执行,会危害到虚拟机。
准备部分:虽然在加载阶段已经给对象分配了内存,但静态变量还没有处理,静态变量需要内存保存数据并具备初始值。
解析部分:将常量池中的符号引用替换成内存的直接引用。(就是在jclass里浏览的索引#xx,需要替换成内存的地址。)
连接阶段-验证
连接阶段的第一个环节是验证,验证的主要目的是检测JAVA字节码文件遵守了JAVA虚拟机规范中的约束。这个阶段一般不需要程序员参与。
验证是要验证很多东西,目前提主要4个。
- 文件格式验证:比如文件是否以0xCAFEBABE开头,主次版本号是否满足Java虚拟机版本要求。
- 元信息验证:例如类必须有父类(super不能为空)。
- 验证程序执行指令的语义:比如方法内的指令执行中跳转到了不正确的位置。
- 符号引用校验:例如是否访问了其他类的private的方法。
连接阶段-准备
准备阶段就是为静态变量(static)分配内存并设置初始值。
例如:
public class Student{
public static int value = 1;
}
Java虚拟机在加载这个类时会在堆上创建一个class对象,首先给value对象分配一块内存区域,并赋值初始值0。在准备阶段只会给静态变量赋值一个默认值叫初值。之后初始化阶段才会赋值为我们设置的值。
为什么要在准备阶段给它赋一个初始值呢?
如果用户只是声明一个静态变量,没有给这个静态变量赋值,虚拟机在给它分配内存区域时,要是该内存区域有一些残留数据,就会出现随机值问题。
如果我们的静态变量是final修饰的基本数据类型,准备阶段直接会将代码中的值进行赋值。
为什么这样设计呢?
在static的基础上加上final之后,那就是在类的整个加载过程中不可变的全局变量,static说明它是属于整个类的,类加载时就分配了内存,final说明其值不可变,即,位置固定内容也固定了。这样Java虚拟机在准备阶段就直接赋值,不需要在后面阶段再进行了。
连接阶段-解析
解析阶段主要是将常量池中的符号引用替换为直接引用。
符号引用就是在字节码文件中使用编号来访问常量池中的内容。
直接引用就是直接使用内存地址方式引用,性能比较高。
类的生命周期:初始化阶段
在连接阶段后,类的信息就已经加载到内存中,校验工作也已经完成,之后就进入初始化阶段。
初始化阶段会执行静态代码块中的代码,并为静态变量赋值。(从字节码指令来看,就是执行了字节码文件中clinit部分的字节码指令。)
例如我们写个demo1示例代码
public class Demo1{
public static int value = 1;
static{
value = 2;
}
public static void main(String[] args)
{
}
}
字节码文件的方法信息为:
初始化阶段主要目标为<clinit>,而clinit内指令为:
分析可知:最后value值为2。
如果先写静态代码块,后写静态全局变量的字节码指令为:
这里我们发现,在字节码指令中,clinit方法中的执行顺序与Java中编写的顺序是一致的。
在初始化阶段中,哪些方法会导致类的初始化呢?
1.访问一个类的静态变量或静态方法(但变量是final修饰的并且等号右边是常量则不会触发初始化)。
2.调用Class.forName(String className)方法时会触发初始化(而另一个forName重载方法(String,boolean)是可以指定是否触发初始化)。
3.new一个该类对象时会触发初始化。
4.执行Main方法的当前类会触发初始化。
一个类被加载过时,它不会再被初始化并加载。
clinit指令在特定情况下不会出现,例如:
1.无静态代码块且无静态变量赋值语句。
2.有静态变量的声明,但是没有赋值语句。
3.静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。
需要注意的是,直接访问父类的静态变量,不会触发子类的初始化。子类的初始化clinit调用之前,会先调用父类的clinit初始化方法。
大厂相关题
分析:执行main方法先执行初始化Test1,就会先打印D,之后执行println("A"),类在初始化之后就不会再初始化加载,所以new之后也不会再打印D,后面执行两次构造方法,在init构造方法字节码指令我们看到,先打印C,再B,也就是代码块先于构造方法执行。
最终答案是:DACBCB。
题二:
分析:调用new创建对象,需要初始化BO2,而在初始化是优先初始化父类的,也就是a=1,之后再执行子类的初始化代码,也就是a=2。所以最后a=2。