1. 概述
计算机只能识别01代码,而不同系统对01代码的识别也不一样.运行在虚拟机上成为了一种重要的跨平台手段.
2. 无关性的基石
构成平台无关性的基石就是不同虚拟机和所有平台都统一使用的程序存储格式--字节码(ByteCode).
实现语言无关性的基础仍然是虚拟机和字节码存储格式,Java虚拟机只与Class文件这种特定的二进制文件格式所关联.
3. Class文件的结构
这里以JDK1.4时代的Java虚拟机中的定义为主线.
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在一起,中间没有任何分隔符.当遇到需要占用8字节以上空间的数据项时,会按照高位在前的方式分割成若干8位字节进行存储.
Java虚拟机,Class文件格式采用一种类似与C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:<u>无符号数</u>和<u>表</u>.
<u>无符号数属于基本的数据类型,以u1,u2,u4,u8来代表1个字节,2个字节,4个字节和8个字节的无符号数,无符号数可以用来描述数字,索引引用,数量质或者UTF-8编码的字符串值.</u>
<u>表是由多个无符号数或者其他表作为数据项构成的复合型数据类型.所有表都习惯性的以"_info"结尾.</u>
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,常使用一个前置的容量计数器和若干个连续的数据项的形式,这是这连续若干个数据项就被称为某一类型的集合
(1). 魔数与Class文件的版本
每个Class文件的<u>头4个字节成为魔数</u>,他的唯一作用就是确定这个文件是否是一个能被虚拟机接受的Class文件.
Class文件的魔值为0xCAFEBABE(咖啡宝贝....,可能后来logo也跟这个有关系).
<u>紧接着的4个字节存储的是Class文件的版本号</u>:第5和第6字节是次版本号,第7和第8字节是主版本号.
(2). 常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为<u>Class文件之中的资源仓库</u>.,它是Class文件结构中与其他项目关联最多的数据类型,也是<u>占用Class文件空间最大的数据项目之一</u>.
常量池的入口放置一项<u>u2类型</u>的数据代表<u>常量池容量计数值</u>.这个值从1开始计数.索引0表示不引用任何常量池项目.
常量池中重要放置两大类常量:<u>字面量</u>和<u>符号引用</u>.
(3). 访问标志
常量池结束后,紧接着的两个字节代表访问标志,用于表示一些类或者接口层次的访问信息,包括:这个Class是类还是接口,是否为public,是否为abstract类型,是否为final等等.
(4). 类索引,父类索引与借口索引集合
类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型数据的集合.
类索引用于确定类的全限定类名,父类索引用于确定这个类的父类的全限定类名.接口索引集合用来描述这个类实现了那些接口.
索引指的是常量池中类符号引用的位置,再从类符号引用找到类的全限定类名.
(5). 字段表集合
字段表用于描述接口或者类中声明的变量 (不包括方法中的局部变量).
每一个字段表的结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attribute_count | 1 |
attribute_info | attributes | attributes_count |
access_flags:字段修饰符,修饰这个字段的访问信息,功能与类的访问标志类似.
name_index:字段的简单名称,就是字段名(方法名的话去掉括号)
descriptor_index:字段和方法的描述符,描述字段的数据类型,方法的参数列表和返回值.
(6). 方法表集合
与字段表的结构相同.描述了方法的定义.
方法的内容结果编译器编译成字节码指令后,存放在方法属性表集合中一个名为"Code"的属性中.
(7). 属性表集合
属性表就是之前字段表和方法表结构中的attributes字段,Class文件,字段表,方法表都可以携带字节的属性表集合,用于描述某些场景专有的信息.
这部分的数据项目要求不严格,但虚拟机只能识别自己认识的属性.
1). Code属性
Java程序方法体中的代码经过Javac编译器处理,最终变成字节码指令存储在Code属性内.
Code属性表的结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attribute_count |
attribute_name_index:指向常量池中的索引,常量值固定为"Code".
attribute_length:属性值的长度,固定为整个属性表的长度减去6字节(attribute_name_index和attribute_length的u2+u4).
max_stack:操作数栈深度的最大值.
max_locals:代表局部变量所需的存储空间.单位是Slot,相当于32位.
code_length和code:用于存储编译后生成的字节码指令.code_length代表字节码长度,code用于存储字节码指令的一系列字节流.每个指令都是u1. code_length不能超过65535,也就是说方法的内容太长的话Javac编译器会拒绝编译.
如果把Java程序中的信息分为代码(Java代码)和<u>元数据(Metadata,包括类,字段,方法定义及其他信息)</u>.那么在整个Class文件中,Code属性用于描述代码,其他所有数据项目都用于描述元数据.
2). Exceptions属性
Exceptions属性的作用是列举出方法中可能抛出的受查异常,也就是throws关键字后面列出的异常.
3). LineNumberTable属性
用于描述Java源代码行号与字节码行号(偏移量)之间的对应关系.
4). LocalVariableTable属性
用于描述帧栈中局部变量表中的变量与Java源码中定义的变量之间的关系.
5). SourceFile属性
用于记录生成这个Class文件的源代码文件名称.
6). ConstantValue属性
作用是通知虚拟机自动为静态变量赋值.
7). InnerClasses属性
用于记录内部类与宿主类之间的关联.
8). Deprecated和Synthetic属性
Deprecated属性用于表示某个类,字段或者方法,已经被程序作者定位不在推荐使用.可以使用@deprecated注解进行设置.
Synthetic属性代表此字段或者方法不是Java源码直接产生的,而是由编译器自动添加的.
9). StackMapTable属性
目的在于替代以前比较消耗性能的基于数据流分析的类型推导验证器.
10). Signature属性
任何类,借口,初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,Signature属性会为它记录泛型签名信息.
11).BootstrapMethods属性
用于保存invokedynamic指令引用的引导方法限定符.
4. 字节码指令简介
虚拟机的指令由一个字节长度的,代表某种特定操作含义的数字,以及跟随其后的零个或者多个参数而构成.
Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数指令都不包含操作数,只有一个操作码.
(1). 字节码与数据类型
Java虚拟机的指令集中,大多的指令都包含其操作所对应的数据类型信息(同一个功能对于不同数据类型的指令都是不同的).
字节操作码按用途大致分为9类.
(2). 加载和储存指令
用于将数据在帧栈中的局部变量表和操作数栈之间来回传输.包括以下内容:
- 将一个局部变量加载到操作栈:xload,xload_<n>(x是数据类型)
- 将一个数值从操作数栈存储在局部变量表:xstore,xstore_<n>
- 将一个常量加载到操作数栈
- 扩充局部变量表的访问索引的指令:wide
(3). 运算指令
运算或算数指令用于堆两个操作数栈上的值进行某种特定运算,并把结果存入操作栈顶.
所有的算数指令如下:加法指令,减法指令,乘法指令,除法指令,求余指令,取反指令,位移指令,按位与指令,按位或指令,按位异或指令,局部变量自增指令,比较指令
(4). 类型转换指令
可以将两种不同的数值类型进行相互转换,一般用于用户代码中显式的类型转换操作.
Java虚拟机直接支持以下数值类型的宽化类型转换:
- int类型到long,float,double类型
- long类型到float,double类型
- float类型到double类型
相对的,窄化类型转换就必须显式的使用转换指令来完成,可能会导致不同正负号,数量级的情况,很可能会丢失数值的精度.讲一个浮点值窄化为整数类型T(int或者long)时,将遵循以下转换规则:
- 如果浮点值是NaN,转换结果就是0
- 如果浮点值不是无穷大,那么使用IEEE 754的向零舍入模式取证,得到整数v,如果v在转换目标类型的范围内,转换结果就是v
- 否则,根据v的符号,转换为T佐能表示的最大或最小正数.
(5). 对象创建与访问指令
- 创建类的实例的指令
- 创建数组的指令
- 访问类字段(static)和实例字段的指令
- 把一个数组元素加载到操作数栈的指令
- 将一个操作数栈的值存储到数组元素中的指令
- 取数组长度的指令
- 检查类实例类型的指令
(6). 操作数栈管理指令
- 将操作数栈栈顶的一个或者两个元素出栈
- 复制栈顶的一个或者两个数值并将复制值或者两个复制值压入栈顶
- 将栈最顶端的两个数值互换
(7). 控制转移指令
可以让Java虚拟机有条件或者无条件地从指定的位置指令而不是控制转移指令的下一条继续执行(goto)
控制转移指令如下:
- 条件分支
- 符合条件分支
- 无条件分支
(8). 方法调用和返回指令
- invokevirtual指令用于调用对象的实例方法
- invokeinterface指令用于调用接口方法
- invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法,私有方法和父类方法.
- invokestatic指令用于调用static方法
- invokedynameic指令用于调用运行时动态解析出调用点限定符所引用的方法.
(9). 异常处理指令
Java程序中显式的抛出异常的操作(throw语句)都由athrow指令来实现,许多运行时异常则会在其他Java虚拟机指令检测到异常状况时自动抛出.
(10). 同步指令
Java虚拟机支持方法级的同步和方法内部一段指令序列的同步,对应着Java代码中的同步方法和同步代码块.这两种同步都是通过Monitor(管程)支持的.
方法级的同步是隐式的,虚拟机通过方法表中的ACC_SYNCHRONIZED访问标志得到方法时候声明为同步方法.如果为同步方法,执行线程就会要求先成功持有管程,然后才能执行方法,方法执行完毕之后释放管程.(管程就像线程锁一样,限制线程不能进行轮换)
5. 公有设计和私有实现
在Java虚拟机中,公有设计指的是Java虚拟机规范:Java虚拟机应有的共同程序存储格式(Class文件格式以及字节码指令集).
而私有实现就是指虚拟机实现者可以通过在满足虚拟机规范的约束下对具体实现做出修改和优化,这种伸缩性可以使Java虚拟机获得更高性能,更低内存消耗或者更好的可移植性.
6. Class文件结构的发展
......