Java 何时会触发一个类的初始化?
- 使用
new
关键字创建对象 - 访问类的静态成员变量 或 对类的静态成员变量进行赋值
- 调用类的静态方法
- 反射调用类时,如
Class.forName()
- 初始化子类时,会先初始化其父类(如果父类还没有进行过初始化的话)
- 遇到启动类时,如果一个类被标记为启动类(即包含
main
方法),虚拟机会先初始化这个主类。 - 实现带有默认方法的接口的类被初始化时(拥有被
default
关键字修饰的接口方法的类) - 使用 JDK7 新加入的动态语言支持时
MethodHandle
虚拟机在何时加载类
关于在什么情况下需要开始类加载的第一个阶段,《Java虚拟机规范》中并没有进行强制约束,留给虚拟机自由发挥。但对于初始化阶段,虚拟机规范则严格规定:当且仅当出现以下六种情况时,必须立即对类进行初始化,而加载、验证、准备自然需要在此之前进行。虚拟机规范中对这六种场景中的行为称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。
1. 遇到指定指令时
在程序执行过程中,遇到 new、getstatic、putstatic、invokestatic 这4条字节码执行时,如果类型没有初始化,则需要先触发其初始化阶段。
new
这没什么好说的,使用new
关键字创建对象,肯定会触发该类的初始化。
getstatic 与 putstatic
当访问某个类或接口的静态变量,或对该静态变量进行赋值时,会触发类的初始化。首先来看第一个例子:
// 示例1
public class Demo {
public static void main(String[] args) {
System.out.println(Bird.a);
}
}
class Bird {
static int a = 2;
// 在类初始化过程中不仅会执行构造方法,还会执行类的静态代码块
// 如果静态代码块里的语句被执行,说明类已开始初始化
static {
System.out.println("bird init");
}
}
执行后会输出:
bird init
2
同样地,如果直接给Bird.a
进行赋值,也会触发Bird
类的初始化:
public class Demo {
public static void main(String[] args) {
Bird.a = 2;
}
}
class Bird {
static int a;
static {
System.out.println("bird init");
}
}
执行后会输出:
bird init
接着再看下面的例子:
public class Demo {
public static void main(String[] args) {
Bird.a = 2;
}
}
class Bird {
// 与前面的例子不同的是,这里使用 final 修饰
static final int a = 2;
static {
System.out.println("bird init");
}
}
执行后不会有输出。
本例中,a
不再是一个静态变量,而变成了一个常量,运行代码后发现,并没有触发Bird
类的初始化流程。常量在编译阶段会存入到调用这个常量的方法所在类的常量池中。本质上,调用类并没有直接引用定义常量的类,因此并不会触发定义常量的类的初始化。即这里已经将常量a=2
存入到Demo
类的常量池中,这之后,Demo
类与Bird
类已经没有任何关系,甚至可以直接把Bird
类生成的class
文件删除,Demo
仍然可以正常运行。使用javap
命令反编译一下字节码:
// 前面已省略无关部分
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iconst_2
4: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
7: return
}
从反编译后的代码中可以看到:Bird.a
已经变成了助记符iconst_2
(将int
类型2
推送至栈顶),和Bird
类已经没有任何联系,这也从侧面证明,只有访问类的静态变量才会触发该类的初始化流程,而不是其他类型的变量。
关于Java助记符,如果将上面一个示例中的常量修改为不同的值,会生成不同的助记符,比如:
// bipush 20
static int a = 20;
// 3: sipush 130
static int a = 130
// 3: ldc #4 // int 327670
static int a = 327670;
其中:
iconst_n
:将int
类型数字n
推送至栈顶,n
取值0~5
lconst_n
:将long
类型数字n
推送至栈顶,n
取值0,1
,类似的还有fconst_n
、dconst_n
bipush
:将单字节的常量值(-128~127
) 推送至栈顶
sipush
:将一个短整类型常量值(-32768~32767
) 推送至栈顶
ldc
:将int
、float
或String
类型常量值从常量池中推送至栈顶
再看下一个实例:
public class Demo {
public static void main(String[] args) {
System.out.println(Bird.a);
}
}
class Bird {
static final String a = UUID.randomUUID().toString();
static {
System.out.println("bird init");
}
}
执行后会输出:
bird init
d01308ed-8b35-484c-b440-04ce3ecb7c0e
在本例中,常量a
的值在编译时不能确定,需要进行方法调用,这种情况下,编译后会产生getstatic
指令,同样会触发类的初始化,所以才会输出bird init
。看下反编译字节码后的代码:
// 已省略部分无关代码
public