JVM类生命周期与类的加载过程
类的生命周期:
.class文件被加载到虚拟机内存后才可生效。
类加载过程严格按照上述顺序“开始”,但不是按照上述顺序“进行”或“完成”,可能会交错。
1、关于类初始化时机
当且仅当对一个类进行主动引用的时候才会触发初始化阶段。共有5种主动引用场景,详见文末参考文章。
其余被动引用不触发类的初始化,如:
1) 通过子类引用父类的静态字段,不会导致子类的初始化。
2)通过数组定义来引用类,不会触发该类的初始化。如:
public class NotInitialization{
public static void main(String[] args){
SClass[] sca = new SClass[10];
}
}
虚拟机并没有初始化Sclass, 而是触发了一个[Lcn.edu.tju.rico.SClass
类的初始化,其中"["代表数组,该类直接继承于Object,创建动作由字节码指令newarray触发。
3)常量在编译阶段会存入调用类的常量池中,不会触发定义该常量的类的初始化。如:
// 定义类
public class ConstClass{
static{
System.out.println("ConstClass init!");
}
public static final String CONSTANT = "hello world";
}
// 调用类
public class NotInitialization{
public static void main(String[] args){
System.out.println(ConstClass.CONSTANT);
}
}/* Output:
hello world
*///:~
2、进一步解释生命周期的每一步
1)加载
参加文末参考文章
2)验证
cafebabe,参见文末参考文章
3)准备阶段
对类的static变量的内存分配(jdk1.8: 元空间直接内存中), 与第一次初始化(零值)。
注意:若是final 的static变量,则初始化为定义时所赋的值。
4)解析
参加文末参考文章
5)初始化阶段
第二次对类变量初始化,这一次是按照程序员的意志。
执行类构造器<clinit>()
方法,整合,合并所有类变量的赋值动作和静态语句块,类似于对象创建(类的实例化)中的实例构造器<init>()
。
注意:静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。如下:
public class Test{ static{ i=0; System.out.println(i);//Error:Cannot reference a field before it is defined(非法向前应用) } static int i=1; }
那么注释报错的那行代码,改成下面情形,程序就可以编译通过并可以正常运行了:
public class Test{ static{ i=0; //System.out.println(i); } static int i=1; public static void main(String args[]){ System.out.println(i); } }/* Output: 1 *///:~
虚拟机会保证在子类类构造器<clinit>()
执行之前,父类的类构造<clinit>()
执行完毕。
虚拟机会保证一个类的类构造器在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。且,在同一个类加载器下,一个类型只会被初始化一次。
3、小结
结合java对象的创建,我们可以归纳,创建一个java对象需要经历如下几个阶段:
父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数。
4、案例分析(类的初始化与实例化的纠结)
public class StaticTest {
public static void main(String[] args) {
staticFunction();
}
static StaticTest st = new StaticTest();
static { //静态代码块
System.out.println("1");
}
{ // 实例代码块
System.out.println("2");
}
StaticTest() { // 构造函数
System.out.println("3");
System.out.println("a=" + a + ",b=" + b);
}
public static void staticFunction() { // 静态方法
System.out.println("4");
}
int a = 110; // 实例变量/成员变量
static int b = 112; // 静态变量
}
/* Output:
2
3
a=110,b=0 // 因为先进行了实例化,所以这里b是“准备”阶段的0值。
1
4
*///:~
我们看一下上述代码的执行过程:
- 首先第3行调用StaticTest类的静态方法,触发了该类的初始化。
- 类的初始化执行的第一步就是第6行语句。
- 由于第6行是==类的实例化,也就是说,这里将类的实例话嵌入到了类的加载过程之中,在准备阶段之后,类的初始化之前。==类似这样:
public class StaticTest { <clinit>(){ a = 110; // 实例变量 System.out.println("2"); // 实例代码块 System.out.println("3"); // 构造函数中代码的执行 System.out.println("a=" + a + ",b=" + b); // 构造函数中代码的执行 类变量st被初始化 System.out.println("1"); //静态代码块 类变量b被初始化为112 } }
所以就有了上述的输出。
结论:实例初始化不一定要在类初始化结束之后才开始初始化。
参考文章:https://blog.youkuaiyun.com/justloveyou_/article/details/72466105