Java-底层原理-初始化之clinit和init
系列文章目录
Java-底层原理-编译原理
Java-底层原理-javac源码笔记
Java-底层原理-类加载机制
Java-底层原理-clinit和init
摘要
在准备阶段,类变量(静态非final字段)被设初值,如int类型被设为0,常量被设值。
而初始化阶段是类加载的最后一步,此时才会真正开始执行java应用程序代码(字节码)。此阶段中,会真正为类变量赋初值,以及做其他资源的初始化工作。这个阶段就会执行类构造器即
<clinit>
方法。
0x01
<clinit>
<clinit>
1.1 简介
<clinit>
方法是由编译器自动收集的,包括所有类变量(静态非final)赋值和静态语句块(static{})。该收集行为顺序由语句在源码文件中出现的顺序决定,具体来说,静态语句块只能访问到定义在语句块之前的变量:
public class ClinitTest1
{
static {
a = 1;
// 下面这个会报错 illegal forward reference
// int c = b;
}
static int a;
static int b;
public static void main(String[] args)
{
System.out.println("a=" + a);
}
}
1.2 执行时机
类加载的初始化阶段运行
<clinit>
。且在执行子类前,会先执行父类的
<clinit>
方法。也就是说,JVM中执行的第一个clinit方法来自
Object
类。
注意,接口中也有
<clinit>
方法,但实现类初始化或子类接口执行
<clinit>
方法时,不会执行父接口的
<clinit>
。而是要等到父接口定义变量使用时才会调用clinit。
在以下情况发生时,还没有发生类过初始化,则会先触发其初始化。
-
对一个对象使用
new
操作符; - Class.forName(a.b.c.Test)
- XXX.class.newInstance()
- 读取或写入一个类的静态字段(除了final static或已在编译器把变量放入常量池的静态字段);
- 调用一个类的静态方法时
-
使用
java.lang.reflect
包的方法对类进行反射调用的时候 - 当初始化一个类的时候,如果父类还没有初始化过,则需要先触发父类初始化(接口除外)
- 执行main方法所在的类
-
使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为
REF_getStatic
,
REF_putStatic
,
REF_invokeStatic
的方法句柄,并且这个句柄所在类还未初始化过
以下不会触发初始化:
- Class initable4 = loader.loadClass(“demos.classInitialization.InitClass.Initable4”);
- System.out.println(Initable4.staticFinal);
1.3 多线程与clinit
在多线程场景下,由JVM保证只有一个线程能执行该类的
<clinit>
方法。在执行过程中,其他线程全部阻塞等待。等唯一的一次clinit方法完成后,所有线程开始执行自己的代码。
也就是说,一个类只会在一个JVM进程运行期间执行一次
<clinit>
方法。
1.4 无clinit的情况
以下类没有
<clinit>
方法:
-
没有声明任何类变量、没有
static
块语句 - 声明了类变量,但没有对应的变量初始化语句
-
只有final类变量,且赋值语句是常量(如
final static int danlu = 1;
)。这样的赋值会在准备阶段就执行
0x02
<init>
<init>
2.1 简介
就是对象实例初始化的方法。
2.2 执行时机
对象实例化时。
实例化有4种方法:
- new
- XXX.class.newInstance()或XXX.class.getDeclaredConstructor().newInstance()
- 调用对象的clone方法
- 通过 java.io.ObjectInputStream 类的 getObject() 方法反序列化
2.3 init 方法执行顺序
- 父类变量初始化
- 父类语句块
- 父类构造函数
- 子类变量初始化
- 子类语句块
- 子类构造函数
2.4 init加clinit的执行顺序
- 父类静态变量初始化
- 父类静态语句块
- 子类静态变量初始化
- 子类静态语句块
- 父类变量初始化
- 父类语句块
- 父类构造函数
- 子类变量初始化
- 子类语句块
-
子类构造函数
以上1-4位clinit内容。