整理:类加载过程 — 初始化

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

        虚拟机规范严格规定了有且只有5种情况必须立即对类进行初始化

  1. 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类尚未初始化,则需要先触发其初始化。即:使用new关键字实例化对象时,读取或设置一个类的静态字段时(final修饰、已在编译期把结果放入常量池的静态字段除外),调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类尚未初始化,则需要先触发其初始化。
  3. 初始化一个类的时候,发现其父类尚未初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个主类。
  5. 使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类尚未初始化,则需要先触发其初始化。

        这5种场景中的行为称为对一个类进行主动引用;除此之外所有引用类的方式都不会触发初始化,称为被动引用。

public class Test {
    static class Sup {
        public static int i = 1;
        static {
            System.out.println("sup init");
        }
    }

    static class Sub extends Sup {
        static {
            System.out.println("sub init");
        }
    }

    public static void main(String[] args) {
        System.out.println("Test main: " + Sub.i);
    }
}
使用子类引用调用父类的静态字段,不会触发子类的初始化。输出结果:
  sup init
  Test main: 1

        常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

public class MyTest {
    static class Sup {
        public final static int I = new Integer(1);
        public final static int J = 2;	
        static {
            System.out.println("sup init");
        }
    }

    public static void main(String[] args) {
        System.out.println("Test main: " + Sup.I);
    //  System.out.println("Test main: " + Sup.J);
    }
}
输出结果:
  sup init
  Test main: 1
注释代码输出结果:
  Test main: 2

        在准备阶段,类变量被系统设置了初始值(类型变量的默认零值),而在初始化阶段,则根据程序代码的设置,进行初始化类变量和其他资源。或者说:初始化阶段是执行类构造器()方法的过程。

        ()方法执行过程中可能会影响程序运行行为的特点及细节:

  • ()方法由编译器自动收集类中所有类变量赋值动作和静态语句块(static{...})语句合并产生;收集顺序由语句在源文件中出现的顺序所决定。静态语句块能够访问在其之前定义的变量,在其之后定义的变量,可以赋值,但无法访问。
  • public class Test {
        static {
            // 给变量赋值可以编译通过
            i = 0;
            // 编译器提示:Cannot reference a field before it is defined 非法向前引用
            System.out.println("i: " + i);
            // 可以编译通过
            System.out.println("i: " + Test.i);
        }
        private static int i = 1;
    }
  • ()方法与类的构造函数(或者说:实例构造器()方法)不同,它不需要显示的调用父类构造器,虚拟机会确保在子类的()方法执行之前,父类的()方法已经执行完毕。java.lang.Object是虚拟机中第一个执行()方法的类。父类的()方法先执行,意味着父类定义的静态语句块要优先于子类的变量赋值操作。
  • public class Test {
        static class Sup {	
            public static int i = 1;
            static {
                System.out.println("sup static before: " + i);
                i = 2;
                System.out.println("sup static after: " + i);
            }
        }
    
        static class Sub extends Sup {	
            public static int j = 3;
            static {
                System.out.println("sub static before: " + j);
                j = 4;
                System.out.println("sub static after: " + j);
            }
        }
    
        public static void main(String[] args) {
            System.out.println("Test main sub j: " + Sub.j);
        }
    }
  • 输出结果:
      sup static before: 1
      sup static after: 2
      sub static before: 3
      sub static after: 4
      Test main sub j: 4
  • ()方法对于类或接口不是必须的。一个类中没有静态代码块,也没有对变量的赋值操作,编译器可以不为类生成。

  • 接口中不能使用静态语句块,但可以有变量初始化的赋值操作,因此接口也与类一样都会生成()方法。但接口与类不同的是:执行接口的()方法不需要先执行父接口的()方法,只有当父接口中定义的变量使用时,才会初始化。接口的实现类在初始化时也不会执行接口的()方法。

  • 虚拟机会保证一个类的()方法在多线程环境中被正确的加锁、同步,如果多个线程同时初始化一个类,只有一个线程会执行这个类的()方法,其他线程都会阻塞等待,直到活动线程执行()方法完毕,其他线程唤醒后也不会再次执行()方法。同一个类加载器下,一个类型只会初始化一次。

        本文内容主要来源与《深入理解Java虚拟机》第2版 第7章(7.2 类加载的时机、7.35 初始化),用于学习、归纳、总结。

你可能感兴趣的:(Java之路)