文章目录
目录
前言
主要介绍了虚拟机类加载机制。
一、类加载机制
- 时机(程序运行时进行,提高可扩展性和灵活性)
- 类加载的加载、连接、初始化三个过程中,明确规定了初始化开始的时机
- 主动引用类时会进行初始化
- new实例化对象、访问static字段或方法
- 用子类时父类未初始化
- 反射调用某个类的类型未初始化时
- 启动虚拟机时调用主类需先初始化该主类
- 方法句柄对应的类未初始化时
- 接口中含default修饰的默认方法,该接口的实现类初始化时,要先初始化该接口
- 被动使用类时不会进行初始化
- 子类访问父类static字段(静态字段处于哪个类就初始化哪个类),只初始化父类,不初始化子类
- 数组定义引用类(SuperClass[] a),定义a时不会初始化SuperClass,此时初始化的是虚拟机自动生成的直接继承java.lang.Object的对象
- 引用类中final常量时类不会初始化,常量直接存入方法区的运行时常量池,没有直接引用到定义常量的类
- 接口初始化时不要求先初始化其父接口,引用父接口定义的常量时才会初始化它
- 过程
- 加载、验证、准备、初始化、卸载按顺序开始执行(执行过程可能有交叉,但是开始时机按顺序进行),解析可能会在初始化之后(动态绑定特性)
- 加载
- 过程
- 根据全限定名找到对应二进制字节流
- 开发人员可控性最强的阶段,可通过自行创建类加载器,按照自己想要的方式获取二进制字节流
- 字节流代表的静态数据结构加载到方法区中,成为运行时数据结构
- 为该类生成java.lang.class对象,作为方法区中该类类型数据的访问接口
- 根据全限定名找到对应二进制字节流
- 数组类的创建
- 数组类不由类加载器创建,而是由虚拟机直接在内存中创建
- 过程
- 数组类的组件类型为引用类型,递归加载组件类型,并将数组标识在类加载器命名空间上
- 数组类的组件类型不是引用类型,数组与引导类加载器关联
- 过程
- 验证
- 重要但不必须的阶段,如果程序运行的全部代码已经反复使用并验证通过了,则无需再进行重复验证操作
- 过程
- 文件格式验证
- 检查魔数、版本、常量等,保证字节流能正确解析并存入方法区
- 元数据验证
- 字节流进行语义分析、语义校验,校验数据类型
- 字节码验证
- 通过数据流分析和控制流分析,校验方法体
- 由于数据流、控制流分析的高度复杂,引入StackMapTable记录基本块中本地变量表和操作栈状态,在验证阶段只需检查StackMapTable的内容无需再推导
- 符号引用验证
- 在进行从符号引用到直接引用转化的解析过程中,对该被加载的类的外部依赖信息的匹配性校验(检查该类是否缺少或者被禁止访问某些它依赖的外部类、方法、字段等资源)
- 文件格式验证
- 准备
- 为类中定义的静态static变量在方法区中分配内存空间并置初始零值
- 实例变量会在实例化时分配在堆中
- 类中定义的常量final在准备阶段不初始化为零值,而是直接初始化为指定初始值
- 解析
- 符号引用到直接引用(与虚拟机的内存布局相关)的转化过程,寻找外部依赖的过程
- 时机
- ‘静态’ 指令可以在加载完成后还未实际执行代码时就进行解析,第一次解析结果可以缓存,第一次成功后续一直可以成功,第一次失败就算后期需要的信息已经加载到内存中也会收到异常提示
- 对于‘动态’ 指令invokedynamic ,只有实际运行到该指令时才会解析
- ‘静态’ 解析实例(在类D代码段中根据符号引用N解析出)
- 类或接口C
- C不是数组则将N传递给D的类加载器;C是数组且数组元素类型是对象,则让D的类加载器加载数组的元素类型,接着由虚拟机生成代表数组维度和元素的数组对象,访问权限检查
- 字段F
- 找到并解析字段F所属的类或接口C的符号引用,C本身含有字段F;C的父类、父接口含有字段F,访问权限检查
- 方法M
- 找到并解析方法M所属的类C的符号引用(若找到的C是接口则抛出异常),C本身含有方法M,C的父类含有方法M(方法M在C的父接口中找到会抛出异常),访问权限检查
- 接口方法M
- 找到并解析方法M所属的接口C的符号引用(若找到的C是类则抛出异常),C本身含有方法M,C的父接口含有方法M(出现多重继承中不同父接口含有相同字段名、方法名时只返回其中一个的直接引用,有的发行商拒绝编译这种多重继承下的不同父接口中同名情况),模块化之前接口都是public无需访问权限检查,后添加了接口的静态私有方法和模块化约束需要进行访问权限检查
- 访问权限(解析得到直接引用后都需要进行权限检查),
- C、F、M是public且与D一个模块 ll C、F、M是public但与D不是一个模块但C、F、M的模块允许被D访问 ll C、F、M不是public但是与D在一个包内
- 类或接口C
- 初始化
- 定义
- 初始化阶段就是执行Java编译器自动生成的类构造器<clinit>()方法的过程
- 细节
- 除了加载阶段根据全限定名获取二进制字节流阶段可以通过自定义类加载器由开发人员控制外,其他阶段(加载剩余部分、验证、准备、解析、卸载)都由Java虚拟机主导控制,初始化阶段Java虚拟机才开始执行类中编写的Java代码,开始由应用程序主导控制
- 初始化时机(有且仅有6个主动引用类的时机需要初始化)
- 初始化阶段的初始化是按照开发人员想要的方向对字段进行初始化,与准备中初始化零值不同(常量准备阶段就按照开发人员想要方向初始化)
- 类构造器<clinit>()方法
- <clinit>()方法从何而来:
- 编译器自动收集类中类变量赋值动作和静态语句块中的语句构成<clinit>()方法
- 静态语句块只能访问在他之前定义的变量,在他之后定义的变量只能赋值不能访问(static{int a=0} a 只能赋值不能访问)
- 编译器自动收集类中类变量赋值动作和静态语句块中的语句构成<clinit>()方法
- 实例构造器与类构造器区别
- 实例构造器<init>()方法需要显示调用父类构造器,类构造器<clinit>()方法在执行以前其父类构造器<clinit>()方法就已经执行过了(初始化时机规定子类初始化前先保证父类已经初始化)(首个被执行的一定是java.lang.object的<clinit>()方法)(父类静态语句块要优先于子类变量赋值)
- 接口中若有变量赋值语句(接口不允许有静态语句块),则需要构建接口<clinit>()方法,但是其父接口的<clinit>()方法不会先执行(初始化时子接口初始化前对父接口是否初始化没有要求),仅当父接口中变量被使用时才初始化父接口;接口的实现类初始化时也无需先初始化接口
- <clinit>()方法不是必须的,若类中没有变量赋值语句和静态语句块、接口中没有变量赋值语句(接口本就没有静态语句块),Java编译器则无需构造<clinit>()方法
- 多线程同时初始化一个类,只有一个线程会执行该类的类加载器<clinit>()方法;同个类加载器下,一个类型只会被初始化一次
- <clinit>()方法从何而来:
- 定义
二、类加载器
- 功能
- 类加载器可以实现让应用程序自己决定如何获取所需要的类,让加载阶段中通过全限定名定位到二进制字节流的过程在Java虚拟机外部实现,由开发人员控制;
- 类加载器和类本身一起决定类的唯一性
- 双亲委派模型
- 分类
- Java虚拟机角度
- 启动类加载器(C++编写的,Java虚拟机的一部分)和其他类加载器
- 用户角度
- 启动类加载器(null)、扩展类加载器、应用程序类加载器、自定义类加载器
- Java虚拟机角度
- 概念
- 除了启动类加载器以外,其余类加载器都有其父类加载器,子类加载器通过组合父类加载器复用父类加载器代码
- 类加载器收到类加载请求时,先将该请求委派给其父类加载器,层层递进,若所有上层父类加载器都无法完成该类加载请求,子类加载器才自己完成加载
- 意义
- 类与类加载器一起具有了带有优先级的层次关系,越基本的类由越上层的加载器加载(如java.lang.Object无论出现在那个类加载器中最终都由启动类加载器加载),保证任意地方出现的基本的类都由统一的类加载器加载确保其唯一性(基础类型一致性)
- 破坏双亲委派模型
- 用户自定义类加载器代码在findClass()中编写,Java虚拟机自定义的类加载器代码在loadClass()中编写
- 基础类型调用用户代码(出现基础类型时还没执行到用户代码,基础类型不认识用户代码),引入线程上下文类加载器完成父类加载器请求子类加载器完成类加载任务
- 追求程序动态性,要求热部署时,为每个程序模块都实现一个自己的类加载器,程序模块需要更换时,连同类加载器一起更换,双亲委派模型由树状结构变成网状结构(中间是平级类加载器)
- 分类