JVM笔记 - 09类加载器与初始化深度剖析


说明:笔记内容来源于《北京圣思园教育 - 张龙老师 - 深入理解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"被打印出来了。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值