类的加载机制以及类、对象初始化的详细过程
WangScaler: 一个用心创作的作者。声明:才疏学浅,如有错误,恳请指正。
java类的生命周期包括加载、连接(验证、准备、解析)、初始化、使用、卸载五个阶段。而解析阶段会在初始化之前或之后触发。类的加载不是随着jvm的启动而加载,而是随着使用动态的加载。
接下来我们首先了解下虚拟机的类加载机制。
虚拟机的类加载机制
我们知道java的优势之一就是跨平台性,为什么java能跨平台执行呢?就因为java是运行在java虚拟机jvm上的。那么jvm的类加载机制是怎样的呢?我们知道java编译之后的文件是class文件,而虚拟机的类加载机制就是把Class文件加载到内存,进行校验,解析和初始化的过程。
加载
加载就是通过类的全限定名来获取到class文件,将文件的二进制字节流转化成方法区的静态数据结构,然后在内存中生成这个类的class对象并在堆中生成一个便于用户调用的class类型的对象。
验证
验证就是对文件格式、元数据、字节码进行验证(即语法语义的验证)、符号引用的验证,确保Class文件中的字节流不会危害虚拟机的安全。可参考java虚拟机符号引用验证_深入了解Java虚拟机---虚拟机类加载机制。
准备
给静态变量赋初值0。jdk8之前类的元信息、常量池、静态变量都是存储在永久代(方法区),而jdk8之后元空间(方法区)替代了永久代只存储类的元信息,将常量池和静态变量转移至堆内存中。
解析
将符号引用替换成直接引用。解析阶段会在初始化之前或之后触发。
- 1、假如A引用B(具体的实现类),编译阶段编译A的时候,是无法知道B是否被编译的,所以编译阶段B会被符号所代替,这个符号就是B的地址。在解析的时候如果B尚未加载,就会加载B,此时A中的符号将替换成真正的B的地址,这种称为静态解析,此时的解析是在初始化之前发生。
- 2、如果A引用的是B的抽象方法或者接口。那么只有在调用A的时候才知道具体的实现类是哪一个。此时的解析是发生在初始化之后的,也被成为动态解析。
- 3、虚拟机可以对第一次的解析结果进行缓存,避免解析动作的重复执行。
初始化
类、对象的初始化顺序:(静态变量、静态代码块)>(变量、代码块)>构造器。
卸载
- java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
下面以简短的例子来演示初始化的过程。
初始化示例代码
package com.wangscaler.load;
/**
* @author WangScaler
* @date 2021/7/28 19:17
*/
public class Father {
private int i = method();
private static int j = staticMethod();
static {
System.out.println("1、父类静态代码块");
}
Father() {
System.out.println("2、父类构造器");
}
{
System.out.println("3、父类代码块");
}
private int method() {
System.out.println("4、父类方法");
return 1;
}
private static int staticMethod() {
System.out.println("5、父类静态方法");
return 1;
}
}
package com.wangscaler.load;
/**
* @author WangScaler
* @date 2021/7/29 14:15
*/
public class Son extends Father {
private int i = method();
private static int j = staticMethod();
static {
System.out.println("6、子类静态代码块");
}
Son() {
System.out.println("7、子类构造器");
}
{
System.out.println("8、子类代码块");
}
public int method() {
System.out.println("9、子类方法");
return 2;
}
private static int staticMethod() {
System.out.println("10、子类静态方法");
return 2;
}
public static void main(String[] args) {
}
}
类的初始化
例子如上,我们执行上述的代码,main方法里面什么都没有,会有打印产生吗?
执行结果如下:
5、父类静态方法
1、父类静态代码块
10、子类静态方法
6、子类静态代码块
为什么main方法里面什么都没有也会打印呢?
因为
- 1、main方法所在的类优先加载并初始化,固会加载初始化Son这个类。
- 2、Son继承自Father,所以又会优先初始化Father这个类。
3、Father从上依次往下初始化静态变量和静态代码块。
- 先给静态变量j赋值,调用静态方法staticMethod。
- 执行静态代码块,打印
1、父类静态代码块
4、Father加载完之后,加载Son,依然是从上往下依次初始化静态变量和静态代码块
- 给静态变量j赋值,调用静态方法staticMethod
- 执行静态代码块,打印
6、子类静态代码块
实例初始化
修改main方法,如下:
public static void main(String[] args) {
Son son = new Son();
}
执行结果如下:
5、父类静态方法
1、父类静态代码块
10、子类静态方法
6、子类静态代码块
4、父类方法
3、父类代码块
2、父类构造器
9、子类方法
8、子类代码块
7、子类构造器
前四个是毋庸置疑的,那么main方法创建对象(new)时,此时是实例初始化。JVM为每一个类的每一个构造方法都创建一个()方法,用于初始化实例变量,由虚拟机自行调用。
- 执行方法,首行是super(),所以执行父类的方法。
从上至下执行非静态变量、非静态代码块
- 初始化非静态变量i,调用方法method
- 初始化非静态代码块,打印
3、父类代码块
- 最后执行构造器
执行完父类,继续执行Son
- 初始化非静态变量i,调用方法method
- 初始化非静态代码块,打印
8、子类代码块
- 最后执行构造器
多实例初始化
修改main方法
public static void main(String[] args) {
Son son = new Son();
System.out.println("---------------------------wangscaler-----------------------------------");
Son son1 =new Son();
}
}
打印如下
5、父类静态方法
1、父类静态代码块
10、子类静态方法
6、子类静态代码块
4、父类方法
3、父类代码块
2、父类构造器
9、子类方法
8、子类代码块
7、子类构造器
------------------------------wangscaler--------------------------------
4、父类方法
3、父类代码块
2、父类构造器
9、子类方法
8、子类代码块
7、子类构造器
由上可以看出,多个实例就有多个方法,执行过程同实例初始化,就不过多介绍。
重写下的初始化
我们知道final、private修饰的方法和静态方法不能被子类重写。于是我们在实例初始化的代码情况下修改Father。
public int method() {
System.out.println("4、父类方法");
return 1;
}
当然子类的method上需要添加注解@Override,因为此时的子类变成了重写父类的method方法。
这次的执行结果是:
5、父类静态方法
1、父类静态代码块
10、子类静态方法
6、子类静态代码块
9、子类方法
3、父类代码块
2、父类构造器
9、子类方法
8、子类代码块
7、子类构造器
与实例初始化情况不同的是,第五个打印语句,为什么呢?
因为在执行父类的方法的时候,当初始化非静态变量i时,调用方法this.method(),而this指得是正在创建的对象Son,所以执行的是重写之后的method方法。
初始化总结
遇到new、getstatic、putstatic、或者invokestatic 这4条字节码指令,进行初始化。使用java.lang.reflect包的方法,对垒进行反射调用的时候,如果没有初始化,则先触发初始化。当使用JDK1.7的动态语言支持时,如果一个Java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_outStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
类的初始化:
- main方法所在类优先加载初始化
- 子类初始化时,优先初始化父类
类的初始化就是执行方法
- 由静态变量和静态代码块组成。
- 从上至下执行。
- 只执行一次。
实例的初始化:
类创建实例(new)初始化,执行方法
- 的首行是super(),对应父类的方法
- 由非静态变量、非静态代码块、构造器组成
- 非静态变量、非静态代码块从上至下依次执行、构造器最后执行
- 有几个构造器就有几个方法
重写的方法
- 子类重写了父类的方法,那么在子类中调用的一定是重写之后的代码
- 父类中的非静态的方法默认调用的调用对象是this,this在构造器或者方法中,指的就是正在创建的对象。
注意:以下条件下没有方法:
- 没有初始化语句或静态初始化语句初始化;
- 仅包含static、 final修饰的类变量,并且类变量初始化语句是常量表达式;
参考文档
- [1] java虚拟机符号引用验证_深入了解Java虚拟机---虚拟机类加载机制
- [2] java类到底是如何加载并初始化的?
- [3] java类什么时候初始化?
- [4] 简述JAVA类的生命周期
- [5] jvm基础第三节: 与 方法