虚拟机类加载--2.类的初始化
初始化是类加载过程的最后一步,但由于比较重要,故放在前面先讲。
在前面的连接(准确来说是准备)阶段,类的变量已经被赋予默认值(如int类型为0,布尔类型为false,引用类型为null等)。而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。或者可以从另一个角度来表达:初始化阶段是执行类构造器<clini>()方法的过程。
1. <clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,收集的顺序是由语句在源文件中出现顺序所决定。静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,可以赋值,但不能访问。如下将报错:
2. <clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此,在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object;
3.由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,如下将打印2
1 package com.khlin.initialization; 2 3 public class App { 4 5 public static void main(String[] args) { 6 System.out.println(Sub.b); 7 } 8 9 static class Parent { 10 public static int a = 1; 11 static { 12 a = 2; 13 } 14 } 15 16 static class Sub extends Parent { 17 public static int b = a; 18 } 19 }
4.<clinit>()方法对于类或接口并不是必需的,如果类没有静态语句块,也没有赋值动作,那么编译器可以不为这个类生成<clinit>()方法;
5.接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,所以接口和类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,才会初始化。另化,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法;
6.虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。单例的initialization on demand holder模式就是利用了这一特性。如果在一个类的<clinit>()方法中有耗时很长的操作,就有可能造成多个进程阻塞。需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出<clinit>()之后,其他线程唤醒后不会再次进入<clinit>()方法。同一个类加载器下,一个类型只会初始化一次。
1 package com.khlin.initialization; 2 3 public class EndlessLoop { 4 5 static { 6 if (true) { 7 System.out.println(Thread.currentThread() + " init EndlessLoop"); 8 while (true) { 9 } 10 } 11 } 12 13 }
1 public static void main(String[] args) { 2 Runnable runnable = new Runnable() { 3 4 @Override 5 public void run() { 6 System.out.println(Thread.currentThread() + " is running...."); 7 EndlessLoop endlessLoop = new EndlessLoop(); 8 System.out.println(Thread.currentThread() + " runs over"); 9 } 10 }; 11 12 Thread threadA = new Thread(runnable); 13 Thread threadB = new Thread(runnable); 14 threadA.start(); 15 threadB.start(); 16 }
结果会显示: