文章目录
1 概述
代码编译的结果从机器码到字节码,是存储格式发展的一小步,却是编程语言发展的一大步。
无关性
无关性分为 平台无关性 、语言无关性1。
无关性的基石是 虚拟机 、* 字节码*
插入一张图说明一下:
Java语言的各种变量、关键字、元素符号的语义都是由多条字节码命令组合而成的,因此字节码命令提供的语义描述能力肯定比Java语言本身更加强大。
2 Class类文件的结构
一个class文件(磁盘上)唯一对应一个类或接口的定义信息,但是类和接口的定义信息不一定在class文件中(类或接口可以通过类加载器直接生成)。
class文件是一个8位(一个字节)作为基本单位的二进制流,各个数据项目之间没有空隙
class文件以一种类型C++结构体的伪结构来存在数据项,数据项有两种:
- 无符号数,是基本类型。可以用来表示数字、索引引用、数量值、UTF-8编码的字符串
- 表,由无符号数或者其他表作为数据项组成的复合结构
下面数class文件的格式:
名称 | 类型(所占字节) | 数量 |
---|---|---|
magic(魔数) | u4 | 1 |
minor_version(次版本号) | u2 | 1 |
major_version(主版本号) | u2 | 1 |
constant_pool_count(常量池数量) | u2 | 1 |
constant_pool(常量池) | cp_info | constant_pool_count-1 |
access_flags(类或接口访问标志) | u2 | 1 |
this_class(类索引) | u2 | 1 |
super_class(父类索引) | u2 | 1 |
interfaces_count(所实现的接口个数) | u2 | 1 |
interface(接口索引) | u2 | interfaces_count |
fields_count(成员个数) | u2 | 1 |
field(成员信息) | field_info | fields_count |
methods_count(方法个数) | u2 | 1 |
method(方法信息) | method_info | methods_count |
attributes_count(属性个数) | u2 | 1 |
attibute(属性信息) | attribute_info | attributs_count |
2.1 魔数(majic)与Class文件的版本(minor_version、major_version)
class文件开始的4个字节是魔数,唯一作用表示该class文件是否能被java虚拟机接受(其实就是0XCAFFBABE)。
紧接着的2个字节用于表示class文件的次版本号,接下来的2个字节表示class文件的主版本号。
2.2 常量池(constant_pool)
紧接着class版本号之后的是常量池入口,它是class文件中与其他项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一,同时他也是在class文件中第一个出现的表类型。由于常量池的个数不固定,所以常量池入处有一个u2类型的数据用来表示常量池的个数。
常量池两大类常量:
- 子面量。比较接近java语言的常量概念,如文本字符串、声明为final的常量值
- 符号引用。包含类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
分析Class文件字节码的工具javap
下面是常量池的项目类型:
下面是CONSTANT_Class_info的结构:
下面是CONSTANT_UTF8_info结构:
下面是常量池中14中常量项的结构总表:
2.3 访问标志
在常量池结束后紧接着的是两个字节是访问标志,用于识别类或接口的访问信息。access_flags一共16位,但只用到了8位,没用到的一律为0。
2.4 类索引、父类索引、接口索引集合
类索引、父类索引都是u2类型的数据,各自指向一个CONSTANT_Class_info。
对于接口索引集合的入口的第一项是一个u2类型的数据,用于表示实现接口的个数。紧接着后面的是相应接口的接口索引。
2.5 字段表集合
字段表包含类中定义的实例成员、类成员,但不包含方法中定义的局部变量。
下面是字段访问标志:
紧跟着字段访问标识的是name_index和descriptor_index,他们都是对常量池的索引。描述符标识字符含义如下:
对于数组类型,每一维使用一个前置“[”表示。
用描述符描述方法时,按照先参数列表后返回类型的顺序描述,参数列表按照参数的顺序放在“()”中,如 void doSomethin()的描述符为:()V
2.6 方法表集合
方法表集合的结构如下:
方法的访问标识如下:
方法里面的代码都放在了方法表的属性集合里面的Code属性中了。
2.7 属性表集合
在Class文件、方法表、字段表中都可以携带自己的属性表集合,用于描述某些场景专有的信息。
下面是可能的属性:
3 字节码指令简介
java虚拟机指令定义:
指令由一个字节(操作码)和其后的0个或多个操作数组成。由于java虚拟机采用的是面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。
字节码指令集优缺点:
缺点 | 优点 |
---|---|
用一个字节来表示操作码,那么操作码的个数不能超过256个 | 放弃了操作数对齐,省略了很多填充和间隔符号,节省了空间 |
因为放弃了代码对齐,所以虚拟机处理超过一个字节长度的数据的时候,需要进行数据重构 | 小数据流,高传输效率 |
执行模型:
3.1 字节码与数据类型
opcode的T 被替换为相应的类型。由于操作码是1个字节,所以为每个类型都支持相应类型的指令,那一个字节的大小是可能不够的
3.2 加载和存储指令
作用:
加载和存储指令用于在栈帧中局部变量表和操作数栈之间来回传递数据
存在的格式:
作用 | 对应指令 |
---|---|
将一个局部变量加载到操作数栈中 | iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aaload、aaload_<n> |
将操作数栈中数存储到局部变量表中 | istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n> |
将一个常量加载到操作数栈中 | bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconstr_<d> |
扩充局部变量表的访问索引的指令 | wide |
尖括号情况:
以尖括号结尾的助记符表示一组指令。如iload_<n>表示iload_0,iload_1,iload_2,iload_3这几条指令。
3.3 运算指令
定义:
运算和算术指令用于将两个操作数栈上的值进行某种运算,将运算结果存入栈顶。大体上的运算和算术指令可分为对浮点数的运算指令、对整数的运算指令。
存在的指令:
描述 | 指令 |
---|---|
加法指令 | iadd、ladd、fadd、dadd |
减法指令 | isub、lsub、fsub、dsub |
乘法指令 | imul、lmul、fmul、dmul |
除法指令 | idiv、ldiv、fdiv、ddiv |
求余指令 | irem、lrem、frem、drem |
取反指令 | ineg、lneg、fneg、dneg |
位移指令 | ishl、ishr、lshl、lshr、fshl、fshr、dshl、dshr |
按位或指令 | ior、lor |
按位与指令 | iadn、land |
按位异或指令 | ixor、lxor |
局部变量自增指令 | iinc |
比较指令 | dcmpg、dcmpl、fcmpg、fcmpl、lcmp |
IEEE 754中两种舍入模式:
- 向最接近数舍入模式(默认):在进行浮点运算时,所有的运行结果必须舍入到适当精度。非精确的结果必须舍入为可表示的最接近的精确值。当存在两种舍入时,将优先选择最低有效位为0的。
- 向0舍入模式:在把浮点数转换为整数时采用。例如int b=(int)2.6,结果b=2;
3.4 类型转换指令
定义:
将两个不同数值类型进行转换
宽化类型转换,从小范围到大范围进行转换(这是直接的转换,无需显示的转换指令)。下面是java虚拟机支持的宽化类型转换:
窄化类型转换,必须显示使用窄化命令(i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2f)。
将long,int窄化为 整数类型T 时可能发生下面问题:
- 精度丢失(long到int)
- 产生不同的正负号(将负数long或int转换为char,byte等类型时)
运行结果:
将浮点类型窄化为 整数类型T 时遵循的规则:
3.5 对象创建和访问指令
虽然类实例和数组都是对象,但是虚拟机对他们的创建和操作使用了不同的指令。如下:
3.6 操作数栈管理指令
操作数栈管理指令就是直接操作操作数栈的指令。如下:
3.7 控制转移指令
控制转移指令就是程序再有条件或无条件的情况下执行指定位置的指令。控制转移指令如下:
byte,boolean,short,char的比较都是转换为int的比较,然而long,double,float的比较是先通过比较运算指令返回一个数到操作数栈,然后再通过int类型的比较指令进行比较。
3.8 方法调用和返回指令
下面仅列出了5条方法调用的指令:
方法返回指令:ireturn(用于char,boolean,byte,short返回)、dreturn,freturn,lreturn,areturn,还有一条返回类型为void的指令。
3.9 异步处理指令
java中显示用throw语句抛出异常是用athrow实现的。然而用catch进行捕获异常不是用指令实现的(以前是用jsr,ret实现的),而是用异常表实现的。
3.10 同步指令
虚拟机中方法级和方法内部一段指令序列的同步都是通过是用管程(Monitor) 实现的。
方法级别的同步是隐式的,不用是用同步指令,它实现在方法的调用和返回中。
当在调用方法时,先访问方法表结构的ACC_SYNCHRONIZED访问标志得知方法是否为一个同步方法,若是,就先请求保持管程,当方法执行完后释放管程。当方法执行期间,执行线程持有管程,其他线程无法获取同一个管程。
方法内部一段指令序列的同步是使用monitorenter和monitorexit实现的。不管方法时怎样完成的(正常完成,抛出异常),每条monitorenter指令都要执行对应的monitorexit指令。
4 共有设计与私有实现
java虚拟机规范描述了java虚拟机应有的共同程序存储格式:Class文件格式以及字节码指令。
在满足虚拟机规范的条件下对虚拟机进行修改优化是可行的,只要优化后的Class文件依然可以被正确读取,并且包含在其中的语义能够得到完整的保持。
虚拟机的实现有以下两种方式:
5 Class文件结构的发展
Class文件结构一直处于比较稳定的状态,Class文件的主体结构、字节码指令的语义和数量几乎没有出现过变动。所有对Class文件格式的的改进,都集中向访问标志、属性表这些在设计上就可扩展的数据结构中添加内容。
语言无关性是指java虚拟机不仅仅可以运行java程序,还可以运行JRuby、Jython等语言程序。 ↩︎