欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

虚拟机类和接口加载过程

程序员文章站 2022-07-11 20:32:09
...

一、类加载

          1. 类加载的生命周期

         类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证+准备+解析统称为连接。如图所示:

虚拟机类和接口加载过程

       另外  加载,验证,准备,初始化,卸载这5个顺序是确定的,类加载过程必须严格按照这种模式加载,而解析阶段则不然:他在某些情况下可以在初始化阶段之后在开始,这是为了支持Java语言的运行时绑定。       

1.1 加载

        将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。(这里的class文件可以很多种方式获取,ZIP包,网络,运行时获取,Proxy,数据库等)

1.2  验证

         确保加载的类信息符合jvm规范,没有安全方面的问题。

1.3  准备 

    正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。

       这里的赋值指的是赋值null,0,等。比如:

            public static int a = 11;在准备阶段只是给a赋值为0,而赋值11是需要初始化阶段才会发生的。

     但是如果是final修饰的常量则会直接赋值:

           public static final  int b = 12;这里直接赋值b为12。

1.4 解析

       虚拟机常量池内的符号引用替换为直接引用的过程。(比如String s ="aaa",转化为 s的地址指向“aaa”的地址)

1.5 初始化

        初始化阶段是执行类构造器方法(<clinit>)的过程。类构造器方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。这里会初始化准备阶段static修饰的变量。

当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先初始化其父类的初始化;虚拟机会保证一个类的构造器方法在多线程环境中被正确加锁和同步

 

2. 类什么时候发生初始化?

               在类初始化之前,肯定是类进行了加载,验证,准备的。

2.1 主动对类进行引用,才会触发对类的初始化,有且只有如下5种情况才会对类进行初始化

  • 1)遇到new,getstatic、putstatic或者invokestatic这4条字节码指令时,如果类没有进行初始化,那么会触发初始化。

               常见的场景:new关键字实例化对象;读取或者设置一个类的静态字段(final修饰的除外,以及在编译的期间就将结果放入常量池的的静态字段除外);以及调用一个类的静态方法。

  • 2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有惊醒初始化,也会先触发类的初始化
  • 3)当类初始化一个类的时候,如果发现父类还没有进行初始化,则需要先触发父类的初始化
  • 4)当虚拟机启动的时候,用户需要制定一个执行的主类(main方法这个类),虚拟机会先初始化这个类
  • 5)当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,那么需要先触发初始化。

 2.2 被动引用不会触发初始化

   例子1:通过子类引用父类静态字段,不会导致子类初始化

//父类
public class SuperClass{
    static{
        System.out.println("SuperClass init");
    }
    public static int value = 123;
}
//子类
public class SubClass extends SuperClass{
    static{
        System.out.println("SubClass init");
    }
}
public class Test1{
    public static void main(String[] args){
        System.out.println(SubClass.value);
    }
}

这个例子结果只会输出:
SuperClass init

结论:对于静态字段,只有直接定义这个字段的类才会被初始化,因此这里子类引用父类的静态字段,只有初始化父类,不会触发子类。

   例子2:通过数组定义引用类,不会触发此类的初始化

public class Test1{
    public static void main(String[] args){
      SuperClass[] sca = new SuperClass[10];
    }
}

  这个运行后,并没有SuperClass init输出,这里实际上触发了一个名为[Lorg.fenixsoft.classloading.SuperClass类的初始化,这个类由虚拟机自动生成,直接继承于java.lang.Object的子类,创建动作由字节码指令newarray触发。这个类的元素类型表示一维数组。

 

   例子3:常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

public class ConstClass {
    static {
        System.out.println("ConstClass init");
    }
    public static final String VISON = "vison";
}

public class Test3{
    public static void main(String[] args){
        System.out.println(ConstClass.VISON);
    }
}

  这个结果只输出了:

 vison

结论:这里并没有输出ConstClass init说明并没有初始化这个类,这个其实是在编译阶段通过常量传播优化,已经将常量“vison”存储到了Test3这个类的常量池中。所以通过ConstClass.VISON的引用实际上转换为了Test3对自身常量池的引用。

 

二、接口的加载

        接口的加载和类的加载有些不同,接口初始化过程有且仅有的一种:当一个类在初始化时,要求其父类全部都已经初始化过了,但是在一个接口初始化时,并不要求父接口完全完成了初始化,只有在真正使用到父接口的时候才会初始化(如引用接口中定义的常量)。