阅读 571

类的加载机制以及类、对象初始化的详细过程| 8月更文挑战

类的加载机制以及类、对象初始化的详细过程

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在构造器或者方法中,指的就是正在创建的对象。

注意:以下条件下没有方法:

  1. 没有初始化语句或静态初始化语句初始化;
  2. 仅包含static、 final修饰的类变量,并且类变量初始化语句是常量表达式;

参考文档

文章分类
后端
文章标签