JVM的类加载机制中,准备阶段和初始化阶段尤为重要,在程序设计中有时候起到至关重要的作用。因此今天来根据一个例子来讲解JVM中<init>和<clinit>的过程。
吊炸天的Java题目,请问输出结果是什么?
public class JavaTest {
public static void main(String[] args){
f1();
}
static JavaTest javaTest = new JavaTest();
static {
System.out.println("1");
}
{
System.out.println("2");
}
JavaTest(){
System.out.println("3");
System.out.println("a=" + a + ", b=" + b);
}
public static void f1(){
System.out.println("4");
}
int a = 100;
static int b = 200;
}
先给出正确答案:
2
3
a=100, b=0
1
4
init和clinit区别
①init和clinit方法执行时机不同
init是对象构造器方法,也就是说在程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行init方法,而clinit是类构造器方法,也就是在jvm进行类加载—–验证—-解析—–初始化,中的初始化阶段jvm会调用clinit方法。
②init和clinit方法执行目的不同
init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化。
<clinit>
在准备阶段,变量已经赋过一次系统要求的初始值(注意:如果这个类变量是static final的,那么在准备阶段就会根据程序员的意愿完成初始化,而非系统要求的初始值),而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。
编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。 因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。 但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。 只有当父接口中定义的变量使用时,父接口才会初始化。 另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法,只有使用了接口的类变量时才会执行接口的<clinit>。
类的初始化顺序:总结:包含父子类和接口类
普通类:
静态变量、静态代码块(这俩的优先级取决于编码的先后)
普通变量、普通代码块(这俩的优先级取决于编码的先后)
构造函数
继承的子类:
父类静态变量、父类静态代码块(这俩的优先级取决于编码的先后)
子类静态变量、子类静态代码块(这俩的优先级取决于编码的先后)
父类普通变量、父类普通代码块(这俩的优先级取决于编码的先后)
父类构造函数
子类普通变量、子类普通代码块(这俩的优先级取决于编码的先后)
子类构造函数
抽象的实现子类: 接口 - 抽象类 - 实现类
接口静态变量
抽象类静态变量、抽象类静态代码块(这俩的优先级取决于编码的先后)
实现类静态变量、实现类静态代码块(这俩的优先级取决于编码的先后)
抽象类普通变量、抽象类普通代码块(这俩的优先级取决于编码的先后)
抽象类构造函数
实现类普通变量、实现类普通代码块(这俩的优先级取决于编码的先后)
实现类构造函数
解题:
JavaTest程序的入口是public static void main,那么在调用这个main函数之前,需要执行类的加载过程,类加载成功后才会去调用main方法。
那么,
第一步:在类加载过程的准备阶段,先对b进行系统的赋值,b = 0。
第二步:在类加载过程的初始化阶段,执行<clinit>方法,那么先执行类变量的初始化,即:static JavaTest javaTest = new JavaTest();
第三步:在第二步的时候,<clinit>正在执行,而且此时进行类对象的初始化,会去调用<init>方法,因此会首先执行非静态代码块:System.out.println("2"),然后执行非静态变量的初始化:a = 100 (此时的先后顺序依照代码编写的先后顺序),然后执行构造函数:System.out.println("3");System.out.println("a=" + a + ", b=" + b);此时a的值为100,b的值还是0,因为<cinit>还只执行到static JavaTest javaTest = new JavaTest();
第四步:<init>方法已经执行完了,那么就接下来执行<cinit>剩余的部分,先执行类的静态代码块:System.out.println("2"),再执行类的静态变量初始化:static int b = 200。(此时的先后顺序依照代码编写的先后顺序)此时<cinit>方法就执行完成了。
第五步:<clinit>和<init>方法都已经执行完成了,类已经加载完成,此时就是函数的调用了,JavaTest的函数入口是main()方法,因此会调用静态方法f1():System.out.println("4"); 到此,整个程序就执行完成了。
综上分析,可以得出结果:
2
3
a=100, b=0
1
4