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

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

WangScaler: 一个用心创做的做者。html

声明:才疏学浅,若有错误,恳请指正。java

java类的生命周期包括加载、链接(验证、准备、解析)、初始化、使用、卸载五个阶段。而解析阶段会在初始化以前或以后触发。类的加载不是随着jvm的启动而加载,而是随着使用动态的加载。缓存

接下来咱们首先了解下虚拟机的类加载机制。安全

虚拟机的类加载机制

咱们知道java的优点之一就是跨平台性,为何java能跨平台执行呢?就由于java是运行在java虚拟机jvm上的。那么jvm的类加载机制是怎样的呢?咱们知道java编译以后的文件是class文件,而虚拟机的类加载机制就是把Class文件加载到内存,进行校验,解析和初始化的过程。markdown

加载

加载就是经过类的全限定名来获取到class文件,将文件的二进制字节流转化成方法区的静态数据结构,而后在内存中生成这个类的class对象并在堆中生成一个便于用户调用的class类型的对象。数据结构

验证

验证就是对文件格式、元数据、字节码进行验证(即语法语义的验证)、符号引用的验证,确保Class文件中的字节流不会危害虚拟机的安全。可参考java虚拟机符号引用验证_深刻了解Java虚拟机---虚拟机类加载机制app

准备

给静态变量赋初值0。jdk8以前类的元信息、常量池、静态变量都是存储在永久代(方法区),而jdk8以后元空间(方法区)替代了永久代只存储类的元信息,将常量池和静态变量转移至堆内存中。jvm

解析

将符号引用替换成直接引用。解析阶段会在初始化以前或以后触发。ide

  • 一、假如A引用B(具体的实现类),编译阶段编译A的时候,是没法知道B是否被编译的,因此编译阶段B会被符号所代替,这个符号就是B的地址。在解析的时候若是B还没有加载,就会加载B,此时A中的符号将替换成真正的B的地址,这种称为静态解析,此时的解析是在初始化以前发生。oop

  • 二、若是A引用的是B的抽象方法或者接口。那么只有在调用A的时候才知道具体的实现类是哪个。此时的解析是发生在初始化以后的,也被成为动态解析。

  • 三、虚拟机能够对第一次的解析结果进行缓存,避免解析动做的重复执行。

初始化

类、对象的初始化顺序:(静态变量、静态代码块)>(变量、代码块)>构造器。

卸载

  • 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("一、父类静态代码块");
    }
​
    Father() {
        System.out.println("二、父类构造器");
    }
​
    {
        System.out.println("三、父类代码块");
    }
​
    private int method() {
        System.out.println("四、父类方法");
        return 1;
    }
​
    private static int staticMethod() {
        System.out.println("五、父类静态方法");
        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("六、子类静态代码块");
    }
​
    Son() {
        System.out.println("七、子类构造器");
    }
​
    {
        System.out.println("八、子类代码块");
    }
​
    public int method() {
        System.out.println("九、子类方法");
        return 2;
    }
​
    private static int staticMethod() {
        System.out.println("十、子类静态方法");
        return 2;
    }
​
    public static void main(String[] args) {
     
    }
}
​
复制代码

类的初始化

例子如上,咱们执行上述的代码,main方法里面什么都没有,会有打印产生吗?

执行结果以下:

五、父类静态方法
一、父类静态代码块
十、子类静态方法
六、子类静态代码块
复制代码

为何main方法里面什么都没有也会打印呢?

由于

  • 一、main方法所在的类优先加载并初始化,固会加载初始化Son这个类。

  • 二、Son继承自Father,因此又会优先初始化Father这个类。

  • 三、Father从上依次往下初始化静态变量和静态代码块。

    • 先给静态变量j赋值,调用静态方法staticMethod。
    • 执行静态代码块,打印一、父类静态代码块
  • 四、Father加载完以后,加载Son,依然是从上往下依次初始化静态变量和静态代码块

    • 给静态变量j赋值,调用静态方法staticMethod
    • 执行静态代码块,打印六、子类静态代码块

实例初始化

修改main方法,以下:

public static void main(String[] args) {
    Son son = new Son();
}
复制代码

执行结果以下:

五、父类静态方法
一、父类静态代码块
十、子类静态方法
六、子类静态代码块
四、父类方法
三、父类代码块
二、父类构造器
九、子类方法
八、子类代码块
七、子类构造器
复制代码

前四个是毋庸置疑的,那么main方法建立对象(new)时,此时是实例初始化。JVM为每个类的每个构造方法都建立一个()方法,用于初始化实例变量,由虚拟机自行调用。

  • 执行方法,首行是super(),因此执行父类的方法。

  • 从上至下执行非静态变量、非静态代码块

    • 初始化非静态变量i,调用方法method
    • 初始化非静态代码块,打印三、父类代码块
    • 最后执行构造器
  • 执行完父类,继续执行Son

    • 初始化非静态变量i,调用方法method
    • 初始化非静态代码块,打印八、子类代码块
    • 最后执行构造器

多实例初始化

修改main方法

  public static void main(String[] args) {
        Son son = new Son();
        System.out.println("---------------------------wangscaler-----------------------------------");
        Son son1 =new Son();
    }
}
复制代码

打印以下

五、父类静态方法
一、父类静态代码块
十、子类静态方法
六、子类静态代码块
四、父类方法
三、父类代码块
二、父类构造器
九、子类方法
八、子类代码块
七、子类构造器
------------------------------wangscaler--------------------------------
四、父类方法
三、父类代码块
二、父类构造器
九、子类方法
八、子类代码块
七、子类构造器
复制代码

由上能够看出,多个实例就有多个方法,执行过程同实例初始化,就不过多介绍。

重写下的初始化

咱们知道final、private修饰的方法和静态方法不能被子类重写。因而咱们在实例初始化的代码状况下修改Father。

public int method() {
    System.out.println("四、父类方法");
    return 1;
}
复制代码

固然子类的method上须要添加注解@Override,由于此时的子类变成了重写父类的method方法。

此次的执行结果是:

五、父类静态方法
一、父类静态代码块
十、子类静态方法
六、子类静态代码块
九、子类方法
三、父类代码块
二、父类构造器
九、子类方法
八、子类代码块
七、子类构造器
复制代码

实例初始化状况不一样的是,第五个打印语句,为何呢?

由于在执行父类的方法的时候,当初始化非静态变量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修饰的类变量,而且类变量初始化语句是常量表达式;

参考文档

相关文章
相关标签/搜索