JVM 类的初始化
按理解描述,欢迎指正。
一、类的生命周期
编译阶段:java源文件经过编译(javac)编译为符合jvm规范的二进制字节码。
加载阶段:classLoader将.class文件加载到内存。有几种方式:从本地记载、从网络加载、从zip、jar包加载、从数据库加载、动态编译(动态代理、jsp)
连接阶段:分三个阶段,分别为:
校验:类文件的结构检查、语义检查、字节码验证、二进制兼容性验证
准备:为静态变量分配内存,并赋初始化值
解析:将符号引用转换成直接引用
初始化阶段:假如类还没有加载和连接,那先加载和连接。如果类存在直接父类,直接父类没有初始化,先初始化直接父类。如果类存在初始化语句,按顺序执行初始化语句。但是接口例外,类初始化时不会初始化接口,接口初始化时不会初始化父接口。使用classLoader的loadClass方法不初始化类,但是forName会初始化类。注意,静态代码只会执行一次,因为只会初始化一次。
使用阶段:待补充
卸载阶段:待补充
二、什么时候初始化。
规则:类或者接口,只在被首次主动使用时,才被初始化。
主动使用有几种情况:创建类的实例、访问或者修改类或者接口的静态变量、访问类的静态方法、Class.forName()、虚拟机启动时的启动类、java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的没有初始化的类。
下面举几个特殊的例子,加深一下印象。
1、类只有被首次主动使用才初始化,后续的使用不会再一次初始化
示例1:
这段代码的输出为: TestParent init ,不会输出两次
2、final修饰的变量,如果在编译期间能确定,则会将常量值存放到使用类的常量池内
示例2:
这个代码非常简单。运行结果只输出了5。原因是:TestParent类没有初始化,所以静态代码块不会执行,a是确定是常量,编译阶段已经在Dabai这个类的常量池内了。甚至于jvm都没有把TestParent加载到jvm内。因为编译结束后,TestParent.a已经变成了5。所以Dabai并没有任何TestParent的相关信息,也没有主动使用TestParent。
将上面代码内的final去掉,那么就会打印TestParent init 和 5。原因是类被首次主动使用、需要初始化。
如果将a的值变为动态的,那么也会打印TestParent init 和 5。原因为:编译期间确定不了常量的值,推迟到初始化阶段。如下:
3、直接使用才会初始化
实例3:
这段代码最后会输出:TestParent init 和 1。原因是虽然是用子类的引用访问父类的变量,但是对字类没有直接使用。所以对TestChild没有初始化。但是会对TestChild进行加载。原因是虽然没被主动使用,JVM预测TestChild可能会被使用,那么就会加载这个类。如果TestChild.class文件缺失、或者内部存在错误,那么会在首次主动使用这个类时报错。如果这个类一直没被主动使用,类加载器一直不会报错。
4、数组不会对其内元素的类型进行主动使用
示例4:
这段代码会输出:class [Lcom.inspur.eap.datasource.rest.TestParent;
原因是:数组没有对TestParent主动使用,所以TestParent没有初始化,而是动态生成了一个 [Lcom.inspur.eap.datasource.rest.TestParent类。父类为Object。
5、连接阶段中准备阶段给静态变量分配空间并赋予初始值。初始化阶段,按顺序执行初始化代码。
实例5:
这段代码的输出为:1 0。 原因是,Dabai对TestParent进行了首次主动使用,所以TestParent被加载、连接、初始化。连接中准备阶段,num1被赋值0,num2被赋值0。然后初始化阶段,按顺序,第一行执行完之后num1仍然为0,第二行执行完之后num1=1,num2=1,第七行执行完,num2=0。所以输出为0。另外num2在构造方法后声明不会导致初始化报错,因为连接阶段num2已经存在。
6、类和接口初始化的差异。因为接口内的变量默认时 public static final 的,所以必须动态赋值才能体现初始化。
示例6-1:
输出结果为:TestParent init TestChild init 1。原因是,主动使用了TestChild,所以需要初始化TestChild,但他的直接父类TestParent还没初始化,所以先初始化TestParent。
示例6-2:
输出结果为: TestChild init 1。原因是:类初始化,但不会初始化接口。
示例6-3:
输出结果:1。原因:接口内的变量默认是 public static final 的,那么a就是个常量,在编译期就被放在常量池里了,所以都没初始化,且都没加载。
示例6-4:
输出结果: TestChild init 4(10以内的数字)。原因,首先父接口没有初始化,其次子接口初始化了。
7、loadClass 和 forName
示例7-1:
这段代码没有任何输出。原因是loadClass不算主动使用TestParent类。
示例7-2:
输出结果:TestParent init 。原因:forName是对类的主动使用(具体细节以后补充)
先分享这么多,后面再分享类加载器的一些内容。