跳到主要内容

07 虚拟机类加载机制

虚拟机类加载机制

概述

  • 虚拟机的类加载机制: Java虚拟机把 描述类的数据 从 Class文件 加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
  • 类型的加载、连接和初始化的都是在 程序运行期间 完成的,这种策略让Java语言进行提前编译会面临额外的困难,类加载时会增加性能开销,但是提供了极高的扩展性和灵活性,Java天生可动态扩展的特性就是依赖 运行时动态加载动态连接 的特点实现的

类加载的时机

  • 类的生命周期: 加载(Loading)、验证(Verification)、准备(Preparation)、初始化(Initialization)、使用(Using)、卸载(Unloading) 七个阶段,验证、准备、解析 三部分统称为 连接(Linking)
    类的生命周期
    • 初始化阶段:
      • 主动引用: 以下场景发生时,如果类型没有进行过初始化,则需要先触发其初始化阶段
        1. 遇到 newgetstaticputstaticinvokestatic 指令时
        2. 使用 java.lang.reflect 包的方法对类型进行反射调用时
        3. 初始化类时,其父类还没进行初始化,要先触发父类的初始化
        4. 虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),这个类会被虚拟机先初始化
        5. 使用 JDK7 新加入的动态语言支持时
        6. 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果接口的实现类发生初始化,接口要在其之前被初始化
      • 被动引用: 所有引用类型的方式都不会触发初始化

类加载的过程

加载

  • 加载阶段需要完成的三件事
    1. 通过一个类的全限定名来获取定义此类的二进制字节流 (.class文件)
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    3. 在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区中这个类的数据入口
  • Java虚拟机规范 没有指明二进制字节流必须从某个 Class文件 中获取,因此衍生出以下技术:
    • 从ZIP压缩包中读取,JAR、EAR、WAR 等格式的基础
    • 从网络中获取,比如 Web Applet
    • 运行时计算生成,动态代理技术等
    • 由其他文件生成,JSP应用,由JSP文件生成对应的 Class文件
    • 从数据库中读取,某些中间件服务器(比如 SAP Netweaver) 可以把程序安装到数据库完成集群间的代码分发
    • 从加密文件中获取,典型的防Class文件被反编译的保护措施,通过加载时解密Class文件的方式保障程序运行逻辑不被窥探
  • 加载阶段可以使用 虚拟机内置的引导类加载器 来完成,也可以使用 用户自定义的类加载器 完成,数组类型除外
  • 数组类本身不通过类加载器创建,由Java虚拟机 直接在内存中动态构造 出来
  • 加载阶段与连接阶段的部分动作(比如一部分字节码文件格式校验动作) 交叉进行

验证

  • 目的是确保 Class文件 的字节流中包含的信息符合 Java虚拟机规范 的全部约束要求,是 Java虚拟机 保护自身的一项必要措施
  • 验证阶段的检验动作:
    1. 文件格式验证: 验证字节流是否符合 Class文件格式的规范,并能被当前版本的虚拟机处理(魔数、主次版本号、常量池中的常量类型等),通过了这个阶段的验证后字节流才被允许进入 Java虚拟机 内存的方法区中进行存储,此后的三个验证阶段全部 基于方法区的存储结构上进行 ,不会再直接读取、操作字节流
    2. 元数据验证: 对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java虚拟机规范 的要求
    3. 字节码验证(最复杂的阶段): 通过数据流分析和控制流分析确定程序语义是合法的、符合逻辑的,对类的方法体(Class文件中的Code属性)进行校验分析,确保被校验类的方法在运行时不会做出危害虚拟机安全的行为
      • StackMapTable: 描述方法体所有的基本块(Basic Block,指按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,字节码验证期间只需要检查这个属性中的记录是否合法即可,而不需要根据程序推导这些状态的合法性,把字节码验证从类型推导转变为类型检查,节省大量校验时间
    4. 符号引用验证: 发生在虚拟机将符号引用转化为直接引用时 (转化动作发生在连接的第三阶段--解析阶段),可以看作对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,主要目的是确保解析行为能正常执行

准备

  • 正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并 设置类变量初始值 的阶段

解析

  • Java虚拟机 将常量池内的符号引用替换为直接引用的过程
  • 符号引用和直接引用
    符号引用(Symbolic References): 以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的对象不一定是已经加载到虚拟机内存当中的内容
    直接引用: 可以直接指向目标的 指针相对偏移量 或者是一个能间接定位到目标的 句柄,与虚拟机实现的 内存布局直接相关,有了直接引用,则引用的目标必定已经在虚拟机的内存中存在

初始化

  • 类加载过程的最后一个步骤,Java虚拟机 真正开始执行类中编写的Java程序代码,将主导权移交给应用程序
  • 在准备阶段时已经被赋值过一次系统要求的初始零值的变量,在初始化阶段会根据程序员通过程序编码制定的主观计划去 初始化类变量和其他资源,初始化阶段就是 执行类构造器<clinit>()方法 的过程
  • <clinit>():
    • 不是程序员在 Java代码 中直接编写的方法,而是 Java编译器的自动生成物,由编译器自动收集类中的 所有类变量的赋值动作静态语句块(static{}块) 中的语句合并产生的
    • 与类的构造函数不同,不需要显示调用父类构造器,Java虚拟机 会保证在子类的 <clinit>() 执行完成前,父类的已经执行完毕(第一个被执行 <clinit>()方法 的类型必定是 java.lang.Object
    • 对于类和接口来说 不是必需的,如果类中没有静态代码块,也没有对变量的赋值操作,编译器可以不为这个类生成此方法
    • 接口中不能使用静态代码块,但是有变量初始化的赋值操作,因此也会生成此方法,但是并不需要先执行父接口中的 <clinit>()方法,因为只有当父接口中定义变量被使用时,父接口才会被初始化
    • 多个线程同时初始化一个类时,只有其中一个线程回去执行这个类的 <clinit>()方法,其他线程都需要阻塞等待直到活动线程执行完毕(有可能造成多个线程阻塞)

类加载器(Class Loader)

类与类加载器

  • 通过一个类的全限定名来获取描述该类的二进制字节流,实现这个动作的代码称为类加载器(Class Loader),这个动作 放在虚拟机外部实现,以便让应用程序自己决定如何获取所需的类
  • 对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立它在Java虚拟机中的唯一性,每一个类加载器都拥有一个 独立的类名称空间(两个类来源于同一个Class文件,被同一个虚拟机加载,加载它们的类加载器不同,两个类必不相等)

双亲委派模型(Parents Delegation Model)

  • Java虚拟机角度: 只有两种不同的类加载器
    1. 启动类加载器(Bootstrap Class Loader): 使用 C++ 实现,是虚拟机自身的一部分
    2. 其他所有类加载器: 使用 Java 实现,独立存在于虚拟机外部,全部继承自抽象类 java.lang.ClassLoader
  • Java开发人员角度: 三层类加载器、双亲委派的类加载架构
    1. 启动类加载器(Bootstrap Class Loader): 负责加载存放在 <JAVA_HOME>\lib 目录中、被 -Xbootclasspath 参数所制定的路径中存放的能被Java虚拟机识别的类库(按照文件名识别)
    2. 扩展类加载器(Extension Class Loader): 在类 sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的,负责加载 <JAVA_HOME>\lib\ext 目录中、被 java.ext.dirs 系统变量所指定的路径中的所有类库
    3. 应用程序类加载器(Application Class Loader): 由 sun.misc.Launcher$AppClassLoader 实现,负责加载用户路径(ClassPath)上所有的类库,如果应用程序中没有自定义自己的类加载器,这个一般情况下就是程序中默认的类加载器
  • 双亲委派模型(Parents Delegation Model): 类加载器之间的父子关系不是依靠继承来实现的,而是通过组合的方式来复用父加载器的代码
    双亲委派模型
  • OSGI实现模块化热部署: 自定义类加载器机制的实现,每一个程序模块(OSGI中称为Bundle)都有一个自己的类加载器,当需要替换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换

Java模块化系统

  • 模块化的关键目标: 可配置的封装隔离机制
  • 未看待定