前言 最近在看一本书,发现代码里用到了
Thread.currentThread().getContextClassLoader()
,为什么类加载器还与线程有关系呢,为什么不直接使用ClassLoader.getSystemClassLoader()
呢?带着这些疑问又把JVM类加载机制从头到尾学习了一遍。
篇一 类加载时机
我们编写的代码存储在java文件中,java源代码通过编译生成Java虚拟机可识别的字节码,存储在Class文件中。运行java程序时需要将Class文件中的信息加载到Java虚拟机中,而这个过程就是类加载的过程。
如上图所示,假设写一个类A存储为A.java,通过javac A.java
编译生成A.class,A.class中存储了各种描述A类的信息。然后运行程序,执行java A.class
,这时java虚拟机会先将A.class中信息转换成Java虚拟机所需的存储格式,然后存放在方法区中。之后,Java虚拟机会再创建一个java.lang.Class
对象实例,这个实例将作为访问方法区中A类信息的入口。使用A中方法时,要先创建一个实例new A()
,Java虚拟机基于类的描述信息在Java堆中创建一个A的实例。
那何时会触发类的加载呢?Java虚拟机规范中并未明确指出,但对类的初始化时机做了明确说明。这两者有什么关系呢,我们先了解一下类的加载流程:
根据这个流程,初始化触发时类加载的第一个阶段---加载阶段肯定已经完成了,那我们可以这样推论,类初始化的触发时机定会触发整个类加载过程。
上面示例中提到的新建一个对象实例会先加载对象,java虚拟机规范中提到在以下五种情况将触发类的初始化[1]:
1) 遇到new,getstatic,putstatic,invokestatic这四条字节码指令时;
为了验证我们先设置个基础类B,提供了一个静态字段,一个静态块。静态方法和静态块会在编译期汇集到<clinit>
类初始化方法中,固可以用这个方法的运行结果引证类的初始化。
- public class B {
- public static int f;
- static {
- System.out.println("init B");
- }
- public static void m(){
- System.out.println("invoke m");
- }
- }
-
new:新建对象;
- public class C{
- public static void main(String args[]) {
- new B();
- }
- }
运行结果:
- init B
说明类已经初始化了
-
pustatic:设置类的静态字段;
- public class C{
- public static void main(String args[]) {
- B.f=5;
- }
- }
运行结果:
- init B
-
getstatic:获取类的静态字段;
- public class C{
- public static void main(String args[]) {
- System.out.println(B.f);
- }
- }
运行结果
- init B
- 0
-
invokestatic:调用类的静态方法;
- public class C{
- public static void main(String args[]) {
- B.m();
- }
- }
运行结果:
- init B
- invoke m
2) 使用java.lang.reflect
包或Class
的方法对类进行反射调用时;
- public class C{
- public static void main(String args[]) {
- try {
- Class.forName("B");
- } catch (ClassNotFoundException e) {
- }
- }
- }
运行结果:
- init B
3) 当初始化一个类,其父类还没有初始化时;
这里我们新建一个类A继承B,通过初始化A看看B有没有初始化。
- public class A extends B {
- static {
- System.out.println("init A");
- }
- }
-
- public class C{
- public static void main(String args[]) {
- new A();
- }
- }
运行结果:
- init B
- init A
发现B也初始化了,并且是B先初始化完A才初始化,也就是初始化一个类时会先看其父类是否已经初始化,依次类推一直到java.lang.Object
。其实除了类需要初始化,接口也需要初始化,用于初始化接口变量的赋值。与类的初始化不同,接口初始化时并不会递归初始化所有父接口,而是用到哪个接口就初始化哪个接口,如调用接口中的常量。由于接口中不允许有静态块,那<clinit>
就只用于初始化其常量。
新建两个接口E、G,类F实现这两个接口:
- public interface E {
- B b = new B();//依据接口的特性 默认变量static final修饰
- }
- public interface G {
- D d = new D();
- }
- public class D {
- static {
- System.out.println("init D");
- }
- }
- public class B {
- static {
- System.out.println("init B");
- }
- }
- public class F implements E,G {
- //只为了测试这个方法无需任何实现
- }
- public class C {
- public static void main(String args[]) {
- System.out.println(F.b);
- }
- }
这里运行结果只有init B
而没有init D
说明没有触发接口G的初始化。
4) 当Java虚拟机启动时,用户指定的主类(包含main方法的类)要先进行初始化;
程序运行一定会有一个入口,也就是Main类,java虚拟机启动时会现将其初始化。下面我们在B类里加一个空的main方法,运行看一下效果:
- public class B {
- public static int f;
- static {
- System.out.println("init B");
- }
- public static void m(){
- System.out.println("invoke m");
- }
- public static void main(String args[]){
- }
- }
编译后运行java B
控制台输出init B
,说明B也初始化完成了。
5) 在初次调用java.lang.invoke.MethodHandle
实例时,通过java虚拟机解析出类型是REF_getStatic,REF_puStatic,REF_invokeStatic的方法句柄时;
注意,上面所说的场景都有一个前提就是对应的类没有初始化过,如果这个类已经初始化了,直接使用就可以了。
以上这些场景都属于对一个类的主动引用,除了这些场景外其他引用类的方式都不会触发初始化。我们从上面的场景用找几个特例来看一下是否能使其初始化:
1)针对第一条,通过子类调用父类的静态变量或静态方法
这里严重上面第三条的类A,类B
- public class C{
- public static void main(String args[]) {
- A.f=5;
- }
- }
通过类A赋值类B的静态字段f,运行结果只有init B
,而没有初始化类A,因为这里用到的是类变量,只是借用了A对B的继承关系,无需对A进行初始化。
2)调用类中的常量
常量在编译阶段会直接在调用类中将常量值存入其常量池中,与被调用类其实也没有关系了,固对常量的调用并不会引起被调用类的初始化,如下:
- public class D {
- public static final String f = "f";
- static {
- System.out.println("init D");
- }
- }
-
- public class C{
- public static void main(String args[]){
- System.out.println(D.f);
- }
- }
运行结果:
- f
证实了上面的说法
3) 通过数组引用类
对于数组java虚拟机会特殊处理,在执行时Java虚拟机会动态生成一个数组对象,这时初始化的只是这个数组对象。当使用数组的元素时才会真正触发元素类型的初始化。
直接在main方法中新建数组:
- public class H {
- public static void main(String args[]){
- B[] bs = new B[1];
- }
- }
运行结果什么也没有输出,说明B没有初始化。通过输出bs
发现这是一个[LB
对象,是由newarray
指令动态生成的。
- public class H {
- public static void main(String args[]){
- B[] bs = new B[1];
- System.out.println("-------");
- System.out.println(bs[0].f);
- }
- }
运行结果:
- -------
- init B
- 0
这说明直到bs[0].f
时才真正触发B的初始化。
写了这么多才发现仅仅谈到类加载的时机,离着解决篇头的问题还差一大截。没办法,要想彻底了解清楚类加载必须慢下心一步一步来。
若发现文章中任何问题,欢迎指正,互相学习。
-
这里说的初始化指的是类初始化,还有实例初始化是在创建实例时进行的。 ↩