类的初始化和加载
类初始化
- 遇到new,或读取、修改或调用一个类的static变量时。
- 反射调用。
- 初始化一个类时若父类没有被初始化,会初始化父类。
- 虚拟机启动时,用户指定的运行的主类。
只有遇到这四种情况时才会进行类的初始化,叫做主动引用,其他情况均不会发生类的初始化,叫做被动引用。
被动引用:
- 通过子类调用父类的static变量,子类不会被初始化,父类会被初始化。
- 通过类名调用final static 常量,类不会被初始化。
- 通过数组定义类,如Object [] obj = new Object[5];,不会发生初始化。
接口初始化:
- 类初始化要求父类必须初始化,接口不要求父接口初始化。
- 接口里的变量(final static)调用时会进行接口初始化。
ConstantValue属性:
属于类文件结构里的内容。作用是通知虚拟机为静态变量初始化。对于实例变量(非static变量)的初始化在实例构造器里进行。这里所谓的静态变量并不要求有final修饰,但是是对于javac编译器要求要有final修饰。所以一般情况下,ConstantValue针对final static 修饰的基本类型和String。
static 、final、static final修饰字段赋值的区别:
- static修饰的字段在类加载过程中的准备阶段被初始化为0或null,在初始化阶段(构造器)中被赋予指定的值。
- final 在运行时被初始化
- static final 修饰的字段在 Javac 时生成 ConstantValue 属性,在类加载的准备阶段根据ConstantValue的值为该字段赋值,它没有默认值,必须显式地赋值,可以认为编译时被放进常量池。
类加载机制
类加载过程:
加载——验证——准备——解析——初始化
加载:获取class文件二进制流,并把其中的静态结构写入方法区的运行时数据结构中,在对上生成java.object.class对象,作为方法区对象的访问入口。加载阶段可以用系统提供的加载器,也可以自己实现。在这里,相同的class文件用不同的加载器来实现得到的两个结果不相等。这里的类指的是非数组类,数组类不通过加载器加载,而是通过Java虚拟机直接创建。但是数组中的元素类型是由加载器加载。
-
验证:
- 文件格式的验证:验证字节流是否符合Class字节流规范,并能被当前虚拟机处理。目的是为了输入的字节流能被正确地解析并写入方法区。
元数据结构验证:验证各数据类型是否符合规范。
字节码验证:进行数据流和控制流分析,保证在运行的类的方法不会产生危害虚拟机的行为。
符号引用验证:发生在虚拟机将符号引用转换为直接引用时,进行对类和常量池符号引用的匹配校验。
-
准备:
为类变量(static)分配空间和初始化阶段。- static:进行内存分配,并进行默认赋值。具体赋值发生在出发类的构造器中。
- final static:生成ConstantValue属性,赋值并放入常量池。
- 局部变量和final static常量使用前必须赋值,否则编译不通过。
-
解析:
将常量池中的符号引用转换成直接引用的过程。解析主要针对类或接口、字段、类方法、接口方法四种符号引用。- 类或接口解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
- 字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类。
- 类方法解析:对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。
- 接口方法解析:与类方法解析步骤类似,只是接口不会有父类,因此,只递归向上搜索父接口就行了。
class Super{
public static int m = 11;
static{
System.out.println(m);
}
}
class Father extends Super{
public static int m = 33;
static{
System.out.println(m);
}
}
class Child extends Father{
static{
System.out.println("Child");
}
}
public class Test{
public static void main(String[] args){
System.out.println(Child.m);
}
}
执行结果:
11
11
33
- 初始化:
初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的 Java 程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器<init>()方法的过程。方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。
方法与实例构造器<init>()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<init>()方法执行之前,父类的<init>()方法已经执行完毕。因此,在虚拟机中第一个被执行的<init>()方法的类肯定是java.lang.Object。
方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<init>()方法。
接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成<init>()方法。但是接口与类不同的是:执行接口的<init>()方法不需要先执行父接口的<init>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<init>()方法。
虚拟机会保证一个类的<init>()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<init>()方法,其他线程都需要阻塞等待,直到活动线程执行<init>()方法完毕。如果在一个类的<init>()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。
class Father{
public static int a = 1;
static{
a = 2;
}
}
class Child extends Father{
public static int b = a;
}
public class ClinitTest{
public static void main(String[] args){
System.out.println(Child.b);
}
}
执行上面的代码,会打印出 2,也就是说 b 的值被赋为了 2。
我们来看得到该结果的步骤。首先在准备阶段为类变量分配内存并设置类变量初始值,这样 a和b 均被赋值为默认值 0,而后再在调用()方法时给他们赋予程序中指定的值。当我们调用 Child.b 时,触发 Child 的()方法,根据规则 2,在此之前,要先执行完其父类Father的()方法,又根据规则1,在执行()方法时,需要按 static 语句或 static 变量赋值操作等在代码中出现的顺序来执行相关的 static 语句,因此当触发执行 Fathe r的()方法时,会先将 a 赋值为 1,再执行 static 语句块中语句,将 a 赋值为 2,而后再执行 Child 类的()方法,这样便会将 b 的赋值为 2。
如果我们颠倒一下 Father 类中“public static int a = 1;”语句和“static语句块”的顺序,程序执行后,则会打印出1。很明显是根据规则 1,执行 Father 的()方法时,根据顺序先执行了 static 语句块中的内容,后执行了“public static int a = 1;”语句。
另外,在颠倒二者的顺序之后,如果在 static 语句块中对 a 进行访问(比如将 a 赋给某个变量),在编译时将会报错,因为根据规则 1,它只能对 a 进行赋值,而不能访问。