虚拟机加载机制——类加载时机


在阅读本文之前需要注意两点。第一,本文指的“类”包括类和接口,对于类和接口的不同之处,会特别指明。第二,本文指定“Class文件”并非存在磁盘上,这里说的“Class文件”是指一串二进制的字节流

一、类加载(初始化)的时机

类从被虚拟机加载到内存中到从内存中卸载,由以下几个过程(生命周期),如下图所示:
在这里插入图片描述

其实第一个类加载的时机,java虚拟机规范并没有进行强行约束,而是有虚拟机的具体实现来自动把握的。然而类的初始化,虚拟机规范就做出了规定了。

类的初始化有以下5个时机:

  1. 使用new实例化对象时、访问修改类的静态字段(用final修饰的静态字段除外,因为它在编译时已经放入了常量池了,不存在类的符号引用了)。在字节码层面就是上就是遇到new、puststatic、getstatic、invokesatic这几个字节码指令时,如果类没有被初始化,就进行类的初始化。
  2. 如果对类进行反射调用时,如果没有进行类的初始化,则需要先触发其初始化。
  3. 当初始化一个类时,发现其父类没有被初始化,就先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法那个类),虚拟机先初始化这个类。
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例的最终的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有被进行过初始化,则需要先对其进行初始化。

二、主动引用、被动引用及被动引用的几个例子

上面的5个时机称为对类的主动引用(需要对类进行初始化的)。除此之外,所有引用类的方式都不会触发类的初始化,被称为类的被动引用。下面是类的三个类的被动引用的例子:

2.1 通过子类引用父类的静态字段,不会导致子类的初始化

代码:


public class Main {
    public static void main(String[] args) {
        //调用父类的静态成员,并不会导致子类的初始化,只会进行父类的初始化
        int b = subClass.a;
    }
}

class superClass {
    static {
        System.out.println("superClass进行初始化");
    }

    public static int a = 3;
}

class subClass extends superClass {
    static {
        System.out.println("subClass进行初始化");
    }
}

运行结果:
在这里插入图片描述

上图显示了,没有对子类进行初始化。

2.2 通过数组定义引入类,不会触发类的初始化

代码:


public class Main {
    public static void main(String[] args) {
        //通过数组定义引入类,不会触发类的初始化
       subClass[] subArray=new subClass[10];
    }
}

class superClass {
    static {
        System.out.println("superClass进行初始化");
    }

    public static int a = 3;
}

class subClass extends superClass {
    static {
        System.out.println("subClass进行初始化");
    }
}

运行结果:
在这里插入图片描述
运行结果就是没输出。

2.3 访问用final修饰的静态字段,不会触发类的初始化

代码:


public class Main {
    public static void main(String[] args) {
        //访问用final修饰的静态字段,不会触发类的初始化
        int b = superClass.a;
    }
}

class superClass {
    static {
        System.out.println("superClass进行初始化");
    }

    public final static int a = 3;
}

class subClass extends superClass {
    static {
        System.out.println("subClass进行初始化");
    }
}

运行结果:
在这里插入图片描述
运行,没有输出。常量在编译阶段会存入调用类(Main)的常量池中,本质上并没有直接引用到定义常量的类(superClass),因此就不会触发定义常量的类的初始化。


三、类加载的时机中类和接口的区别

类加载的时机中,类和接口的区别在于上面类初始化的时机中的第三点。当一个类初始化时,需要其全部父类都被进行初始。然而接口并不需要父接口全部完成初始化,只有在真正是要到父接口时(如引用接口中定义的常量)才会初始化父接口。

下面代码意义不大。


public class Main {
    public static void main(String[] args) {
        //初始化子類時,需要先初始化其父類接口
        int b = subClass.a;
        new subInterface(){
            @Override
            public void subMethod() {
                System.out.println(subInterface.b);
            }

            @Override
            public void superMethod() {
                System.out.println(subInterface.a);
            }
        }.subMethod();
    }
}


interface superInterface {

    public final static int a = 3;
    void superMethod();

}

interface subInterface extends superInterface {
    public final static int b = 3;
    void subMethod();
}

class superClass {
    static {
        System.out.println("初始化父類");
    }
}
class subClass extends superClass{
    static {
        System.out.println("初始化子類");
    }
    public static int a=3;
}

运行结果(怎样看接口是否进行初始化,不知道):
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/wobushixiaobailian/article/details/83504299