类加载机制

149 阅读7分钟

类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行验证,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

     在Java中,类型的加载,连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。

类加载的时机

   类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:

   加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using),卸载(Unloading)。



    加载,验证,准备,初始化和卸载这5个阶段的顺序是确定的,类的加载过程须按照这种顺序开始,而解析阶段就不一定了:为了支持Java的运行时绑定,它在某些情况下可以再出初始化阶段之后再开始。

    注意:上面所指的开始,不是指某个阶段完成后再开始另一个阶段。这些阶段通常是互相交叉地混合式进行,会在一个阶段执行的过程中调用或者激活另外一个阶段。

   Java虚拟机规范中并没有进行强制约束什么情况下需要开始第一个阶段:加载。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行初始化。

主动引用

  1. 遇到new,getstatic,putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。 代码场景:new 使用new关键字实例化对象。 getstatic,putstatic 读取或设置一个类的静态字段的时候 invokestatic 调用一个类用静态修饰的方法

  2. 使用java.lang.reflect包的方法对类进行反射调用的时候。

  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类初始化。

  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个类。

  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化。

     除了以上的5种场景中的行为称为一个类进行主动引用,除此之外,所有引用列的方法都不会触发初始化,称为被动引用。
    

被动引用

  1. 通过子类引用父类的静态字段,不会导致子类初始化。 假如通过子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
  2. 通过数组定义来引用类,不会触发此类的初始化。 数组类是由Java虚拟机直接创建的。但数组类与类加载器有密切的关系,因为数组类的元素类型是要靠类加载器去创建的。
  3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。 即使引用了类中的常量,但其实在编译的过程中已经将改常量的值存储到该类的常量池中了。因此所有对该常量的引用都会变成对自身常量池的引用。

接口与类的初始化过程大致相同,只是在静态代码块这一块不同。因为接口不能使用静态代码块。而且在主动引用的第三种情况也不同。接口是在只有真正使用到父接口的时候才会初始化。(例如接口中的定义的常量)

类加载的过程

加载

在此阶段开发人员可以使用自定义的类加载器来完成。

加载阶段,虚拟机需要完成以下3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。

  2. 将这个字节流所代表的惊天存储结构转化为方法去的运行时数据结构。

  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

    但对于数据类来说,本身布是通过类加载器创建,它是由Java虚拟机直接创建的。一个数组类的创建过程须遵循以下规则:

  • 如果数组是引用类型,就递归采用原本的加载过程去加载这个引用类型。数组将在加载该类型的类加载器的类名称空间上被标识。

  • 如果数组是基本类型,Java虚拟机将会把数组标记为与引导类加载器关联。

  • 数组类的可见性与它的类型可见性一致,如果不是引用类型,那数组类的可见性默认为public。

    另外加载阶段完成后,会在内存中实例化一个java.lang.Class类的对象,它虽然是对象,但这个对象时存放在方法区中作为程序访问方法区仲的类型数据的外部接口。

验证

验证阶段是非常重要的,因为决定了Java虚拟机是否能承受恶意代码的攻击。从性能的角度上讲,验证阶段的工作量在虚拟机的类加载中又占了相当大的一部分。

验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证,元数据验证,字节码验证,符号引用验证。

准备

准备阶段是正式为类变量分配内存并设置初始值的阶段,而这个过程的操作都会在方法区仲进行分配。

需要注意的是,如果是常量的话在编译阶段就已经将值放到常量池了。

解析

解析阶段就是虚拟机将常量池内的符号引用替换为直接引用。而这一阶段与类的初始化是不冲突的,并非一定要所有的解析结束以后才执行类的初始化。不同的 JVM 实现不同。

类的初始化

   初始化是执行类构造器<clinit>() 方法的过程。类的初始化是延迟的,直到类第一次被主动使用,JVM才会初始化类。

   初始化过程的主要操作是执行静态代码块和初始化静态域。在一个类被初始化之前,它的直接父类也需要被初始化。但是,一个接口的初始化,不会引起其父接口的初始化。在初始化的时候,会按照源代码中从上到下的顺序依次执行静态代码块和初始化静态域。

  类构造函数是由 Java 编译器完成的。它把类成员变量的初始化和 static 区间的代码提取出,放到一个<clinit>方法中。这个方法不能被一般的方法访问(注意,static final 成员变量不会在此执行初始化,它一般被编译器生成 constant 值)。同时,<clinit>中是不会显示的调用基类的<clinit>的,因为已经执行了基类的初始化。该初始化过程是由 Jvm 保证线程安全的。

因此一个类加载以及初始化过程:

      先为类的成员变量进行赋值,再执行类的构造函数。

参考资料:

www.hollischuang.com/archives/20…

深入理解Java虚拟机-类加载机制