JAVA类加载流程

发布于:2023-01-18 ⋅ 阅读:(531) ⋅ 点赞:(0)

类加载流程

类加载流程描述了类加载器把 class 文件加载到虚拟机的过程。class 文件像是一个模板,被加载到 JVM 之后,可以被复刻为多个实例。

类加载流程包括 “加载“,”链接“,”初始化“ 三个步骤, 其中 ”链接“ 包括了 ”验证“,”准备“,”解析“三个小步骤,最终加载到方法区。

加载

类加载过程大致可描述为三步:

  1. 通过获取类的全限定名来获取类的二进制字节流(以物理磁盘为例),全限定名指的是:包名+类型,如:test.World;
  2. 将二进制字节流表示的静态存储结构转化为方法区的运行时数据结构,用于实例化时放到运行时数据区对应的位置使用;
  3. 在内存中生成代表这个类的 java.lang.class 对象,作为方法区这个类的数据访问入口。
package test

class World {
    
}

JVM 加载类之前会提前判断该类是否已经被加载。如果被加载了,则直接跳过了加载流程。

链接

”链接“ 包括了 ”验证“,”准备“,”解析“三个小步骤。

验证

验证的目标主要是保证加载之后的 class 二进制字节流数据符合虚拟机的要求,保证被加载类的正确性,不会危害虚拟机的自身安全。验证的内容主要包括四种:

  • 文件格式验证;
  • 元数据验证;
  • 字节码验证;
  • 符号引用验证。

准备

准备阶段主要完成对类变量的初始化:

  • 对类变量分配内存并且做默认初始化,即被 static 修饰的的变量;
  • 对类常量做显示初始化,即被 final 修饰的变量,对于此类变量,在编译的时候已经做了内存分配和默认初始化。
package test

class World {
    private static int a = 1; // 在准备阶段只是做默认初始化,赋值为 0
    public static void main(String[] argv) {
        System.out.println(a);    
    }
}

需要注意的是:在准备阶段不会对实例变量做初始化,实例变量是在实例化的时候进行初始化,并且是在堆空间,而类变量是在方法区。

解析

解析过程主要负责将常量池中的符号引用转化为直接引用。

初始化

初始化过程实际上是执行类构造器方法 clinit 的过程。该方法不需要定义,是 javac 根据类中所有的类变量的显示赋值和静态代码块汇总而成。

注意:clinit 和 init 方法不同,init 方法是根据类的构造器编译生成的。

  • 构造器方法按语句在文件中出现的顺序执行。
package test

class World {
    private static int a = 1; // 在准备阶段只是做默认初始化,赋值为 0
    static {
        a = 10;
        b = 20;    
    }
    
    private static int b = 10;
    public static void main(String[] argv) {
        System.out.println(World.a);  
        System.out.println(World.b);  
    }
}

在链接的准备阶段,类变量 a 和 b 已经被分配内存并且进行默认初始化,在初始化阶段,按照语句在文件中出现的顺序执行,b 会被赋值为 20,然后再被赋值为 10;从字节码中中可以得到验证。

  • 如果没有静态参数或者静态代码块是不执行类构造器方法 clinit 的。
package test

class World {
    private int b = 10;
    public static void main(String[] argv) {
        int a = 20; 
    }
}

从编译之后的字节码来看,并没有 clinit 方法,也就不存在执行 clinit 过程。

  • 若某个类具有父类,JVM 会保证该类在执行 clinit 方法之前,其父类的 clinit 方法已经执行完毕。
package test;

public class World {

    static class Fu {
        public static int a = 10;
        static {
            a = 20;
        }
    }

    static class Zi extends Fu {
        public static int b = 1;
        static {
            b = a;
        }
    }
    public static void main(String[] argv) {
        System.out.println(Zi.b);
    }
}

加载 Zi 类的时候会提前加载 Fu 类,会执行 Fu 类的 clinit 方法(按顺序执行),此时 a = 20;再加载 Zi 执行 clinit 方法(按顺序执行)b = a = 20。

  • 虚拟机保证一个类的 clinit 方法在多线程下被同步加锁。保证类只会被加载一次。
package test;

class Hello {
    static {
        System.out.println("Hello 被加载了");
    }
}

public class World {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Hello hello = new Hello();
                System.out.println(Thread.currentThread().getName() + "执行");
            }
        };

        Thread thread1 = new Thread(runnable, "1");
        Thread thread2 = new Thread(runnable, "2");
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
    }
}

尽管多个线程同时使用了 Hello 类,尝试了加载,但是静态代码块中的 "Hello 被加载了" 只是被打印了一次。

Hello 被加载了
1执行
2执行