用一张思维导图尽可能囊括一下JVM的类加载过程的全流程。

本文参考自来自周志明《深入理解Java虚拟机(第2版)》,拓展内容建议读者可以阅读下这本书。

class loading

文字版如下:

加载 Loading

过程

  • 通过类的全限定名来获取定义此类的二进制字节流

    • 非数组类的加载,由类加载器加载,可以是启动类加载器,也可以是用户自定义的类加载器
    • 数组类的加载,不由类加载器创建,而是由JVM直接在内部创建

      • 组件类型(数组降一维后的类型)是引用类型,递归调用加载过程直到降到一维类型后通过类加载器加载,数组类型最终标识为此类加载器所加载,数组类可见性和组件类型保持一致
      • 组件类型不是引用类型而是原始类型,则该数组类型的类加载器将标识为启动类加载器,数组类型可见性为public
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中(HotSpot为方法区)生成一个代表了这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

类加载器

  • 启动类加载器 Bootstrap ClassLoader,加载<JAVA_HOME>/lib中的类
  • 拓展类加载器 Extension ClassLoader,加载<JAVA_HOME>/lib/ext中的类
  • 应用程序类加载器 Application ClassLoader,加载用户类路径上的ClassPath中的类
  • 自定义类加载器 User ClassLoader

连接 Linking

验证 Verification

  • 文件格式验证:字节流是否符合Class文件格式规范

    • 是否以magic开头
    • 主次版本号是否在虚拟机处理范围内
    • 常量池中的常量是否有不支持的类型
    • 指向的常量索引值是否有指向不存在常量或不符合类型常量的情况
    • CONSTANT_Utf8_info的常量是否符合utf8编码规范
    • Class文件各个部分及文件本身是否有被删除或附加的其他信息
  • 元数据验证:字节码描述的信息进行语义分析

    • 是否有父类
    • 父类是否继承了不允许被继承的类(final的)
    • 如果不是抽象类是否实现了其父类或接口之中要求实现的类
    • 类中字段、方法是否与父类产生矛盾

      • 覆盖了父类的final字段
      • 不符合规范的方法重载
方法参数类型一致返回值类型不一致
  • 字节码验证:通过数据流和控制流分析程序语义的合法性,即类的方法体的校验分析

    • 保证时刻操作数栈与指令代码序列能配合工作
    • 保证跳转指令不会跳转到方法体以外的字节码指令上
    • 保证方法体的类型转换是有效的
  • 符号引用验证:类的常量池中各种符号引用的信息进行匹配性校验

    • 符号引用中通过字符串描述的全限定名是否能找到对应类
    • 指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
    • 符号引用中的类、字段和方法的访问性是否可被当前类所访问

连接 Linking

验证 Verification

  • 文件格式验证:字节流是否符合Class文件格式规范

    • 是否以magic开头
    • 主次版本号是否在虚拟机处理范围内
    • 常量池中的常量是否有不支持的类型
    • 指向的常量索引值是否有指向不存在常量或不符合类型常量的情况
    • CONSTANT_Utf8_info的常量是否符合utf8编码规范
    • Class文件各个部分及文件本身是否有被删除或附加的其他信息
  • 元数据验证:字节码描述的信息进行语义分析

    • 是否有父类
    • 父类是否继承了不允许被继承的类(final的)
    • 如果不是抽象类是否实现了其父类或接口之中要求实现的类
    • 类中字段、方法是否与父类产生矛盾

      • 覆盖了父类的final字段
      • 不符合规范的方法重载
方法参数类型一致返回值类型不一致
  • 字节码验证:通过数据流和控制流分析程序语义的合法性,即类的方法体的校验分析

    • 保证时刻操作数栈与指令代码序列能配合工作
    • 保证跳转指令不会跳转到方法体以外的字节码指令上
    • 保证方法体的类型转换是有效的
  • 符号引用验证:类的常量池中各种符号引用的信息进行匹配性校验

    • 符号引用中通过字符串描述的全限定名是否能找到对应类
    • 指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
    • 符号引用中的类、字段和方法的访问性是否可被当前类所访问

准备 Preparation

  • 类变量分配空间
  • 类变量分配零值
  • 类常量分配初始值:由类字段的ConstantValue属性进行赋值

解析 Resolution

  • 实际上就是把常量池中的符号引用替换为直接引用的过程

    • 符号引用

      • 在常量池中即非字面量的类型

        • CONSTANT_Class_info
        • CONSTANT_Fieldref_info
        • CONSTANT_Methodref_info
        • CONSTANT_InterfaceMethodref_info
      • 特征

        • 与虚拟机实现的内存布局无关
        • 引用的目标并不一定已经加载到内存中
        • 由虚拟机Class文件格式规范,因此不同虚拟机能够接受的符号引用格式是确定的
    • 直接引用

      • 表达形式

        • 直接指向目标的虚拟机内存中的指针
        • 相对偏移量
        • 能够定位到目标的句柄
      • 特征

        • 与虚拟机的内存布局直接相关
        • 引用的目标必须已经存在于内存中
        • 同一符号引用在不同虚拟机中的直接引用一般不同,由虚拟机自己制定格式
  • 符号引用解析

    • 类或接口的解析

      • 对CONSTANT_Class_info符号引用的解析

        • 对全限定名的解析
      • 虚拟机加载类D中的类符号引用N为一个类或接口C的直接引用

        • C不是数组类型

          • 虚拟机将N代表的全限定名传递给D的类加载器来加载C
          • C被成功加载后(可能是之前已经加载过或者本次执行了首次加载),虚拟机将D中的符号引用N替换为C的直接引用
          • 符号引用验证,如D是否具备对C的访问权限
        • C是数组类型


          • 虚拟机将N代表的全限定名(如[Ljava.lang.Integer)传递给D的类加载器来加载C(详见数组类加载流程)

            • D的类加载器先加载C的组件类型(如java.lang.Integer)
            • 虚拟机在方法区生成一个代表了数组维度和组件类型的数组对象
          • C被成功加载后,虚拟机将D中的符号引用N替换为C的直接引用
          • 符号引用验证,如D是否具备对C的访问权限
    • 字段解析

      • 对CONSTANT_Fieldref_info符号引用的解析

        • 对class_index的解析
        • 对nameAndType_index的解析
      • 虚拟机在类D中加载字段符号引用N为字段F的直接引用

        • 虚拟机在N中指定的类C里寻找字段描述符和N中指定的字段描述符一致的字段F

          • 能找到,就将符号引用N替换为F的直接引用

            • 符号引用验证,如D是否具备对F的访问权限
          • 找不到,在类C实现的接口中按照继承关系从下向上寻找字段描述符和N中指定的字段描述符一致的字段F

            • 能找到就将符号引用N替换为F的直接引用

              • 符号引用验证,如D是否具备对F的访问权限
            • 找不到,在类C继承的父类中按照继承关系从下向上寻找字段描述符和N中指定的字段描述符一致的字段F

              • 能找到就将符号引用N替换为F的直接引用

                • 符号引用验证,如D是否具备对F的访问权限
              • 找不到,抛出java.lang.NoSuchFieldError异常
    • 类方法解析

      • 对CONSTANT_Methodref_info符号引用的解析

        • 对class_index的解析
        • 对nameAndType_index的解析
      • 虚拟机在类D中加载类方法符号引用N为方法M的直接引用

        • 虚拟机在N中指定的类C里寻找方法描述符和N中指定的方法描述符一致的方法M

          • 能找到

            • class_index指定的类不是接口,就将符号引用N替换为M的直接引用
            • class_index指定的类是接口,抛出java.lang.IncompatibleClassChangeError异常
            • 符号引用验证,如D是否具备对M的访问权限
          • 找不到,在类C继承的父类中按照继承关系从下向上寻找方法描述符和N中指定的方法描述符一致的方法M

            • 能找到就将符号引用N替换为M的直接引用

              • 符号引用验证,如D是否具备对M的访问权限
            • 找不到,在类C实现的接口中按照继承关系从下向上寻找方法描述符和N中指定的方法描述符一致的方法M

              • 能找到,说明类C是抽象类,抛出java.lang.AbstractMethodError异常(为什么说明C是抽象类呢?C的方法在C中找不到,但是在C实现的接口中找到了,这意味着C实现了接口但是没有实现接口的这个方法,因此C类只可能是抽象类。)
              • 找不到,抛出java.lang.NoSuchMethodError异常
    • 接口方法解析

      • 对CONSTANT_InterfaceMethodref_info符号引用的解析

        • 对class_index的解析
        • 对nameAndType_index的解析
      • 虚拟机在类D中加载接口方法符号引用N为方法M的直接引用

        • 虚拟机在N中指定的接口C里寻找方法描述符和N中指定的方法描述符一致的方法M

          • 能找到

            • class_index指定的类是C接口,就将符号引用N替换为M的直接引用
            • class_index指定的类C不是接口,抛出java.lang.IncompatibleClassChangeError异常
            • 符号引用验证,接口方法都是public的所以没有访问权限的问题
          • 找不到,在接口C继承的父接口中按照继承关系从下向上寻找方法描述符和N中指定的方法描述符一致的方法M

            • 能找到就将符号引用N替换为M的直接引用
            • 找不到,抛出java.lang.NoSuchMethodError异常

初始化 Initialization

初始化就是执行<clinit>()方法的过程

<clinit>()

  • <clinit>()是编译期生成在Class字节码中的,由编译器自动收集类中的所有类变量的赋值动作和静态代码块static{…}中的语句合并而成
  • <clinit>()是类构造器,与实例构造器<init>()不同,虚拟机保证会在调用前先调用其父类的<clinit>(),因此不需要显式调用父类构造器
  • 父类的<clinit>()对类变量的赋值操作优先于子类的<clinit>()执行
  • <clinit>()并非必需,如果类中无静态代码块或对类变量的赋值操作,那么编译器可以不生成<clinit>()方法,Class字节码中也就没有<clinit>()方法
  • 接口无静态代码块但是可以用类变量赋值操作,因此也会生成<clinit>方法,但是不需要先调用父接口的<clinit>()方法,只有父接口的类变量使用时才调用<clinit>()方法初始化父接口
  • 虚拟机会保证多线程环境下类的<clinit>()方法可以阻塞地调用,即线程T1调用类C的<clinit>()方法初始化C的过程中,线程T2会阻塞而无法进入类C的<clinit>()方法中的

使用 Using

卸载 Unloading


JinhaoPlus
1.5k 声望92 粉丝

扎瓦程序员