Java程序运行在Java虚拟机上,实现平台无关性。
其它语言的应用程序也可以运行在Java虚拟机上,实现语言无关性。
平台无关性和语言无关性的基础就是虚拟机和字节码(Class文件)。
Java语言中的各种变量、关键字和运算符号的定义最终都是由多条字节码命令组合而成,因此字节码命令提供的语义描述能力比Java语言本身更强大。
一. Class类文件结构
任何一个Class文件都对应唯一的一个类或接口定义,但类或接口不一定都得定义在文件里(类加载器可直接生成类或接口)。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有任何分隔符。当某个数据项需要占用8位字节以上的空间时,则按照高位在前的方式分隔成若干个8位字节进行存储。
Class文件中定义了两种数据类型:
无符号数
无符号数用来描述数字、索引引用、数量值或者按照UTF-8编码的字符串值,以u1、u2、u4、u8表示1个字节、2个字节、4个字节、8个字节的无符号数。表
表用于描述有层次关系的复合结构类型,由多个无符号数或其他表作为数据项构成的复合数据类型,习惯性以“_info”结尾。
1. Class文件格式
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
2. 说明
- 魔数(magic)
Class文件的前4个字节,用来确定这个文件是否为一个能被虚拟机接受的Class文件。
Java的Class文件魔数为0xCAFEBABE
。
- 次版本号(minor_version)、主版本号(major_version)
随JDK发行而不断增加。JDK能向下兼容以前版本的Class文件,但是不能运行之后版本的Class文件。
- 常量池(constant_pool)
常量池是Class文件中的资源仓库,是占用Class文件空间最大的数据项目之一。
常量池中常量的数量是不固定的,因此需要在常量池的入口处加入一个u2类型的数据来表示常量池中常量个数。
常量池中的第0项特意被空出来不存放常量,当要表达“不引用任何一个常量池项目”时,就可以将索引值置为0来表示了。因此常量池中存放的常量是从第1项开始的,所以常量池中常量个数为constant_pool_count-1。
常量池中存放的常量主要包括:
(1)字面量:文本字符串、声明为final的常量值等。
(2)符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
这些常量都是表,被分别定义为如下14中表结构:
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 方法句柄 |
CONSTANT_MethodType_info | 16 | 方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 动态方法调用点 |
这些表结构的第一位都是一个u1的标志位,根据此标志位值可以确定该常量的表结构是什么样的,然后才能正确解析。
javap -verbose ClassName
可以解析名为ClassName的class文件字节码内容。
- 访问标志(access_flags)
访问标志用于识别类/接口层次的访问信息,例如这个Class是类还是接口,是否定义为public,是否为abstract类型等。
- 类索引(this_class)、父类索引(super_class)、接口索引集合(interfaces)
Class文件中这三项数据用于确定这个类的继承关系。
类索引:确定这个类的全限定名;
父类索引:确定这个类的父类的全限定名;
索引集合:描述这个类实现了哪些接口;
查找全限定名的过程:
- 字段表集合(fields)
字段表用于描述接口或类中声明的变量,包括类级变量、实例级变量,但不包括方法内部声明的局部变量。
字段表集合中不会列出从超类或者父接口中继承而来的字段。
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
(1)access_flags用于描述字段作用域(public、private、protected)、实例变量还是类变量(static)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)等信息。
(2)name_index是对常量池的引用,常量池中存放实际的字段的简单名。
(3)descriptor_index也是对常量池的引用,常量池中存放实际的字段的描述符。
描述符:
标识字符 | 含义 |
---|---|
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型short |
Z | 基本类型boolean |
V | 特殊类型void |
L | 对象类型 |
[ | 数组 |
举例:
java.lang.String[][]
描述为[[Ljava/lang/String;
int[]
描述为[I
- 方法表集合(methods)
结构与字段表集合类似。
父类方法在子类中没有被重载,那么方法表集合中就没有来自父类的方法信息。
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
为什么Java中不能根据返回值不同来重载方法?
由方法表集合结构可知,Java的方法特征签名中只有方法名、参数顺序和参数类型,不包括返回值,所以不能用返回值对方法进行重载。
- 属性表集合(attributes)
在Class文件中,属性表集合用于描述某些场景专有的信息。
字段表和方法表的最后就是使用属性表来描述一些额外信息的。
属性表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
自定义 | info | 1 |
attribute_name_index是一个指向CONSTANT_Utf8_info型常量的索引,表示属性名。
attribute_length指示了属性值占用的位数。
info是属性值的结构,完全自定义。
举例:
(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 | attributes_count |
attribute_name_index:指向CONSTANT_Utf8_info型常量索引(值固定为“Code”)。
max_stack:操作数栈深度的最大值,该方法执行的任意时刻,操作数栈都不会超过这个深度,虚拟机运行时根据此值分配栈帧中的操作栈深度。
max_locals:局部变量表所需的存储空间。
code:存储Java源程序编译后生成的字节码指令。
exception_table:方法的显式异常处理表。
(2)Exceptions属性
Exceptions属性用于列举出方法中可能抛出的受查异常,即throws关键字后面列举的异常。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exceptions | 1 |
u2 | exception_index_table | number_of_exceptions |
二、字节码指令
字节码指令 = 操作码 + 操作数
操作码:一个字节长度的、代表某种特定含义的数字;
操作数:跟随在操作码后的零至多个参数。
操作码只有一个字节(0-255),所以最多只有256个操作码。
对于大部分与数据类型相关的字节码指令,操作码助记符都有特殊字符表示:
类型 | 助记符 |
---|---|
int | i |
long | l |
short | s |
byte | b |
char | c |
float | f |
double | d |
reference | a |
- 1. 加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。
(1)将一个局部变量加载到操作数栈:iload、iload_<n>、lload、lload_<n>等。
(2)将一个数值从操作数栈存储到局部变量表:istore、istore_<n>等。
(3)将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w等。
(4)扩充局部变量表的访问索引:wide。
注:iload_<n>是指在操作码后直接加上了操作数,省去了取操作数的动作。
例如:iload_0 等价于操作数为0时的iload指令。
- 2. 运算指令
运算或算术指令用于对操作数栈上的两个值进行某种特定运算,并把结果重新存入到操作数栈顶。
(1)加法指令:iadd、ladd、fadd、dadd。
(2)减法指令:isub、lsub、fsub、dsub。
(3)乘法指令:imul、lmul、fmul、dmul。
(4)除法指令:idiv、ldiv、fdiv、ddiv。
(5)求余指令:irem、lrem、frem、drem。
(6)取反指令:ineg、lneg、fneg、dneg。
(7)位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
(8)按位或指令:ior、lor。
(9)按位与指令:iand、land。
(10)按位异或指令:ixor、lxor。
(11)局部变量自增指令:iinc。
(12)比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
- 3. 类型转换指令
类型转换指令可以将两种不同的数值类型进行互相转换。
Java虚拟机直接支持宽化类型转换,无需显式的转换指令:
int --> long、float、double
long --> float、double
float --> double
对于窄化类型转换,需要显式使用转换指令:
i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f。
- 4. 对象创建和访问指令
(1)创建类实例指令:new。
(2)创建数组指令:newarray、anewarray、multianewarray。
(3)访问类字段和实例字段:getfield、putfield、getstatic、putstatic
(4)把一个数组元素加载到操作数栈的指令:baload、caload、saload等。
(5)将一个操作数栈的值存储到数组元素中:bastore、castore、sastore等。
(6)取数组长度的指令:arraylength。
(7)检查类实例类型的指令:instanceof、checkcast。
- 5. 操作数栈管理指令
(1)将操作数栈的栈顶一个或两个元素出栈:pop、pop2。
(2)复制栈顶一个或两个元素并将复制值重新压入栈顶:dup、dup2、dup_x1等。
(3)将栈最顶端的两个数值互换:swap。
- 6. 控制转移指令
控制转移指令可以让Java虚拟机有条件或无条件的去指定的指令位置处执行程序,而不是默认的在下一条指令位置处执行程序。
概念模型上来说,控制转移指令就是有条件或无条件的修改PC(Program Counter)寄存器的值。
(1)条件分支:ifeq、iflt、ifle、ifne等。
(2)复合条件分支:tableswitch、lookupswitch。
(3)无条件分支:goto、goto_w、jsr、jsr_w、ret。
- 7. 方法调用和返回指令
(1)invokevirtual:调用对象的实例方法,根据对象的实际类型进行分派。
(2)invokeinterface:调用接口方法,运行时搜索实现了这个接口方法的对象,找出合适的方法调用。
(3)invokespecial:调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
(4)invokestatic:调用类方法。
(5)invokedynamic:在运行时动态解析出调用点限定符所引用的方法。
- 8. 异常处理指令
Java程序中显示抛出异常的操作(throw)都是有athrow指令实现的。
- 9. 同步指令
Java虚拟机支持方法级的同步和方法内部一段指令序列的同步。
方法级同步无须通过字节码指令控制,虚拟机可以从方法表的ACC_SYNCHRONIZED访问标志得知该方法是否为同步方法,如果设置了ACC_SYNCHRONIZED标志,则执行线程要求先成功持有管程(Monitor),管程同时只能被一个线程持有,然后才能执行方法,方法完成(无论是正常完成还是非正常完成)后释放管程。
同步一段指令集需要使用monitorenter和monitorexit两条指令完成。编译器必须确保无论这个方法是正常结束还是异常结束,调用过的每条monitorenter指令必须执行对应的monitorexit指令。