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 parent4 = new Parent4();
Parent4[] parent4s = new Parent4[1];
int[] ints = new int[1];
System.out.println(parent4s.getClass());
System.out.println(parent4s.getClass().getSuperclass());
System.out.println("==============");
System.out.println(ints.getClass());
System.out.println(ints.getClass().getSuperclass());
}
}
class Parent4{
static {
System.out.println("Paren4 init");
}
}
输出结果
class [LclassLoader.Parent4;
class java.lang.Object
==============
class [I
class java.lang.Object
输出结果显示并没有触发
Parent
的初始化过程,但是却触发了class [LclassLoader.Parent4;
的初始化阶段,打印出的这个名称,它直接继承class java.lang.Object
,代表了数组的component
,即数组的组成元素。
将class
文件反编译后,可以看出它的创建动作由助记符newarray
触发。
anewarray
:表示创建一个引用类型的数组(类、接口、数组),并将其引用值压入栈顶。
newarray
:表示创建一个基本类型的数组(int、char),并将其引用值压入栈顶。
NO.4
public class MyTest6 {
public static void main(String[] args) {
Single instance = Single.getInstance();
System.out.println("count1:" + Single.count1);
System.out.println("count2:" + Single.count2);
}
}
class Single{
public static int count1;
public static int count2 = 0;
private static Single single = new Single();
private Single(){
count1++;
count2++;
System.out.println("构造方法count1:" + count1);
System.out.println("构造方法count2:" + count2);
}
public static Single getInstance(){
return single;
}
}
输出结果:
构造方法count1:1
构造方法count2:1
count1:1
count2:1
在
MyTest6
中调用Single
的静态方法,触发Single
的初始化阶段。----连接阶段,将静态变量全置为默认值:
count1 = 0
count2 = 0
single = null
----初始化阶段,顺序执行静态语句:
执行到此句时
private static Single single = new Single();
,执行Single
的构造方法。
count1 = 1
count2 = 1
并将其打印,最后再在
MyTest6
的main
方法中调用时,直接从Single
的常量池中取出。
修改下Single
的代码
class Single{
public static int count1;
private static Single single = new Single();
private Single(){
count1++;
count2++;
System.out.println("构造方法count1:" + count1);
System.out.println("构造方法count2:" + count2);
}
//调下顺序
public static int count2 = 0;
public static Single getInstance(){
return single;
}
}
输出结果:
构造方法count1:1
构造方法count2:1
count1:1
count2:0
再修改下Single
的代码
class Single{
//初值赋为1
public static int count1 = 1;
private static Single single = new Single();
private Single(){
count1++;
count2++;
System.out.println("构造方法count1:" + count1);
System.out.println("构造方法count2:" + count2);
}
public static int count2 = 0;
public static Single getInstance(){
return single;
}
}
输出结果:
构造方法count1:2
构造方法count2:1
count1:2
count2:0
经过上面的程序可以看出,静态变量的声明语句,以及静态代码块都被看做类的初始化语句,Java
虚拟机会按照初始化语句在类文件中的先后顺序来依次执行它们。
NO.5
public class MyTest7 {
static {
System.out.println("MyTest7 invoked");
}
public static void main(String[] args) {
Parent7 parent7;
System.out.println("---------------");
// parent7 = new Parent7();
Son7 son7 = new Son7();
System.out.println("---------------");
System.out.println(Son7.a);
}
}
class Parent7{
static int a = 5;
static {
System.out.println("Parent7 invoked");
}
}
class Son7 extends Parent7{
static int b = 6;
static {
System.out.println("Son7 invoked");
}
}
输出结果:
MyTest7 invoked
---------------
Parent7 invoked
Son7 invoked
---------------
5
输出结果显示:首先使用
MyTest7
的启动类,导致了MyTest7
的初始化,执行了静态代码块;然后声明了一个Parent7
的变量,并不会导致Parent7
的初始化;最后创建了一个Son7
的实例,触发Son7
的初始化,触发Parent7
的初始化。
将Son7 son7 = new Son7();
替换为parent7 = new Parent7();
输出结果:
MyTest7 invoked
---------------
Parent7 invoked
---------------
5
输出结果显示:只有
Parent7
初始化,而Son7
并没有初始化。
上述代码也印证了,在创建实例时以及启动类时,会导致类的初始化;当一个类初始化时,会先初始化它的父类。
NO.6
public class MyTest8 {
public static void main(String[] args) {
//System.out.println(Son8.a);
Son8.doSomething();
}
}
class Parent8{
static int a = 1;
static {
System.out.println("Parent8 invoked");
}
static void doSomething(){
System.out.println("Parent8'doSomething");
}
}
class Son8 extends Parent8{
static {
System.out.println("Son8 invoked");
}
}
输出结果:
Parent8 invoked
Parent8'doSomething
输出结果显示:
Parent8
被初始化了。
调用类的静态方法时,会导致类的初始化。
结论
类的初始化是类加载过程的最后阶段,在前面的类加载过程中,都是有虚拟机来进行主导和控制(除了用户可以自定义类加载外,请看我后续博客),到了初始化阶段,才真正开始执行Java
程序中的字节码。
在连接中的准备阶段,静态变量被赋予了默认值,到了初始化阶段,这些变量才被赋予真正的值。在对类进行初始化时,Java
虚拟机会按照初始化语句在类文件中的先后顺序来一次执行它们。
一个类只有在被首次主动使用才会触发初始化阶段,也只有上文提到的七种方式才算主动使用,其他都是被动使用。