引言
一星期一更的节目可能会迟到,但永远不会缺席!!!
参考书籍:“深入理解java虚拟机”
初始化(Initiallization)
初始化阶段,简言之,为类的静态变量赋予正确的初始值。
具体描述:
类的初始化是类装载的最后一个阶段,如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行Java字节码。(即:到了初始化阶段,才真正开始执行类中定义的Java程序代码。)
初始化阶段的重要工作是执行类的初始化方法:clinit()方法
- 该方法仅能由Java编译器生成并由JVM调用,程序开发者无法自定义一个同名的方法,更无法直接在Java程序中调用该方法,虽然该方法也是由字节码指令所组成。
- 它是由类静态成员的赋值语句以及static语句块合并产生的。
在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的的clinit总是在子类clinit之前被调用。也就是说,父类的static块优先级高于子类
java编译器并不会为所有的类都产生clinit()初始化方法。哪些类在编译为字节码后,字节码文件中将不会包含clinit()方法?
- 一个类中并没有声明任何的静态类变量,也没有静态代码块
- 一个类中声明静态类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
- 一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式
也就是说,一个类是否进行初始化可以通过字节码文件是否生成clinit()方法来判断,那么上篇留下的疑问,我们结合下面这些案例就可以很好地去理解。
案例1
一个类中并没有声明任何的静态类变量,也没有静态代码块
public class ClassLoaderDemo5 {
}
没有生成clinit方法
案例2
一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式
public class ClassLoaderDemo5 {
public static final int a = 1;
}
没有生成clinit方法(需要注意的是,这里的赋值过程其实就是在上篇文章中说的准备(Preparation)过程完成的!)
案例3
一个类中声明静态类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
public class ClassLoaderDemo5 {
public static int a;
}
案例4(从这个案例开始有初始化方法)
一个类中声明静态类变量,有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
public class ClassLoaderDemo5 {
public static int a;
static {
a = 10;
}
}
在clinit方法中进行了a的初始化操作
案例5
一个类中包含static final修饰的非基本数据类型的字段
public class ClassLoaderDemo5 {
public static final Integer a =10;
}
同样也是在clinit方法中进行的a的初始化
案例6
一个类中有静态代码块
public class ClassLoaderDemo5 {
static {
int a = 10;
}
}
clinit方法的线程安全性:
对于clinit()方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性。
虚拟机会保证一个类的clinit()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程执行clinit()方法完毕。
正是因为函数clinit()带线程安全的,因此,如果在一个类的clinit()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息。
如果之前的线程成功加载了类,则等在队列中的线程就没有机会再执行clinit()方法了。那么,当需要使用这个类时,虚拟机会直接返回给它已经准备好的信息。