提纲
无关性的基石
字节码构成了平台无关性的基石
虚拟机和字节码存储形式是语言无关性的基础
Class文件的结构
无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表一个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用描述数字、索引应用、数量值或者按照UTF-8编码构成字符串值。
表:是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于买哦书有层次关系的复合结构的数据,整个Class文件本质上就是一张表。
魔数与class文件的版本
cafe babe 0000 0033 魔数 版本号 |
魔数的作用:确定这个文件是否作为一个能被虚拟机接受的Class文件。在文件存储表示中广泛使用。Class文件的魔数值为0xcafebabe
class的版本号:第5和第6个字节是次版本号,第7和第8个字节为主版本号。JDK1.1支持版本号为45.0(0000002D)~45.65535(FFFF 002D)的Class文件。JDK1.3支持版本号为45.0(0000 002D)~46.65535(FFFF002E)的class文件。JDK1.7的最大主版本号为51.0,JDK1.8的最大主版本号为52.0(下图为网上引用)
常量池
0016 常量池容量 | 07 0002 01 标志位 常量名 标志位 | 000f 000f 636c 617a 7a2f 5465… 标志位 length bytes | … … |
常量池的容量:容量计数是从1而不是0开始的,如常量池的值为0x0016,即十进制的22,这就代表常量池中有21项常量。索引值范围为1~21。若某些常量池的索引数据在特定情况下需要表达“不引用任何一个常量池项目”的含义时,就把索引值设为0表示。
Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集
合、字段表集合、方法集合等的容量计数都与一般习惯相同,都是从0开始的。
常量池主要存放两大类常量:字面量和符号引用
字面量:接近java层面的常量概念,如文本字符串、声明为final的常量值等。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。包括:
1.类和接口的权限定名;
2.字段的名称和描述符;
3.方法的名称和描述符。
Class文件中方法、字段等都需要引用COSTANT_Utf8_info型常量来描述名称,所以COSTANT_Utf8_info的最大长度就是java中方法、字段名的最大长度。而length最大值为65535.所以Java中如果定义了超过64KB字符的变量或方法名,将无法编译。(下图为网上引用)
访问标志
用于识别一些类或者接口层次的访问信息,包括:这个class是类还是接口;是否定义public类型;是否定义为abstract类型;如果是类是否被声明为final等。(图为网上引用)
类索引、父类索引与接口索引集合
0001 类索引 | 0003 父类索引 | 0000 接口数量 | 。。。 接口索引 |
类索引用于确定这个类的全限定名;
父类索引用于确定这个类的父类的全限定名;
接口索引集合描述这个类实现了那些接口,这些被实现的接口将按implements(接口实现接口为extends)语句后的顺序从左到右排列在接口索引集合中。包括接口计数器和接口索引表。
字段表集合
0001 容量计数器 | 0002 0005 0006 0000 。。。。 访问标志 字段名称 字段描述符 属性表集合 | 。。。。。。 |
(网上引用)
字段的简单名称和权限定名的区别:权限定名是把类中全名中的“,”替换成了“/”而已。而简单名称是指没有类型核参数修饰的方法或者字段名称。
(网上引用)
描述符:用来描述字段的数据类型、方法的参数列表和返回值。
标识字符 | 含义 | 标识字符 | 含义 |
B | byte | J | long |
C | char | S | short |
D | double | Z | boolean |
F | float | V | void |
I | int | L | 对象类型 |
根据描述符规则,基本数据类型以及无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的权限定名来表示。
对于数组类型,每一维度将使用一个前置的“[”字符来描述。如string[][]类型的数组表示为“[[Ljava/lang/String;”来表示。
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号()之内。如int a(double b)描述符为“(D)I”
方法表集合
0002 容量计数器 | 0001 0007 0008 0001 0009 访问标志 名称索引 描述符索引 属性表集合 | 。。。。 。。。。 |
方法表包括访问标志、名称索引、描述符索引、属性表集合等几项
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法表的访问标志如下:
(网上引用)
方法里的Java代码,经过编译成字节指令后,存放在方法属性表集合中的code属性里。
属性表集合
属性表集合是字段表和方法表中的一部分,属于嵌套的关系。
0001 0007 0008 0001 属性数 | 0009 0002 002f 0001 0001 0000 0005 2ab7 000a b1 Code 属性长度 max_stack max_locals codelength code | |||
0000 exception_length | 00 02 attributes | 00 0c 00 0000 06 00 01 00 00 00 03 LineNumbleTable属性长度 line_length start_pc line_number | ||
00 0d 00 0000 0c 00 01 00 00 00 05 00 0e 00 0f 00 00 LocalValueTable 属性长度 loc_length start_pc length name_index描述符 index | ||||
00 01 00 10 00 11 00 01 访问标志位 方法名 描述符 属性数 | 00 09 … … … …… code … … … …… |
在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
属性表的结构如下:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
方法体中的代码经过javac编译后最终变为字节码指令存储在Code属性内,code属性出现在方法表的属性集合中,接口或者抽象类中的方法就不存在Code属性。
属性名称 | 使用位置 | 含义 |
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量池 |
Deprecated | 类,方法,字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部便狼描述 |
StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 |
Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 |
Synthetic | 类,方法表,字段表 | 标志方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 |
RuntimeInvisibleAnnotations | 表,方法表,字段表 | 用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法 |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokeddynamic指令引用的引导方式限定符 |
字节码指令简介
java虚拟机操作码的长度为1个字节,所以字节码指令集的操作总数不超过256条。
字节码与数据类型
对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为那种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。
加载和存储指令
将一个局部变量加载到操作数栈:*load*;
将一个数值从操作数栈存储到局部变量表:*store*;
将一个常量加载到操作数栈:*push、ldc*、*const*;
扩充局部变量表的访问索引指令:wide
运算指令
加法指令:*add;
减法指令:*sub;
乘法指令:*mul;
触法指令:*div;
求余指令:*rem;
取反指令:*neg;
位移指令:*shl、*shr;
按位或指令:ior、lor;
按位与指令:iand、land;
按位异或指令:ixor、lxor;
局部变量自增指令:inc;
比较指令:*cmp*
类型转换指令
虚拟机直接支持以下数值类型的宽化类型转换:
int类型到long、float、double类型;
long类型到float、double类型;
float类型到double类型。
处理窄化类型转换时,需要用显示地使用转换指令来完成包括:i2b,i2c,i2s,l2i,f2i,f2l,d2i,d2l和d2f。
在一个浮点值窄化转换为整数类型T的时候,将遵循以下转换规则:
如果浮点值是NaN,那么转换结果就是int或long类型的0;
如果浮点值不是无穷大的画,浮点值使用IEEE754的向0舍入模式取整,获得整数值v,如果v在目标类型T的标志范围内,那转换结果就是v;
否则,将根据v的符号,转换为T所能表示的最大或者最小整数。
对象创建与访问指令
创建类的实例的指令:new;
创建数组的指令:*newarray;
访问类的字段和实例字段的指令:getfield、putfield、getstatic、putstatic;
把一个数组元素加载到操作数栈的指令:*aload;
将一个操作数栈的值存储到数组元素的指令:*astore;
取数组长度的指令:arraylength;
检查类实例类型的指令:instanceof、checkof;=。
操作数栈管理指令
将操作数栈的栈顶一个或两个元素出栈:pop、pop2;
复制栈顶一个或两个数值并将复制值压入栈顶:dup、dup2、dup_x1、dup_x1、dup2_x1、dup_x1、dup2_x1;
将栈最顶端的两个数值互换:swap;
控制转移指令
条件分支:if*;
复合条件分支:*switch;
无条件分支:goto*、jsr*、ret;
方法调用和返回指令
invokevirtual用于调用对象的实例方法,根据对象的实际类型分派。
invokeinterface用于调用接口方法,运行时会搜索一个实现了这个接口方法的对象;
invokespecial调用一些需要特别处理的实例方法,包括初始化方法、私有方法、父类方法;
invokestatic用于调用类方法;
invokedynamic用于调用动态解析出调用点限定符所引用的方法。
返回指令:*return。
异常处理指令
athrow显示抛出的异常操作。另外当除数为0时会在div指令中抛出ArithmeticException异常
同步指令
通过monitorenter和monitorexit指令支持synchronized关键字的语义。
共有设计与私有实现
在虚拟机规范的约束下对具体实现做出修改和优化也是完全可行的。虚拟机实现的方式有两种:
将输入的java虚拟机代码在加载或执行时翻译成另一种虚拟机的指令集;
将输入的java虚拟机代码在加载或执行时翻译成宿主主机cpu的本地指令集。