虚拟机类加载机制

概述

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

由于不需要在编译时进行连接,java语言的类型加载、连接和初始化过程都是运行期完成,这样的策略可以提供更高的灵活性:

  • 面向接口的应用程序,在运行期再指定实际的实现类。
  • 用户通过自定义的类加载器,让一个本地的应用程序可以在运行期从网络或者其他地方加载一个二进制流作为程序的一部分,如JSP、OSGi技术。

类加载的时机

类的整个生命周期包括:

  1. 加载(Loading)
  2. 验证(Verification)
  3. 准备(Preparation)
  4. 解析(Resolution)
  5. 初始化(Intialization)
  6. 使用(Using)
  7. 卸载(Unloading)

jvm规范严格规定了 有且只有 5种情况必须对类进行初始化:

  1. 使用new实例化对象、读取或设置一个类的静态字段(static final修饰的除外)、调用一个类的静态方法
  2. java.lang.reflect包对类进行反射调用
  3. 初始化一个类,如果发现其父类没有初始化,需要先初始化其父类;但是接口不会先初始化其父类接口
  4. 虚拟机启动时,会先初始化执行主类(包含main()方法的类)
  5. 当使用JDK1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic/REF_putStatic/REF_invokeStatic的方法句柄对应的类没有初始化时,先触发其初始化

这5中场景中的行为称为对一个类进行主动引用,除此之外,所以引用类的方式都不会触发初始化,称为被动引用。

被动引用举例:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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 NoInitialization {
    public static void main(String[] args) {
    System.out.println(SubClass.value);
    }
    }
  • 通过数组定义来引用类,不会触发该类的初始化

    1
    2
    3
    4
    5
    public class NoInitialization {
    public static void main(String[] args) {
    SuperClass[] sca = new SuperClass[10];
    }
    }

    此处没有初始化SuperClass,但是触发了一个叫做”[Lpackagename.SuperClass”的类的初始化。这个类有虚拟机自动生成,直接继承自Object类。

  • 引用常量不会触发初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class ConstClass {
    static {
    System.out.println("ConstClass init");
    }
    public static final String HELLOWORLD = "hello world!";
    }

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

    这个在java中叫做常量传播优化,在编译阶段,已经将常量的值”hello world!”存储到NoInitialization类的常量池中,以后NoInitialization对ConstClass.HELLOWORLD的引用实际上都转换为对自身常量池的引用,与ConstClass没有关系了。

类加载的过程

  1. 加载

    加载阶段虚拟机完成以下任务:

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

      jvm从未具体指定如何获取二进制字节流的方式,于是发展出了许多多样的加载方式:

      • 从zip包读取,这是JAR/EAR/WAR格式的基础
      • 从网络获取,如Applet
      • 运行时计算生成,如动态代理技术,在java.lang.reflect.Proxy中使用了ProxyGenerator.generateProxyClass来生成”*$Proxy”的代理类字节流
      • 由其他文件生成,如JSP
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

    3. 在内存中生成一个java.lang.Class对象(并没有明确规定是在java堆中,HotSpot中,虽然Class对象是对象,但是存放在方法区中),作为方法区这个类的各种数据的访问入口
  2. 验证

    1. 文件格式验证

      验证字节流是否符合class文件格式的规范,是否能被当前虚拟机处理。

      • 魔数0xCAFEBABE开头
      • 版本号
      • 常量池索引是否有指向不存在的常量
      • CONSTANT_Utf8_info是否有不适合UTF-8的数据
    2. 元数据验证

      对字节码进行语义分析,以保证其符合java语言规范。

      • 这个类是否有父类
      • 是否继承了不允许被继承final的类
      • 如果不是抽象类,是否实现了父类或者接口中要求实现的所有方法
      • 类中字段、方法是否矛盾
    3. 字节码验证

      通过数据流或控制流分析,确定程序语义是否合法、符合逻辑,对类的方法体进行校验分析。

      • 保证任意时刻操作数栈的数据类型与指令代码序列能配合工作
      • 保证跳转指令不会跳转到方法体之外的字节码指令上
      • 保证方法体中的类型转换是有效的
    4. 符号引用验证

      • 符号引用中通过字符串描述的全限定名是否能找到类
      • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
      • 访问性(private, protected, public, default)是否可达
  3. 准备

    为类变量(static量而非实例变量)分配内存并设置类变量初始值(是数据类型的零值,int的0、boolean的false、reference的null等)的阶段,这些变量的内存都在方法区内进行分配。

  4. 解析

    虚拟机将常量池中的符号引用替换为直接引用的过程。

    • 符号引用(Symbolic References):

      以一组符号来描述所引用的目标,可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

    • 直接引用(Direct References):

      可以是指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。

    1. 类或接口的解析

      假设当前代码所处的类是D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,需要如下3步:

      1. 如果C不是一个数组类型,jvm会将N的全限定名传递给D的类加载器去加载这个类C。
      2. 如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符是类似”[Ljava/lang/Integer”的形式,那会按照第一点的规则加载数组元素类型。
      3. 如果上面的步骤没有异常,C已经在jvm中是个有效的类了,在解析完成之前,还要进行符号引用验证,确定D对C的访问权限。
    2. 字段解析

      1. 如果C本身就包含了简单名称和字段描述符相匹配的字段,则直接返回这个字段的直接引用,查找结束。
      2. 否则,如果C实现了接口,将会按继承关系从下往上递归搜索各个接口,如果出现了相匹配字段则返回直接引用,查找结束。
      3. 否则,如果C不是java.lang.Object的话,按继承关系从下往上递归搜索其父类,如果出现了相匹配字段则返回直接引用,查找结束。
      4. 否则,返回java.lang.NoSuchFieldError异常。
      5. 在成功返回引用后,将会对这个字段进行权限验证,如果不具备访问权限,会抛出java.lang.IllegalAccessError异常。
    3. 类方法解析

      除了先要校验C是否是接口外,基本与字段解析类似。

    4. 接口方法解析

      类似于类方法解析

  5. 初始化

    初始化阶段是执行类构造器()方法的过程。

    • ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序所决定。
    • ()方法与类的构造函数()不同,它不需要显式调用父类构造器,因为虚拟机会保证在子类()调用之前父类已经初始化完毕。
    • ()方法对于类或者接口并不是必需的
    • 接口在初始化时不会先执行父类接口的()方法
    • 虚拟机会保证一个类的()在多线程环境下被正确加锁、同步

类加载器

虚拟机设计团队将类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放在jvm之外去实现,一遍让应用程序自己决定如何去获取所需要的类,这个动作的代码模块就是“类加载器”。

  1. 类与类加载器

    两个类是否相等,只有在这两个类由同一个类加载器加载的前提下才有意义,否则,即使来自从一个Class文件,被同一个jvm加载,也必定不相等。

  2. 双亲委派模型

    绝大多数java程序都会用到以下3种类加载器:

    • 启动类加载器(Bootstrap ClassLoader)

      加载\lib中类库

    • 扩展类加载器(Extension ClassLoader)

      加载\lib\ext中的类库

    • 应用程序类加载器(Application ClassLoader)

      加载用户ClassPath上指定的类库

      类加载器的双亲委派模型(Parents Delegation Model)

      ParentsDelegationModel

      使用组合而不是继承来复用父类加载器的代码。

      保证了一种带有优先级的层次关系。

      实现代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      protected synchronized Class<?> loadClass (String name, boolean resolve)
      throws ClassNotFoundException {
      Class c = findLoadedClass(name);
      if (c == null) {
      try {
      if (parent != null) {
      c = parent.loadClass(name, false);
      }else {
      c = findBootstrapClassOrNull(name);
      }
      }catch (ClassNotFoundException e) {

      }
      if (c == null) {
      c = findClass(name);
      }
      }
      if (resolve) {
      resolveClass(c);
      }
      return c;
      }
  3. 破坏双亲委派模型

    1. JDK1.2双亲委派模型引入之前
    2. 基础类需要调用回用户代码,如JNDI/JDBC等
    3. 追求程序动态性,如HotSwap、Hot Deployment等