Java类的生命周期-初始化阶段
前两篇讲述了类生命周期的加载阶段和连接阶段,那么本篇我们来讲最为重要的初始化阶段,借助字节码文件与大厂面试题更好的理解类的初始化
头篇提到,类的生命周期可疑将他分为五个阶段,本篇要讲述的就是第三阶段-初始化阶段:
初始化作用
在初始化阶段会执行静态代码块并且为静态变量赋值,其最核心的部分就是执行字节码文件中的clinit部分
我们以一个类的初始化为例:
public class InitTest {
public static int value = 0;
static {
value = 2;
}
public static void main(String[] args) {
}
}
我们在类中声明了一个静态字段,赋值为0,在下面的静态代码块中又将值修改为2
编译过后我们打开他的字节码文件
如果你不会使用打开字节码文件的工具,本篇末尾会附有教程
字节码文件中会出现三个方法
- init是构造方法,这里没有写构造方法但是他会默认生成一个无参构造
- main就是我们的主方法
- clinit就是在初始化阶段执行的初始化方法
打开clinit方法就可以看到他的字节码指令:
0 iconst_0
1 putstatic #2 <InitTest.value : I>
4 iconst_2
5 putstatic #2 <InitTest.value : I>
8 return
iconst指令是将常量0放入操作数栈中,putstatic指令又将操作数栈中的数字放入上一阶段为静态字段开辟好的内存中,静态字段的名字在常量池中寻找,此处为#2,即对应常量池中的2号索引,下面两句也是同样操作,最后静态变量value的值就成了2。
若是此时将代码中的两次赋值顺序颠倒:
public class InitTest {
static {
value = 2;
}
public static int value = 0;
public static void main(String[] args) {
}
}
那么此时clinit方法的字节码指令就变成了:
0 iconst_2
1 putstatic #2 <InitTest.value : I>
4 iconst_0
5 putstatic #2 <InitTest.value : I>
8 return
就会发现字节码文件中的执行顺序也发生了颠倒,此时说明字节码指令的执行顺序与代码编写顺序是一致的
初始化条件
初始化阶段并不是一定会执行,他只在四种情况下进行初始化:
- 访问一个类的静态变量或静态方法,但是final修饰的静态字段不会触发初始化
- 调用Class.forName(String className)时
- new一个该类的对象时
- 执行该类的Main方法时
初始化方法clinit在下面三种情况下不会出现
- 无静态代码块且无静态变量赋值语句
- 有静态变量的声明,但是没有赋值语句
- 静态变量由final进行修饰
涉及到子类继承父类时,初始化执行有两种情况
- 直接访问父类的静态变量,不会触发子类的初始化
- 子类初始化clinit调用之前,会先调用父类的初始化
大厂面试题
题目如下,写出控制台的输出内容:
public class Test1 {
public static void main(String[] args) {
System.out.println("A");
new Test1();
new Test1();
}
public Test1(){
System.out.println("B");
}
{
System.out.println("C");
}
static {
System.out.println("D");
}
}
执行main方法时,会对类进行初始化,所以我们可以看一下初始化方法clinit中的字节码指令:
0 getstatic #1 <java/lang/System.out : Ljava/io/PrintStream;>
3 ldc #9 <D>
5 invokevirtual #3 <java/io/PrintStream.println : (Ljava/lang/String;)V>
8 return
其内容就是在执行静态代码块中的内容,ldc指令将常量池中的字符串D加载到操作数栈中,而invokevirtual指令是调用println方法将其打印出来。
初始化完毕后,他会执行main方法中的内容,首先打印A,随后new了两个Test1的对象,前面说过在new对象时,类也会进行初始化,但是类在执行时至多只会进行一次初始化,所以这里不需要再次执行初始化方法,所以此时会调用两次构造方法,我们查看init方法中的字节码指令:
0 aload_0
1 invokespecial #6 <java/lang/Object.<init> : ()V>
4 getstatic #1 <java/lang/System.out : Ljava/io/PrintStream;>
7 ldc #7 <C>
9 invokevirtual #3 <java/io/PrintStream.println : (Ljava/lang/String;)V>
12 getstatic #1 <java/lang/System.out : Ljava/io/PrintStream;>
15 ldc #8 <B>
17 invokevirtual #3 <java/io/PrintStream.println : (Ljava/lang/String;)V>
20 return
第3-5行与6-8行为主要内容,其字节码指令与clinit中相似,此时发现字节码指令会先打印C再打印B,所以初始化代码块会在构造方法之前执行,那最后的输出顺序为:DACBCB
第二题
public class Test2 {
public static void main(String[] args) {
System.out.println(B.a);
}
}
class A{
static int a = 0;
static {
a = 1;
}
}
class B extends A{
static {
a = 2;
}
}
大家可以在评论区讨论一下输出的a的值是多少
附:字节码工具
1.在IDEA中下载插件:jclasslib
下载并应用
2.点击视图->Show ByteCode By Jclasslib
注意此时光标要停留在你想查看字节码文件的源代码上
3.查看字节码文件
此时就可以查看字节码文件的常量池、方法、字段等信息
看到这里对你有帮助请点一个关注,如果不太明白可以查看一下之前的两篇,学习一下前两个阶段