java程序在对某个类进行引用、使用时,就会开始对该类进行加载,比如直接使用类加载器进行显式加载、创建该类的对象、使用该类的类变量等情况。类的加载是通过java虚拟机的类加载子系统完成的。类的加载主要分为三个阶段。

类的加载步骤

在这里插入图片描述

  1. 类加载子系统负责从文件系统或者网络上加载class文件,class文件在文件开头会有特定的文件标志。
  2. ClassLoader只负责class文件的加载,至于他是否可以运行,则有执行引擎决定。
  3. 加载的类信息存放在一块称为方法区的内存空间中,除了类信息外,方法区还会存放运行时常量池信息,还可能包括字符串字面量和数字字面量(这部分常量信息是Class文件中常量池部分的内存映射)。
加载Load阶段

这个阶段主要分为三步,由类加载器ClassLoader负责执行。

  1. 通过一个类的全限定名或者定义此类的二进制字节流。
  2. 将这些字节流中所代表的静态存储结构转化为方法区中的运行时数据结构。
  3. 在内存中生成一个代表这个字节码文件的java.lang.Class对象,作为方法区这个类的各种数据访问入口。

字节码文件的来源:

  • 从本地文件系统中直接加载。
  • 通过网络获取,比如Web Applet应用。
  • 从打包中获取。jar、war等。
  • 运行时计算生成,比如动态代理技术。
链接阶段Linking

链接阶段又分为三个阶段。

验证

目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害到虚拟机自身安全。主要包括4种验证:文件格式验证、元数据验证、字节码验证、符号引用验证。

准备阶段

该阶段为类变量分配内存并设置该类变量的默认初始值,比如对象引入设置为null,int型设置为0。这里不会包含使用final修饰的static,因为final在编译时就会分配内存了,准备阶段会显示初始化。这里也不会为实例变量分配内存以及初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配在java堆中。

解析阶段

将常量池内的符号引用转换为直接引用的过程。
关于符号引用和直接引用可以参考:
https://blog.csdn.net/mp252119282/article/details/82988504

事实上,解析操作往往会伴随着类在执行初始化完成之后再执行。

初始化阶段

初始化阶段执行类初始化方法clinit方法的过程。此方法不需要定义,是javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并而来。
也就是说这个阶段会对类变量进行初始化和对静态代码块内的代码执行。

demo

public class TestInit {

    private static int name = 5;
    static {
        name = 6;
    }

    public static void main(String[] args) {
        System.out.println(name);
    }
}

使用Idea的jclasslib插件对该文件反编译后的结构进行查看如下图。
在这里插入图片描述
clinit方法中字节码指令的意思是先把静态属性name赋值5,此时对应的是类中的name定义时赋值的代码。然后在把静态属性name赋值6,此时对应的是类中静态代码块的代码。

构造器方法中的指令是按照语句在源文件中出现的位置顺序执行。

public class TestInit {

    private static int name = 5;
    static {
        name = 6;
        value = 7;
    }

    private static int value = 6;

    public static void main(String[] args) {
        System.out.println(value);
    }
}

程序结果输出value的值为6,原因是value的内存分配以及默认值初始化在准备阶段已经执行了,而静态代码块是在初始化阶段执行,所以就算value定义的位置在静态代码块之后,程序依然不会报错,此时clinit方法的顺序是先把name赋值5、把name赋值6、把value赋值7、把value赋值6,按顺序由上往下执行,所以最终结果为6。
在这里插入图片描述
jclasslib反编译后如下图:
在这里插入图片描述

若该类具有父类,JVM会保证父类的clinit方法会在子类的clinit方法执行之前完全执行结束。
验证:

public class Father {

    static {
        System.out.println("Father执行开始");
        for (int i = 0;i==0;){

        }
        System.out.println("Father执行结束");
    }
}

public class Son extends  Father{
    private static int i = 0;
    static {
        System.out.println("Son执行开始");

        System.out.println("Son执行结束");
    }

    public static void main(String[] args) {
        System.out.println(i);
    }
}

上面的代码,在父类的静态代码块里面添加了一个死循环,不然父类初始化执行结束。
执行结果:
在这里插入图片描述
只输出了一个,子类clinit一值没有执行。因为父类clinit没有执行完成。

JVM保证一个类的clinit方法在多线程环境下会被同步加锁,也就是同一时间只有一个线程能够执行该方法,并且该clinit方法只会被执行一次。

验证:

public class TestInit {

    private static int name = 5;
    static {
        name = 6;
        value = 7;
    }

    private static int value = 6;

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Inner inner = new Inner();
            }
        },"t1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Inner inner = new Inner();

            }
        },"t2");
        t1.start();
        t2.start();
    }

    static class Inner{

        static {
            System.out.println(Thread.currentThread().getName() + "执行Inner类的初始化");
            //死循环
            for (int i = 0;i == 0;);
        }
    }
}

上面代码使用两个线程来加载Inner类。
结果:
在这里插入图片描述
只有线程t1进入了初始化,线程t2被同步锁阻塞在外面,实际上线程t2执行Inner类的初始化,因为类只会加载一次,所以也只会初始化一次。

当类中没有要初始化的代码,也就是说类中没有定义静态代码块,并且静态变量都是使用默认初始化,不显式初始化或者根本没有静态变量时,编译器不会生成clinit方法。


public class TestInit1 {

    private static int i;
}

结果:clinit不存在。
在这里插入图片描述

类加载完成。

特别说明

init方法实际上是我们类的对象构造方法,如果类中没有显示定义,JVM就会为我们默认生成一个无参构造方法,这也就你可以看到上面的截图中都是由一个init方法。

public class TestInit1 {

    private int i;

    public TestInit1(int i){
        this.i = i;
    }
    public TestInit1(){
        i = 5;
    }
}

上面代码定义了两个构造方法。
在这里插入图片描述
两个init方法。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐