类初始化
类初始化阶段是类加载过程的最后一步。此前,无论是在加载、连接(验证、准备、解析)阶段,如果没有使用自定义的加载器,我们编写的代码(字节码的形式)几乎没有运行,其动作完全由虚拟机主导和控制。因此,初始化阶段才是Java程序代码(字节码)真正执行的阶段。
编译器会帮我们生成一个cinit( )方法,它是编译器通过自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的一个方法。所以,cinit( )方法实际上相当于初始化赋值方法,但是cinit( )方法不同于构造函数,它不需要显式的调用父类的cinit方法,但是会保证子类的cinit( )方法会在父类的cinit( )执行完毕后执行,所以虚拟机中第一个执行的cinit( )方法一定是Object类的。
cinit( )方法不是不可或缺的,当程序中没有静态代码块,也没有赋值操作,那么编译器完全可以不用生成cinit( )方法。
接口中没有静态代码块,但是接口可以有赋值操作,所以接口也可以有cinit( )方法,但是接口的cinit( )方法执行的时候不用执行它的父接口的cinit( )方法,只有当用到父接口中的变量时,才会执行父接口的cinit( )方法。
多线程环境下,cinit( )方法是同步的,多个线程会竞争方法的锁资源,同一时间只能有一个线程去执行cinit( )方法,其他线程会阻塞。
何时会导致类的初始化? 答:当类第一次真正被使用的时候。
有以下6种情况:
1.创建类的实例(比如,通过new关键字创建实例)
2.访问某个类或者接口的静态变量。或者对其静态变量赋值;
3.调用类的静态方法
4.反射
5.初始化一个类的子类,也会初始化该类的父类
6.JVM启动时被标记为启动类的那些类
除了这6种,其他对类的使用都不是主动使用,不会导致类的初始化。
根据上述知识,来看一个类加载次序的例子:
public class TestClassLoader1 {
public static void main(String[] args) {
Count c = Count.getCount();
System.out.println(c.num1);
System.out.println(c.num2);
}
}
//定义一个内部类Count
class Count{
private static Count count = new Count(); //位置1
public static int num1;
public static int num2 = 0;
//private static Single single = new Single(); //位置2
Count(){
num1++;
num2++;
}
public static Count getCount(){
return count;
}
}
上述程序运行时,首先会执行main方法,在main方法中,调用了Count类的静态方法getCount,符合上述6种情况的第3种,所以会导致Count类被加载到内存,加载的时候在连接的准备阶段,静态变量就已经被赋予默认初始值(count=null,num1=0,num2=0),由于主动使用会导致类的初始化,所以还会将用户赋予的正确值赋予它们,程序顺序执行,首先给count变量复制,new关键字调用构造方法,num1和num2都自加了一次,都等于1,接着,num1用户没有对其赋值,不用初始化,num2用户对其赋值为0,所以num2又变为0。
所以,程序最后输出的是1,0。
上述程序,如果把位置1的语句移到位置2,程序的输出结果就将变成1,1,分析过程和上面是一致的。
对于上述主动使用类的第5种情况:初始化一个类的子类时,也会初始化它的父类,有下面的例子验证
import java.util.Random;
public class TestClassLoader2 {
public static void main(String[] args) {
System.out.println(T2.y);
}
}
class T1 {
public static int x = 1;
static{
System.out.println("T1 block");
}
}
class T2 extends T1{
public static int y = 1; //位置1
static{
System.out.println("T2 block");
}
}
分析:首先在初始化T2之前,会先初始化它的父类T1,输出“T1 block”,然后初始化T2,输出“T2 block”,最后输出main方法的值1,最终的输出结果就是:
T1 block
T2 block
1
对于含有final修饰的变量,如果在编译的时候可以确定其确切的值,则就会被加入到调用类(本例是TestClassLoader2类)的常量池中,所以对其的访问不算对该类的主动使用,下面的例子可以验证这一点:
例1:还是上面的例子,把位置1的语句改为:
public static final int y = 1;<span style="white-space:pre"> </span>//位置1
那么程序的输出结果为:1 .(分析:编译的时候y的值已经可以唯一的确定,不会改变的,实质就是一个常量,所以对其的访问不会导致T2类的初始化,也就不会导致 T1类的初始化)
特别注意,子类引用父类中的静态字段,只会触发父类初始化,而不会导致子类初始化。也就是本例中如果main()方法是打印T2.x,则不会输出"T2 block"。
例2:还是上面的例子,把位置1的语句改为:
public static final int y = new Random().nextInt(100); //位置1
那么程序的输出结果为:
T1 block
T2 block
76
原因就是对于静态变量y的值在编译期间不能确定,所以y然是一个静态变量,对其的访问会导致类T2、T1的的初始化。
例子3:当程序访问的静态变量和静态方法确实在当前类或接口中定义时,才可认为是对当前类或接口的主动使用.
public class TestClassLoader3 {
public static void main(String[] args) {
System.out.println(Child.x);
}
}
class Parent{
public static int x;
static{
System.out.println("Parent Block");
}
}
class Child extends Parent{
static{
System.out.println("Child Block");
}
}
程序的输出结果为:(原因是对Child的静态变量x的访问不是真正访问Child类的静态变量,而是其父类的静态变量,所以只会初始化其父类)
Parent Block
0
当JVM初始化一个类的时候,要求它的所有父类已经被初始化,但这条规则并不适用于接口。初始化一个类时,并不会初始化它所实现的接口;初始化一个接口时,也不会初始化它已实现的父接口,只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。