虚拟机类加载机制
1,类文件结构
java中.java程序经过编译生成.class文件,.class文件由(不同平台windows,Linux,macOs)虚拟机加载,
生成机器指行的二进制机器码
1.1,Class类文件结构
- Class文件是一组以8个字节为基础单位的二进制流,各个数据项严格的按照顺序紧凑的排列在文件中。
- Class文件只有两种数据类型,”无符号数“和”表“
无符号数:u1,u2,u4,u8,表示1字节,2字节,4字节,8字节。
表由多个无符号数或者其他表作为数据项构成的复合数据类型。以”_info“结尾。
1.1.1,magic:魔数:前四个字节:0xCAFEBABE 确定这个文件是否为一个能被虚拟机接受得Class文件。
1.1.2, minor_version: 次版本号
major_version: 主版本号
1.1.3,constant_pool_count: 常量池的容量,从1开始计数,第0位用于特殊用处,用于指向常量池的索引,担又不引用任何一个常量池项目
如22,表示1~21个。
constant_pool: 常量池中主要存放,字面量(文本字符串,final常量值)和符号引用()。
- 常量池中每一项都是一个表。
- Class文件不会保存各个方法,字段的最终在内存中的布局信息,这些字段,方法的符号引用不经过虚拟机在
运行期间转换的话是无法得到真正的内存入口地址,
1.1.4,access_flags:访问标识,是类/接口,public?abstract
1.1.5, this_class: 类索引
super_class:父类索引
interfaces:接口索引集合
Class文件由这三项确认继承关系。
1.1.6,fields_count:字段表大小
fields :字段表,用于存放接口或类中存放的遍量。【字段修饰符,作用域(public,pro,pri),static?,final?,volatile?】
1.1.7,方法表:
methods_count:
methods: 储存方法的,访问标识,名称索引,描述符索引,属性表集合
1.1.8,sttribute_info:属性表集合:Code属性,java程序方法体里的代码经过javac编译器处理后,最终变为字节码指令存储在Code属性内。
2,类加载机制:
- 类加载:虚拟机把Class文件中数据,加载到内存,并对数据进行校验,转换,解析和初始化。最终形成可以被虚拟机直接使用的类型。
- 类从加载到卸载将经历:加载,验证,准备,解析,初始化,使用,卸载。
2.1,有且只有6种情况必须立即对类进行初始化,【加载,验证】
①遇到new,getstatic,putstatic,invokestatic,这四条字节码指令必须立即对类进行“初始化”。
- new实例化对象
- 读取/设置静态字段(被final修饰,已在编译器把字段放入常量池的静态字段除外)
- 调用一个类型的静态方法
②使用Java.lang.reflect包的方法对类型进行反射调用的时候,若类未初始化,则需要初始化。
③当初始化类时,若父类未初始化,需先初始化父类。
④虚拟机启动,用户需指定一个要执行的主类,(包含Main()方法的那个类)虚拟机会初始化这个类。
⑤动态语言支持。。。。
⑥默认方法,(default修饰的接口方法)
2.2,不需要初始化场景
①通过子类类名,引用父类静态字段,子类不需要初始化(未引用子类对象)
②通过类数组来引用类,不会触发类的初始化(只开辟空间,未引用类)classA[] a=new classA[10]
③通过类名访问常量,不触发(常量在编译阶段会存入,调用类的常量池,本质上没有直接引用定义常量的类)
2.3,类加载过程:
2.3.1,加载:
- 通过一个类的全限定名来获取定义此类的二进制字节流【Class文件】【可以从ZIP文件、网络中、其他文件、数据库、加密文件、等获取】
- 将这个字节流所代表的静态储存结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的,java.long.Class对象,作为方法区这个类的各种数据的访问入口。
2.3.2,验证:
- 加载过程中,class文件-二进制流,不一定是由Java源码编译而来,又很多途径,所以要验证是否符号Java虚拟机规范。
- 验证阶段的工作量在虚拟机的类加载过程中占了相当大比重。
①文件格式验证:【词法分析】
- 基于二进制流验证【验证魔数,版本号,常量池是否有不支持的类型,等。。。】
- 通过验证,文件数据加入方法区,进行后续验证。
②元数据验证:【语义分析】
- 验证数据信息描述是否符合《Java语言规范》的要求。
如:父类是否继承final类,是否实现父类或接口中全部方法,等待。。。
③字节码验证:【语法分析】
- 最复杂,通过数据量分析和控制流分析,确定语义是合法的,符合逻辑的。
- 对class文件中Code属性校验分析。
保证:
1,操作数栈数据类型与指令代码序列都能配合工作
2,保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
3,类型转换是否有效,(可以把子类对象赋值给父类,不可以把父类对象赋值给子类)
④符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候。
- 通过字符串描述的全限定名是否能找到对应的类。
- 在指定类中是否存在符号方法的字段描述符及简单名称所描述的方法和字段。
- 符号引用的类,字段,方法,的可访问性(pri,pro,pub)是否可被当前类访问。
2.3.3,准备:
- 给类变量(static)分配内存并设置类变量初始化0值的阶段
public static int value=123,准备阶段,value=0
public static final int value=123,准备阶段,value=123(值写入Class文件中的)
2.3.4,解析:
- 虚拟机将常量池内符号替换为直接引用---方法在实际运行时内存布局中的入口地址,
①类或接口的解析:将类全限定名传给类加载器去加载这个类,加载过程中,由于元数据验证,字节码验证的需要,又可能触发其他相关类
的加载动作,
②字段解析:先解析这个字段的类/接口符号引用C,成功则进入查找搜索:
- 如果C本身包含,简单名称和字段描述符都与目标相匹配的字段,返回这个字段的直接引用。查找结束
- 否则,如果C实现了接口,则按照继承关系,从下往上递归搜索各个接口和它的父接口,如果包含字段
返回直接引用,查找结束
- 否则,如果C不是java.lang.Object的话,将按照继承关系从下往上,递归搜索其父类,如果在父类中包含了字段,
则返回这个字段的直接引用,查找结束
- 否则,查找失败,抛出java.lang.IlleagalAccessError异常。
③方法解析:先解析这个方法的类/接口符号引用C,成功则进入查找搜索:
- 分为类方法和接口方法,
- 与字段解析步骤一样。
2.3.5,初始化
- 在准备阶段,类变量已经初始化零值,而在初始化阶段,则会根据程序员写的代码,去初始化类变量和其他资源。
- 初始化阶段就是执行类构造器<client>()方法的过程。
①<client>()方法是又编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。
3,类加载器 实现“通过一个类的全限定名来获取描述该类的二进制字节流”动作的代码。
- 三种类加载器互相配合
启动类加载器-Bootstrap Class Loader 负责加载<JAVA_HOME>\lib目录中可识别的类库类,
扩展类加载器-Extension Class Loader 负责加载<JAVA_HOME>\lib\ext目录中,可识别的类库类。
应用程序类加载器-Application Class Loader 负责加载用户类路径(ClassPath)上的所有类库。
- 双亲委派模型:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,
每一层次的加载器都是如此,那么最终加载请求传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成
这次加载请求(它的搜索范围内没有找到所需的类)时,子加载器才会尝试自己完成加载。
双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的
类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载
自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object
类。
4,字节码执行引擎: 输入字节码二进制流,输出执行结构。
4.1,栈帧:
- 虚拟机以方法作为最基本的执行单元,“栈帧”是用于支撑虚拟机进行方法调用和方法执行背后的数据结构,是JVM中虚拟机栈的栈元素。
- 每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
- 栈帧储存了方法的:局部变量表,操作数栈,动态连接,方法返回地址。
①局部变量表:
- 存放方法参数和方法内部定义的局部变量。
- 容量以变量槽为最小单位,一个变量槽可以存放32位以内的数据。
- 变量槽复用:局部变量超出作用域,其变量槽可以被其他变量占用。
- 如果执行的是实例方法(没有被static修饰的方法),那局部变量表中第0位索引的变量槽默认用于传递方法所属对象实例的引用,
在方法中可以通过关键字“this”访问到这个隐含参数。
②操作数栈:
- 当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译时,编译器必须严格保证这一点。
③动态连接:每个栈帧都含一个执向运行时常量池中该栈帧所属方法的引用,字节码中的方法调用指令就以常量池里指向方法的符号引用
作为参数,【这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用---静态解析。另一部分将在每一次
运行期间都转化为直接引用----动态链接】
④方法返回地址:
- 当一个方法开始执行,只有两种方法退出:①执行引擎遇到任意一个方法返回的字节码指令。②另一种是在执行过程中遇到异常,
并且没有在方法体里妥善处理,只有在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。
- 无论怎么退出,都会回到最初调用方法的地方,退出时要保存一些信息,用来恢复主调方法的执行状态,一般,主调方法的PC计数器
的值,可以作为返回地址。
- 方法退出即当前栈帧出栈(虚拟机栈),恢复上层方法局部变量表和操作数栈。
4.2,方法调用: 唯一的任务是确定调用哪一个方法。一切方法调用在Class文件里面存储的都只是符合引用,而不是方法在实际运行时内存布局的
入口地址。
4.2.1,解析:---静态过程,在编译期间完全确定。
所有方法调用的目标方法在Class文件里都是一个常量池中的符合引用,在类加载解析阶段,会将一部分符合引用转化为直接引用,
前提:方法程序在真正运行前就有可确定的版本(无重载,重写),如:静态方法,私有方法,实例构造器,父类方法,final方法。
4.2.2,分派
- 静态分派---方法重载
①静态类型
class Human
class Man extends Human 编译阶段,a,b的类型为Human,并未进行负责
class Woman extends Human Human--静态类型,外观类型
Human a=new Man(); Man,Woman ----运行时类型,实际类型
Human b=new Woman()
②静态分派:所有依赖静态类型来决定执行版本的分派动作,称为静态分派。【虚拟机重载时通过参数的静态类型,
而不是实际类型作为判断依据】
- 动态分派:---方法重写
①invokevirtual指令:
- 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C.
- 如果在类型C中找到与常量中描述符的简单名称都相等的方法,则进行访问权限校验,通过,则返回方法的直接引用
- 否则,按照继承关系从下往上,依次对父类进行搜索验证,
- 最终没找到合适方法,则抛出java.lang.AbstractMethodError异常。
②因为invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以并不是把常量池中方法的符合引用
解析为直接用于就结束了,还会根据方法的实际接收者的实际类型,来选择方法。---重写本周。
重载与重写:
重载-编译时多态
重写-运行时多态
在java语言中,重载方法,处理函数名相同,还要有一个不同的特征签名:特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合
包括参数的类型,顺序,个数。
在虚拟机规范中(Class文件里),特征签名范围要广一些,包括返回值类型。
当直接调用方法时:
f();虚拟机不知道调用的哪一个;