Java 虚拟类加载机制

类加载的时机

  • 遇到 new、getstatic、putstatic或 invokestatic

    这四条字节码指令时:

    • 使用 new 关键字实例化对象时。
    • 读取或设置一个类型的静态字段时。
    • 调用一个类型的静态方法时。
  • 使用 java.lang.reflect 包的方法对类型进行反射调用时,如果类型没有进行过初始化,则需要先触发其初始化。

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

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

  • 当使用 JDK 7 新加入的动态语言支持时。

  • 当一个接口中定义了 JDK 8 新加入的默认方法时。

类加载的过程

加载

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
  • 并没有指明二进制字节流必须得从某个 Class 文件中获取。

验证

  • 文件格式验证
    • 检查是否以魔数 0xCAFEBABE 开头。
    • 主、次版本号是否在当前 Java 虚拟机接受范围之内。
    • 常量池的常量中是否有不被支持的常量类型(检查常量 tag 标志)。
    • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
    • CONSTANT_Utf8_info 型的常量中是否有不符合 UTF-8 编码的数据。
    • 检查 Class 文件中各个部分及文件本身是否有被删除或附加的其他信息。
  • 元数据验证
    • 检查类是否有父类(除了 java.lang.Object 之外,所有的类都应当有父类)。
    • 检查类的父类是否继承了不允许被继承的类(被 final 修饰的类)。
    • 如果类不是抽象类,检查是否实现了其父类或接口中要求实现的所有方法。
    • 检查类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现不符合规则的方法重载)。
  • 字节码验证
    • 校验类的方法体(Class 文件中的 Code属性),保证类的方法在运行时不会做出危害虚拟机安全的行为:
      • 保证任意时刻操作数栈的数据类型与指令代码序列能配合工作。
      • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
      • 保证方法体中的类型转换总是有效。
    • 如果类的方法体字节码没有通过字节码验证,则该类肯定有问题;但通过字节码验证也不能保证其完全安全。
    • Code属性的属性表中新增了一项名为 StackMapTable 的属性:
      • 描述方法体所有的基本块(Basic Block,即按控制流拆分的代码块)开始时本地变量表和操作栈应有的状态。
      • Java 虚拟机不需要根据程序推导这些状态的合法性,只需检查 StackMapTable 属性中的记录是否合法即可。这样将字节码验证的类型推导转变为类型检查,节省了大量校验时间。
  • 符号引用验证
    • 对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,即检查类是否缺少或被禁止访问它依赖的某些外部类、方法、字段等资源。
    • 校验内容包括:
      • 符号引用中通过字符串描述的全限定名是否能找到对应的类。
      • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
      • 符号引用中的类、字段、方法的可访问性。

准备

  • 正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并设置类变量初始值的阶段。
  • 此时进行内存分配的仅包括类变量,而不包括实例变量。
  • 初始值通常为数据类型的零值。

解析

  • 将符号引用替换为直接引用的过程:
    • 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
    • 直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关。
  • 虚拟机可以对第一次解析的结果进行缓存。
  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这 7 类符号引用进行:
    • CONSTANT_Class_info
    • CONSTANT_Fieldref_info
    • CONSTANT_Methodref_info
    • CONSTANT_InterfaceMethodref_info
    • CONSTANT_MethodType_info
    • CONSTANT_MethodHandle_info
    • CONSTANT_Dynamic_info
    • CONSTANT_InvokeDynamic_info

初始化

  • 类的初始化阶段是类加载的最后一步,在这个阶段,Java 虚拟机会根据程序员在类中定义的代码为类变量赋予正确的初始值。

类加载器

类加载器(Class Loader) 是将类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到 Java 虚拟机外部实现,以便应用程序自己决定如何获取所需的类。

类与类加载器

  • 每个类都必须由加载它的类加载器和该类本身一起共同确立其在 Java 虚拟机中的唯一性
  • 每个类加载器都拥有一个独立的类名称空间。比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。

双亲委派模型

  • Java 一直保持着三层类加载器、双亲委派的类加载架构:
    • 启动类加载器(Bootstrap Class Loader):
      • 负责加载存放在 \lib 目录,或者被 -Xbootclasspath 参数所指定路径中存放的,并且是 Java 虚拟机能够识别的(按照文件名识别,如 rt.jartools.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库。
    • 扩展类加载器(Extension Class Loader):
      • sun.misc.Launcher$ExtClassLoader 中以 Java 代码的形式实现。
      • 负责加载 \lib\ext 目录中,或者被 java.ext.dirs 系统变量所指定路径中的所有类库。
    • 应用程序类加载器(Application Class Loader):
      • sun.misc.Launcher$AppClassLoader 实现,负责加载用户类路径(ClassPath)上的所有类库。

双亲委派模型

  • 各种类加载器之间的层次关系被称为类加载器的“双亲委派模型”:
    • 双亲委派模型要求除顶层的启动类加载器外,其余类加载器都应有自己的父类加载器。
    • 类加载器之间的父子关系一般不是通过继承关系实现的,而是通常使用组合关系来复用父加载器的代码
    • 工作过程:
      • 如果一个类加载器收到类加载请求,它首先不会自己去尝试加载该类,而是将请求委派给父类加载器完成
      • 每个层次的类加载器都如此,因此所有加载请求最终都应该传送到最顶层的启动类加载器中。
      • 只有当父加载器反馈自己无法完成此加载请求(即在其搜索范围中没有找到所需的类)时,子加载器才会尝试自己完成加载

显而易见的好处

  • Java 中的类随着其类加载器一起具备一种带有优先级的层次关系。
  • 例如类 java.lang.Object,它存放在 rt.jar 之中,无论哪个类加载器要加载此类,最终都会委派给处于模型最顶端的启动类加载器进行加载。
    • 因此 Object 类在程序的各种类加载器环境中都能保证是同一个类

破坏双亲委派模型

  • 在一些特殊情况下,双亲委派模型可能被打破。例如在自定义类加载器的实现中,如果不调用父类加载器的 loadClass 方法,而是直接加载类,这就破坏了双亲委派模型。

Java 模块化系统

模块的兼容性

模块化下的类加载器