title: 类加载机制(二):类的初始化
date: 2019-03-13 18:49:55
categories:
- Java虚拟机
tags: - 类加载机制
- 类的初始化
类的初始化
引言
一般Java
程序的class
文件经过加载、连接后,就进入初始化阶段,顺序执行static
语句,为静态变量赋予正确的值,执行static
代码块,初始化类。
类的使用方式
Java
程序对类的使用分为两种:
----主动使用
----被动使用
所有的Java
虚拟机实现必须在每个类或接口被Java
程序首次主动使用时才会初始化它们。
主动使用方式
主动使用分为七种:
----创建类的实例
----访问某个类或接口的静态变量,或者对该静态变量赋值
----调用类的静态方法
----反射(如Class.forName(com.test.Test)
)
----初始化一个子类
----Java
虚拟机启动时被标明为启动类的类
----JDK1.7
开始提供的动态语言支持:java.lang.invoke.MethodHandle
实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic
句柄对应的类没有初始化,则初始化。
除了以上七种情况,其他使用Java
类的方法都被看作是对类的被动使用,都不会导致类的初始化。
类的初始化步骤
对于类来说:
假如这个类还没有被加载和连接,那就先进行加载和连接
假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化父类
假如类中存在初始化语句,那就一次执行这些初始化语句
对于接口来说:
当Java
虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
在初始化一个类时,并不会先初始化它所实现的接口
在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
示例
NO.1
public class MyTest1 {
public static void main(String[] args) {
System.out.println(Son.str);
}
}
class Parent{
public static String str = "parent str";
static {
System.out.println("parent static启动");
}
}
class Son extends Parent{
static {
System.out.println("son static启动");
}
}
输出结果
输出结果显示只有
Parent
类被加载了。对于静态字段来说,只有直接定义了该字段的类才会被初始化。虽然Son
没有被主动使用,但它已经被加载了。类加载器并不需要等到某个类被首次主动使用时再加载它。
将Parent
类中的str
变量注释掉,添加到Son
类中
输出结果:
输出结果显示
Parent
类与Son
类都被初始化了。通过使用Son
的静态变量,导致Son
的初始化,而当一个类在初始化时,首先要求其父类全部都已经初始化,即导致Parent
初始化。
我们还可以从第一段打印类加载信息(通过添加虚拟机参数-XX:+TraceClassLoading
)看出,虽然Son
没有被主动使用,但它已经被加载了。类加载器并不需要等到某个类被“首次主动使用”时再加载它。
NO.2
public class MyTest2 {
public static void main(String[] args) {
System.out.println(Parent2.str);
System.out.println(Parent2.bi);
System.out.println(Parent2.si);
System.out.println(Parent2.icons_1);
System.out.println(Parent2.iconst_2);
}
}
class Parent2{
public static final String str = "Hello Jvm";
public static final int bi = 127;
public static final int si = 32767;
public static final int icons_1 = 1;
public static final int iconst_2 = 2;
static {
System.out.println("Parent2 init");
}
}
输出结果
Hello Jvm
127
32767
1
2
将Parent
的class
文件从classPath
中删除掉,再运行程序,程序没报错,输出结果一样。
常量的本质含义:常量在编译阶段会存入调用这个常量的常量池中。本质上,调用这个常量并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。如:
Paren2
中定义的常量被存入到了MyTest2
中,之后两个类就没有任何关系了。甚至将Paren2
的.class
文件删除也没关系。
public class MyTest3 {
public static void main(String[] args) {
System.out.println(Parent3.str);
}
}
class Parent3{
public static final String str = UUID.randomUUID().toString().replace("-","");
static {
System.out.println("Paren3 init");
}
}
输出结果:
Paren3 init
2b00eb3dbd934bf7ab610407058d276f
输出结果显示
Parent3
被成功初始化了。而且,删除掉Parent3
的class
文件,也会报java.lang.NoClassDefFoundError
的错误。
在编译期间,对于并不能确定的常量来说,不会被存入到调用类的常量池中。而是在运行期间,主动使用常量的所属类,完成所属类的初始化。
NO.3
public class MyTest4 {
public static void main(String[] args) {
// Parent4 pare