(六)类的“加载”过程

一、前言

一个类的生命周期包括 “加载”、使用、卸载 三个过程,而一个类的“加载”过程又可以依次细分为 加载、验证、准备、解析、初始化 五个步骤。

以下五种情况必须立即对类进行这五个“加载”步骤来“初始化”这个类: 

  •  遇到new、getstatic、putstatic或invokestatic这四条字节码指令,且这个类没有进行初始化。
  • 使用java.lang.reflect包中的方法对类进行反射调用时,且这个类没有进行初始化。
  • 初始化一个类的时候,如果这个类的父类没有初始化,先对父类立即进行初始化,这个过程可以俄罗斯套娃。
  • 虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • java.lang.invoke,MethodHandle实例最后的解析结果有 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这些方法句柄对应的类没有进行初始化,则对这个类进行初始化。

二、加载

1、先加载这个类的的二进制字节流

2、再将这个类中的静态储存结构转化为方法区的运行时数据结构

3、最后内存中生成这个类  对应的 java.lang.Class对象,作为方法区这个类的各种数据的访问入口

三、验证

1、文件格式验证:验证这个文件是否已魔数 OXCAFEBABE开头;检查虚拟机是否可以运行这个版本号的文件;检查是否有不被支持的常量等。

2、元数据验证:对字节码描述的信息进行语义分析,验证是否符合Java语言规范,比如这个类是否有父类,是不是继承了不能被继承的类等等。

3、字节码验证:通过数据流和控制流分析确定程序语义是否合法,是否复合逻辑。

4、符号引用验证:在虚拟机符号引用解析为直接引用时,对类自身以外(常量池中的各种引用)的信息进行匹配性校验是否有错误。

四、准备

准备阶段是正式为类变量(static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。如数值类型变量(byte、short、int、long、float、double)都将初始化为不同数据类型的0char类型变量初始化为'u0000',boolean类型变量初始化为false,对象引用变量初始化为null。  比如    static int a=2;   在准备阶段这个a会初始化为0,在最后的类初始化过程中才会把a赋值为2。  但是用 static final 修饰的类常量属性,在准备阶段就会初始化为指定的值,如 static final a=4,在准备阶段a会直接赋值为4。这个阶段就是给静态变量分配内存设置初值的阶段。

五、解析

 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。分别会对类和接口、字段、类方法和接口方法进行解析。

六、初始化

 类初始化阶段是类加载过程的最后一步。到了初始化阶段,才开始真正执行类中定义的Java程序代码。初始化阶段是执行类构造器<clinit>()方法的过程

1、<clinit>()方法是由编译器自动收集类中的所有static修饰的类变量的赋值动作和静态语句块(static{ }块)中的语句合并产生的,其语句执行顺序和语句在源文件中出现的顺序决定。静态语句块可以给这个静态语句块之后的static变量赋值,但是不能对后面的static进行访问,如打印这个变量的值就是非法的。

2、<clinit>()方法不同于类的构造方法(<init>()方法),它不需要显示地调用父类构造器,虚拟机会先确保这个类地父类先调用 <clinit>()方法。所以第一个被执行<clinit>()方法地类是 java.lang.Object。这也意味着父类中定义地静态语句块地执行要先于子类地静态语句块。

3、一个类如果没有静态语句块和对static变量地赋值操作,编译器可以不为这个类生成<clinit>()方法。

4、<clinit>()方法是线程安全的。

5、在一个类全部加载完成之后在new一个类的实例时会调用类的构造方法 <init>() ,<init>()方法 满足(1)执行顺序依据源文件的代码顺序(2)父类先行(可套娃):先调用父类<init>()->自身的代码块与变量赋值->自身的构造方法中的代码块

实例代码:

public class Father {
    public static int a=1;
    public int b=10;
    Father(){
        System.out.println("父类的构造方法执行了");
        this.test2();
        System.out.println("父类的构造方法结束了");
        System.out.println("-------------------");
    }
    static{
        System.out.println("父类的static代码块执行了");
        System.out.println("a="+a);
        System.out.println("父类的static代码块结束了");
        System.out.println("-------------------");
    }
    {
        System.out.println("父类的实例方法块执行了");
        System.out.println("b="+b);
        System.out.println("父类的实例方法块结束了");
        System.out.println("-------------------");
    }

    public void test2(){
        System.out.println("父类的test2方法执行了");
    }
}
public class Son extends Father{
    public static int a=2;
    public int b=101;
    Son(){
        System.out.println("子类的构造方法执行了");
        this.test2();
        System.out.println("子类的构造方法结束了");
        System.out.println("-------------------");
    }

    static{
        System.out.println("子类的static代码块执行了");
        System.out.println("a="+a);
        System.out.println("子类的static代码块结束了");
        System.out.println("-------------------");
    }

    {
        System.out.println("子类的实例方法块执行了");
        System.out.println("b="+b);
        System.out.println("子类的实例方法块结束了");
        System.out.println("-------------------");
    }

    @Override
    public void test2(){
        System.out.println("子类的test2方法执行了");
    }

    public static void main(String[] args) {
        new Son();
    }
}

输出结果:

父类的static代码块执行了
a=1
父类的static代码块结束了
-------------------
子类的static代码块执行了
a=2
子类的static代码块结束了
-------------------
父类的实例方法块执行了
b=10
父类的实例方法块结束了
-------------------
父类的构造方法执行了
子类的test2方法执行了 (这里调用了子类中声明的覆盖了父类的test2()方法。要调用这个父类中的test2()方法,需要在子类中使用super关键字显式的调用。这感觉是个设计缺陷,这样用很容易导致bug。)

父类的构造方法结束了
-------------------
子类的实例方法块执行了
b=101
子类的实例方法块结束了
-------------------
子类的构造方法执行了
子类的test2方法执行了
子类的构造方法结束了
-------------------

原文地址:https://www.cnblogs.com/jianguan/p/14493305.html