类加载过程(三)

类加载全过程:加载、验证、准备、解析、初始化

一、初始化

初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中自定义的Java程序代码(或是字节码)。

二、<clinit>()方法

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以说:初始化阶段是执行类构造器<clinit>方法的过程。

1)<clinit>()方法执行特点
  • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定影在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
此时编译器会给出“非法向前引用”的提示
此时编译器会给出“非法向前引用”的提示
  • <clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显式的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object
  • 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
//父类
public class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
}
//子类
public class Sub extends Parent {
    public static int B = A;
}
//测试
public class Test {
    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}
运行结果,字段B的值为2,而不是1
  • <clinit>方法对于类或接口来说并不是必须的,如果一个类中没有静态代码块,也没有对变量的复制操作,俺么编译器可以不为这个类生成<clinit>方法
  • 接口中不能使用静态代码块,但仍有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法
  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成对多个进程阻塞(当执行<clinit>方法的线程退出,其他线程被唤醒后不会进入<clinit>方法。同一个类加载器下,一个类型只会初始化一次),实际中这种阻塞往往是隐蔽的
public class DeadLoopClass {
    static {
     //此处必须使用if判断,否则编译器将拒绝编译
        if (true){
            System.out.println(Thread.currentThread()+"init DeadLoopClass");
            while (true){} //模拟长时间操作
        }
    }

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
            System.out.println(Thread.currentThread()+"start");
            DeadLoopClass deadLoopClass = new DeadLoopClass();
            System.out.println(Thread.currentThread()+"end");
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.start();
        thread2.start();
    }

}

首先要注意一点,上述代码中的静态代码块里必须使用if判断才能编译通过

Error:(4, 5) java: 初始化程序必须能够正常完成

首先,先来看模拟线程长时间运行的结果:

线程阻塞时的运行结果, 只有主线程执行了本类的<clinit>()方法,其他线程被阻塞

当我们注释掉“while (true){}”,得到的运行结果如下:

即便通过多线程初始化同一个类,同一类加载器下,<clinit>方法也只会被执行一次

有读者会发现上面两图的运行结果和《深入理解Java虚拟机》中的结果略有不同,这是因为笔者使用的开发工具是IDEA,并非书中作者所使用的Eclipse。这两个开发工具在开辟线程时略有不同,详见:

杂谈——IDEA与Eclipse的线程开辟区别

发表评论

邮箱地址不会被公开。 必填项已用*标注