目录
说明:笔记内容来源于《北京圣思园教育 - 张龙老师 - 深入理解JVM》视频课程。如有侵权,请联系删除。
1 常量的回顾分析
在JVM笔记 - 03常量的本质含义与反编译及助记符详解中,介绍了类的静态常量在编译期的处理逻辑。
现在细化下对常量的分析。
1.1 静态常量-在编译阶段可以确定具体值
访问类的静态常量,且在编译期间常量的值能够确定,类不会被初始化
package com.test;
public class MyTest8 {
public static void main(String[] args) {
System.out.println(FinalTest.a);
}
}
class FinalTest {
//有final修饰,为常量,且在编译期间能确定具体的值
public static final int a = 5;
static {
System.out.println("FinalTest static block");
}
}
执行结果为:
如果FinalTest被初始化,则静态代码块会被执行;
静态代码块的内容没有被打印出来,说明静态代码块没有被执行。
FinalTest的属性 a 是编译期常量,5 在编译后,就直接被放在了MyTest8这个类的常量池中。
所以编译后,MyTest8和FinalTest就没有任何关联关系了。
这时,即使把FinalTest.class文件删除,程序还是能正常执行的。
使用命令"javap -c" 对MyTest8.class进行反编译,在反编译结果中可以看到助记符"iconst_5"。
助记符"iconst_5" 的含义把 int 类型的常量5从常量池推送至栈顶,
说明编译器已经把main()方法中的"FinalTest.a" 替换为了 5,并且把 5 放到了MyTest8的常量池中。
1.2 静态变量
访问类的静态变量,类会被初始化
package com.test;
public class MyTest8 {
public static void main(String[] args) {
System.out.println(FinalTest.a);
}
}
class FinalTest {
//没有final修饰,不是常量
public static int a = 5;
static {
System.out.println("FinalTest static block");
}
}
执行结果为:
FinalTest的属性 a 是变量,不会被放到MyTest8这个类的常量池中,在运行期会寻找FinalTest。
这时,如果把FinalTest.class文件删除,则程序运行时会出现"NoClassDefFoundError"
1.3 静态常量-在编译阶段不能确定具体值
访问类的静态常量,且在编译期间常量的值不能确定,类会被初始化
package com.test;
import java.util.Random;
public class MyTest8 {
public static void main(String[] args) {
System.out.println(FinalTest.a);
}
}
class FinalTest {
//有final修饰,为常量,且在编译期间不能确定具体的值
public static final int a = new Random().nextInt(3);
static {
System.out.println("FinalTest static block");
}
}
执行结果为:
FinalTest的属性 a 是常量,
但在编译期不能确定,就不会被放到MyTest8这个类的常量池中,
程序在运行期会寻找FinalTest。
这时,如果把FinalTest.class文件删除,则程序运行时还是会出现"NoClassDefFoundError"
使用命令"javap -c" 对MyTest8.class进行反编译,在反编译结果中没有看到助记符"iconst_5"。
在助记符"getstatic"的注释中,写着"Field com/test/FinalTest.a:I",
说明main()方法中的"FinalTest.a" 引用的是FinalTest 类的静态变量 a,
静态变量 a 是无法在编译期确定的,只有在运行期才知道 a 的数值到底是什么。
所以main()方法在运行时,会访问FinalTest 类的静态属性 a,
即首次主动使用了 FinalTest 类,
所以FinalTest类会被初始化,FinalTest 的静态代码块会被执行。
2 父子类的初始化顺序
2.1 父子类的初始化顺序
package com.test;
public class MyTest9 {
static {
System.out.println("MyTest9 static block");
}
public static void main(String[] args) {
System.out.println(Child.b);
}
}
class Parent {
static int a = 3;
static {
System.out.println("Parent static block");
}
}
class Child extends Parent {
static int b = 4;
static {
System.out.println("Child static block");
}
}
设置JVM参数"-XX:+TraceClassLoading",打印具体的加载过程。
执行结果如下:
从加载日志中可以看到,类加载的先后顺序是MyTest9、Parent、Child。
a. JVM笔记 - 02类的加载、连接、初始化过程详解中介绍了类的主动使用的7种场景。
因为MyTest9 中包含main()方法,Java虚拟机启动时,MyTest9 会被标明为启动类。
程序执行时访问main()方法,是对MyTest9 的主动使用,会导致MyTest9的初始化 。
MyTest9初始化,打印了"MyTest9 static block"。初始化MyTest9 需要先加载MyTest9
b. 在main()方法中,访问了Child的静态变量 b,是对Child的首次主动使用,会初始化Child
c. 初始化Child,需要先初始化Parent,初始化Parent需要先加载Parent
2.2 父子类的主动使用
package com.test;
public class MyTest9 {
static {
System.out.println("MyTest9 static block");
}
public static void main(String[] args) {
Parent parent;
System.out.println("----------------");
parent = new Parent();
System.out.println("----------------");
System.out.println(parent.a);
System.out.println("----------------");
System.out.println(Child.b);
}
}
class Parent {
static int a = 3;
static {
System.out.println("Parent static block");
}
}
class Child extends Parent {
static int b = 4;
static {
System.out.println("Child static block");
}
}
执行结果如下:
结果分析如下:
a. 因为MyTest9 中包含main()方法,Java虚拟机启动时,MyTest9 会被标明为启动类。
程序执行时访问main()方法,是对MyTest9 的主动使用,会导致MyTest9的初始化 。
MyTest9初始化,打印了"MyTest9 static block"。初始化MyTest9 需要先加载MyTest9
b. main()方法中,声明了Parent 的引用,并不是对Parent 的主动使用,
所以Parent 没有加载、初始化。
Parent parent;
c. 创建Parent 的实例,是对Parent 的首次主动使用,会造成Parent 的初始化,
初始化Parent 需要先加载Parent
parent = new Parent();
d. 访问Parent 的静态变量,是对Parent 的主动使用。
但因为首次主动使用,才会初始化Parent ,所以Parent 没有再次初始化,
所以静态代码块"Parent static block"没有被再次打印出来,
只是打印了静态变量 a 的值 3
System.out.println(parent.a);
e. 访问Child 的静态变量 b,是对Child 的首次主动使用,会初始化Child。
初始化Child,需要先初始化Parent,初始化Parent需要先加载Parent。
因为Parent 已经被加载、初始化了,所以没有重复初始化Parent。
所以静态代码块"Parent static block"没有被再次打印出来,
只是加载Child 并打印了Child 的静态代码块"Child static block",
和Child 的静态变量 b 的值 4
System.out.println(Child.b);
2.3 静态变量和主动使用
package com.test;
public class MyTest9 {
public static void main(String[] args) {
System.out.println(Child.a);
System.out.println("----------------");
Child.doSomething();
}
}
class Parent {
static int a = 3;
static {
System.out.println("Parent static block");
}
static void doSomething() {
System.out.println("doSomething");
}
}
class Child extends Parent {
static {
System.out.println("Child static block");
}
}
执行结果如下:
解析结果如下:
通过子类访问父类的静态变量 或静态方法,
是对父类的主动使用,不是对子类的主动使用。
或者讲,访问静态变量/静态方法时,是对 定义静态变量/静态方法的类的主动使用。
a. 访问Child 的静态变量 a,因为静态变量 a 是在Parent 中定义的,
所以访问静态变量 a 是对Parent 的首次主动使用,会导致Parent 的初始化,
初始化 Parent 需要先加载Parent。
System.out.println(Child.a);
因为没有对Child 主动使用,所以Child 没有被初始化,但被加载了。
所以静态代码块"Child static block"没有被打印出来。
b. 访问Child 的静态方法doSomething(),因为静态方法doSomething() 是在Parent 中定义的,
所以访问静态方法doSomething() 是对Parent 的主动使用,
因为不是对Parent 的首次主动使用,所以不会导致Parent 的再次初始化,
所以静态代码块"Parent static block"没有被再次打印。
Child.doSomething();
因为没有对Child 主动使用,所以Child 没有被初始化,所以静态代码块"Child static block"没有被打印出来。
2.4 类加载器和反射
package com.test;
public class MyTest9 {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = ClassLoader.getSystemClassLoader();
//类加载器
Class<?> clazz = loader.loadClass("com.test.Apple");
System.out.println(clazz);
System.out.println("----------------");
//反射
clazz = Class.forName("com.test.Apple");
System.out.println(clazz);
}
}
class Apple {
static {
System.out.println("Apple static block");
}
}
执行结果如下:
结果分析如下:
a. 调用java.lang.ClassLoader的loadClass()方法加载一个类,
并不是对类的主动使用,不会导致类的初始化。
Class<?> clazz = loader.loadClass("com.test.Apple");
loaderClass()不是对Apple的主动使用,所以Apple没有被初始化,
所以静态代码块"Apple static block"没有被打印。
虽然Apple没有没初始化,但Apple被加载了。
b. 反射是对类的主动使用,会导致类的初始化
clazz = Class.forName("com.test.Apple");
反射是对Apple的首次主动使用,会导致Apple被初始化,
所以静态代码块"Apple static block"被打印出来了。