Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型
一、类加载的时机
- 整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载
- 加载、验证、准备、初始化和卸载顺序是确定的,解析阶段就不一定
- 在某种情况下可以在初始化阶段之后再开始,为了支持 Java 的动态绑定
- 初始化情况
- new关键字实例化对象的时候
- 读取或设置一个类型的静态字段
- 调用一个类型的静态方法的时候
- 对类型进行反射调用的时候
- 初始化类的时候,父类没有进行过初始化
- 虚拟机启动的时候,需要指定一个要执行的主类(包含main方法)
- …
- 对于静态字段,只有直接定义这个字段的类才会被初始化
- 通过子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化
- 接口初始化过程
- 一个类初始化时,其父类全部都已经初始化过了,接口在初始化时,并不要求其父接口全部都完成初始化
- 只有在使用到父接口的时候(接口中定义的常量)才会初始化
- 一个类初始化时,其父类全部都已经初始化过了,接口在初始化时,并不要求其父接口全部都完成初始化
二、类加载的过程
即加载、验证、准备、解析和初始化这五个阶段
2.1 加载
- 完成的事情
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 可以从 ZIP 压缩包、JAR、EAR、WAR格式获取
- 从网络获取(Web Applet),运行时计算生成(反射)、数据库获取、加密文件获取
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
- 加载阶段既可以使用 Java 虚拟机里内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成(重写类加载器的 findClass() 或 loadClass())
- 数组类本身不通过类加载器创建,它是由 Java 虚拟机直接在内存中动态构造出来的
- 如果是引用类型,递归采用定义的加载过程去加载这个组件类型
- 如果不是引用类型,虚拟机会把数组标记为引导类加载器关联
- 类型数据安置在方法区之后,会在 Java 堆内存中实例化一个 java.lang.Class 类的对象,作为程序访问方法区中的类型数据的外部接口
- 加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的
- 白话文
- 用类的全限定名来获取这个类的二进制字节流,然后将这个字节流的静态存储结构转化为方法区的运行时数据结构,最后是在内存中生成一个java.lang.Class 对象,作为方法区这个类的数据访问入口
2.2 验证
- 确保 Class 文件的字节流中包含的信息符合规范的全部要求,确保不会危害虚拟机安全
- 比如:访问数组边界以外的数据、转型为它并未实现的类型、跳转到不存在的代码
- 很可能会因为载入了有错误或者有恶意企图的字节码流而导致整个系统受攻击甚至崩溃
- 四个阶段检验动作:文件格式、元数据、字节码、符号引用
- 文件格式验证
- 是否以魔数 0xCAFEBABE 开头、主次版本号是否在 Java 虚拟机接受范围之内、常量是否有不被支持的常量类型
- 文件格式验证
- 元数据验证,语义分析
- 是否有父类、父类是否继承了不允许被继承的类、是否实现了其父类或接口之中要求实现的所有方法
- 字节码验证
- 通过数据流分析和控制流分析,确定语义是合法的、符合逻辑的,就需要对类的方法体进行校验分析
- 保证任意时刻操作数栈道数据类型与指令代码序列都能配合工作(int 类型按 long 类型加载)
- 保证任何跳转指令都不会跳到方法体以外的字节码指令
- 保证方法体中的类型转化总是有效的(父类对象赋值给子类数据类型)
- 符号引用验证
- 符号引用验证可以看作是对类自身以外的各类信息进行匹配性校验
- 通俗:是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源
- 校验内容
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的可访问性是否可被当前类访问
- 符号引用验证可以看作是对类自身以外的各类信息进行匹配性校验
2.3 准备
- 准备阶段是正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并设置类变量初始值的阶段(初始值就是零值)
- 这时候进行内存分配仅包括类变量,而不包括实例变量,实例变量在初始化时随对象一起分配在 Java 堆中。
2.4 解析
- Java 虚拟机将常量池内的符号引用替换为直接引用的过程
- 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可
- 直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄
- 类或接口的解析(所处类为 D,符号引用 N,类或接口 C )
- 如果 C 不是一个数组类型,虚拟机将代表N的全限定名传递给D的类加载器去加载这个类 C
- 如果 C 是一个数组类型,并且数组元素类型为对象,N的描述符会是类似“[Ljava/lang/Integer” 的形式
- 上面两个步骤没有出现任何异常,那么 C 在虚拟机中实际上已经成为一个有效的类或接口
- 字段解析
- 先对字段表内 class_index 项中索引的 CONSTANT_Class_info 符号引用进行解析(字段所属的类或接口的符号引用)
- 解析成功就把这个字段所属的类或接口用C表示,继续对后续字段搜索
- 如果 C 本身就包含了简单名称和字段描述符都与目标相匹配的字段,直接返回字段的直接引用,否则,按照继承关系从下往上递归搜索各个接口和它父接口或者父类
- 查找过程成功返回了引用,会对这个字段进行权限验证
- 方法解析
- 先解析出方法表的 class_index 项中索引的方法所属的类或接口的符号引用
- 后续方法搜索
- Class 文件格式中类的方法和接口的方法符号引用的常量类型定义分开,在方法表中发现 class_index 中索引的C是一个接口的话抛异常
- 通过第一步,在类中查找是否有简单名称和描述符都与目标相匹配的方法,有就返回
- 否则,在类的父类中递归查找、在类实现的接口列表及它们的父接口之中递归查找
- 否则,失败
- 接口方法解析
- 解析接口方法表的 class_index 项中索引的方法所属的类或接口的符号引用
- 后续接口方法搜索
- 如果在接口方法表中发现 class_index 中索引 C 是个类而不是接口,抛异常
- 否则,在接口中查找是否有简单名称和描述符都与目标相匹配的方法
- 否则,在接口的父接口中递归查找
- Java 的接口允许多重继承,如果 C 的不同父接口中存有多个简单名称和描述符都与目标匹配,返回其中一个并结束查找
- 否则,失败
2.5 初始化
- Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主动权移交给应用程序
- 初始化阶段,根据程序员通过编码制定主观计划去初始化类变量和其他资源
- 初始化阶段就是执行类构造器()方法的过程
- ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{} 块)汇总语句合并产生,顺序由出现顺序决定
- ()方法与类的构造函数,不需要显式地调用父类构造器,Java 虚拟机保证子类执行前,父类已经执行完毕
- 接口不能使用静态语句块,但仍然有变量初始化的赋值操作,接口的()方法不需要先执行父接口()方法,只有被使用时才会初始化
三、类加载器
3.1 类与类加载器
- 只用于实现类的加载动作,每一个类加载器都拥有一个独立的类名称空间
- 不同类加载器加载的类,这两个类必定不相等
3.2 双亲委派模型
- 在虚拟机角度来看,只存在两种不同的类加载器:启动类加载器(Bootstrap ClassLoader)和其他所有的类加载器
- JDK 1.2 以来,Java 一直保持着三层类加载器、双亲委派的类加载结构
- 大多数 Java 程序会使用到以下 3 个系统提供的类加载器进行加载
- 启动类加载器:负责加载存放在 /lib 目录,Java 程序无法直接调用,需要委派给引导类加载器去处理
- 扩展类加载器:负责加载 /lib/ext 目录或者 java.ext.dirs 系统变量所指定的路径中所有的类库
- 应用程序类加载器:负责加载用户类路径(ClassPath)上所有的类库,是默认的类加载器
- 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器
- 通常使用组合关系来复用父加载器的代码
- 双亲委派模型工作过程
- 如果一个类加载器收到了类加载的请求,首先不是自己尝试加载,将这个请求委派给父类加载器去完成,每一个层次的类加载器都是这样,所以所有加载请求都应该传送到最顶层的启动类加载器中,只有父类无法加载到时候(没有这个类),子类才会尝试自己去加载
- 使用双亲委派模型可以保证在程序的各种类加载器环境中是同一个类
- 如果没有使用双亲委派模型,由各个类加载器自行去加载的话,会很混乱,有重名的类就可能加载不到
- 双亲委派模型源码流程
- 先检查请求加载到类型是否已经被加载过,若没有则调用父加载器的loadClass() 方法,若父加载器为空则默认使用启动类加载器作为父加载器,父类加载器加载失败抛异常后才会调用自己的 findClass() 方法尝试加载
- 破坏双亲委派模型
- 第一次在双亲委派模型出现之前,类加载器的概念和抽象类已经存在,需要兼容这些代码,无法避免 loadClass() 被子类覆盖的可能性
- 第二次是双亲委派模型自身的缺陷导致,基础类型又要调回用户的代码,添加线程上下文类加载器来解决
- 第三次是用户对程序动态性的追求而导致的,像代码热替换、模块热部署等
- OSGI 实现模块化热部署的关键是它自定义的类加载机制的实现,不再双亲委派模型推荐的树状结构
- 将 java.* 开头的类,委派给父类加载器加载
- 否则,将委派列表名单内的类,委派给父类加载器加载…
四、Java 模块化系统
- 实现模块化的关键目标–可配置的封装隔离机制
- Java模块定义还包括
- 依赖其他模块的列表
- 导出的包列表
- 开放的包列表
- 使用的服务列表
- 提供服务的实现列表
- 模块的兼容性
- JDK9提出了与“类路径”(ClassPath)相对应的“模块路径”(ModulePath)的概念
- …
- 模块下的类加载器
- 扩展类加载(Extension Class Loader)被平台类加载器(Platform Class Loader)取代,三层类加载器和双亲委派模型没有改变