这篇文章主要内容来自"深入java虚拟机",刚毕业那会太急功近利,这块离业务代码太远就没细看。这次花点时间整理一下,加深对字节码的认识。
类文件结构
“一次编写,到处运行”表达的是“与平台无关”。如何做到“与平台无关”?Sun公司以及其他虚拟机提供商发布了许多可以运行在各种平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码(class文件)。
Java虚拟机的野心远不止做到“与平台无关”,Java虚拟机还做到“语言无关性”。Java虚拟机支持其他语言的代码通过对应语言的编译器,编译成字节码,从而运行在Java虚拟机之上。
Class文件是一组以8位字节为基础单元的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数 和 表。
表1:Class文件格式定义
名称 |
类型 |
数量 |
描述 |
magic |
u4 |
1 |
class文件的识别码,固定,为“0xCAFFBABE” |
minor_version |
u2 |
1 |
次版本号,JDK1.7_u91,其中91就是次版本号 |
major_version |
u2 |
1 |
主版本号,JDK1.7对应值为51,JDK1.8对应值为52,依次类推。虚拟机拒绝执行超过其版本号的Class文件 |
constant_pool_count |
u2 |
1 |
常量池中常量的数量,从1开始计数 |
constant_pool |
cp_info |
constant_pool_count |
占用Class文件空间最大的数据项。常量池中存放两大常量:字面量(java中的常量,如文本字符串、声明为final的常量值等)和符号引用(类以及接口的全限定名、字段的名称和描述符、方法的名称和描述符)。cp_info有14种类型结构,通过开头的u1类型的标志位来表示,每一种类型的具体格式见 表2:常量池结构表 |
access_flags |
u2 |
1 |
访问标识,用于识别class还是接口、是否定义为public、是否定义为abstract、是否被声明为final等,u2总共16位,每一位可以代表一类信息是否。具体就不阐述了 |
this_class |
u2 |
1 |
类索引,一个指向常量类型为CONSTANT_UTF8_info的全限定名字符串 |
super_class |
u2 |
1 |
父类索引,一个指向常量类型为CONSTANT_UTF8_info的全限定名字符串 |
interfaces_count |
u2 |
1 |
继承接口数量 |
interfaces |
u2 |
interfaces_count |
接口索引,一个指向常量类型为CONSTANT_UTF8_info的全限定名字符串 |
fields_count |
u2 |
1 |
字段数量,包括类级变量以及实例级数量,但不包括方法内部的局部变量 |
fields |
field_info |
fields_count |
字段表结构包括5个类型:access_flag(和类的access_flag类似)、name_index(字段的简单名称,指向常量池索引)、descriptor_index(方法的描述符,描述字段的数据类型、方法的参数列表和返回值,对于数据类型,每一个维度使用一个前置的“[”字符描述,基础类型使用第一个字符大写,对象类型使用L+类全限定符,方法int indexOf(char[] source, Object target)的描述符常量为([CLjava/lang/Object)I )、attributes_count(属性的数量)、attribute_info(和类的attribute_info一样) |
methods_count |
u2 |
1 |
方法的数量 |
methods |
method_info |
methods_count |
方法的表结构和字段的描述是完全一致的,方法里的java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中的一个名为“Code”的属性里面。属性的描述见下行 |
attributes_count |
u2 |
1 |
属性的数量 |
attributes |
attribute_info |
arrtributes_count |
在Class文件、字段表、方法表都可以携带自己的属性表集合。目前虚拟机能识别的属性类型有21中,见 表3:虚拟机规范预定义的属性。属性表结构包含attribute_name_index(属性类型名称,指向常量池的索引)、attribute_length(属性长度,为属性表总长度-6)、info(属性信息,各属性类型自定义的属性结构) |
表2:常量池结构表
类型 |
标志 |
描述 |
CONSTANT_Utf8_info |
1 |
Utf-8 编码的字符串。1个u1类型的tag,值为1,一个u2类型表示字符长度的length;length个u1类型的bytes。 |
CONSTANT_Integer_info |
3 |
整形字面量。1个u1类型的tag,值为3;1个u4类型的int值 |
CONSTANT_Float_info |
4 |
浮点型字面量。1个u1类型的tag,值为4;1个u4类型的float值 |
CONSTANT_Long_info |
5 |
长整型字面量。1个u1类型的tag,值为5;1个u8类型的long值 |
CONSTANT_Double_info |
6 |
双精度浮点型字面量。1个u1类型的tag,值为6;1个u8类型的double值 |
CONSTANT_Class_info |
7 |
类或接口的符号引用。一个u1类型的tag,值为7;1个u2类型的name_index,指向CONSTANT_Utf8_info的常量 |
CONSTANT_String_info |
8 |
字符串类型字面量。一个u1类型的tag,值为8;1个u2类型的索引,指向CONSTANT_Utf8_info的常量 |
CONSTANT_Fieldref_info |
9 |
字段的符号引用。一个u1类型的tag,值为9;1个u2类型的索引,指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项;1个u2类型的索引,指向字段描述符CONSTANT_NameAndType的索引项 |
CONSTANT_Methodref_info |
10 |
类中方法的符号引用。一个u1类型的tag,值为10;1个u2类型的索引,指向声明方法的类描述符CONSTANT_Class_info的索引项;1个u2类型的索引,指向名称及类型描述符CONSTANT_NameAndType的索引项 |
CONSTANT_InterfaceMethodref_info |
11 |
接口中方法的符号引用。一个u1类型的tag,值为11;1个u2类型的索引,指向声明方法的接口描述符CONSTANT_Class_info的索引项;1个u2类型的索引,指向名称及类型描述符 |
CONSTANT_NameAndType_info |
12 |
字段或方法的部分符号引用。一个u1类型的tag,值为12;1个u2类型的索引,指向该字段或方法名称常量项的索引;1个u2类型的索引,指向该字段或方法描述符常量项的索引 |
CONSTANT_MethodHandle_info |
15 |
表示方法句柄。一个u1类型的tag,值为15;1个u1类型区间为1~9的值,表示方法句柄类型(方法句柄类型的值表示方法句柄的字节码行为);1个u2类型的索引,指向常量池 |
CONSTANT_MethodType_info |
16 |
标识方法类型。一个u1类型的tag,值为16;一个u2类型的描述符索引,指向CONSTANT_Utf8_info类型的索引项 |
CONSTANT_InvokeDynamic_info |
18 |
表示一个动态方法调用点。一个u1类型的tag,值为18;一个u2类型的索引,指向当前Class文件中引导方法表的bootstrap_methods[]数据;1个u2类型的索引,指向常量池中类型为CONSTANT_NameAndType_info的索引项 |
表3:虚拟机规范预定义的属性
属性名称 |
使用位置 |
含义 |
Code |
方法表 |
Java代码编译成的字节码指令。Code属性的结构 表4:Code属性表的结构 |
ConstantValue |
字段表 |
final关键字定义的常量值。属性结构为1个u2类型指向常量池的变量名索引;1个u4类型,固定值为2;1个指向常量池的属性变量值索引。 |
Deprecated |
类、方法表、字段表 |
被声明为deprecated的方法和字段。1个u2类型的执行常量池的名称索引。1个u4类型,值固定为0的数据值。 |
Exception |
方法表 |
方法抛出的异常,列举出方法中可能抛出的受检查异常。包含1个u2类型表示可能抛出异常的种类number_of_exception,和number_of_exception个指向常量池中CONSTANT_CLASS_info型常量的索引 |
EnclosingMethod |
类文件 |
仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClasses |
类文件 |
内部类列表,用于记录内部类与宿主类之间的关联。 |
LineNumberTable |
Code属性 |
Java源码的行号与字节码指令的对应关系 |
LocalVariableTable |
Code属性 |
方法的局部变量描述,描述栈帧中局部变量表中局部变量表中的变量与java源码中定义的变量之间的关系。包含一个u2类型代表本地变量表长度的值local_variable_table_length,和local_variable_table_length个代表栈帧与源码中局部变量关联的local_variable_info结构(1个u2类型代表局部变量生命周期开始的字节码偏移量start_pc,1个u2类型代表局部变量覆盖范围的字节码长度,1个u2类型指向CONSTANT_Utf8_info索引代表变量名称和1个u2类型指向CONSTANT_Utf8_info索引代表变量描述符,1个u2类型代表局部变量在栈帧局部变量表中slot的位置 ) |
StackMapTable |
Code属性 |
JDK1.6中新增的属性,供新的类型检查验证器检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
Signature |
类、方法表、字段表 |
JDK1.5中新增的属性,这个属性用于支持泛型情况下的方法签名,在java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则Signarure属性会为它记录泛型签名信息。 |
SourceFile |
类文件 |
记录源文件名称。一般情况下,类名和文件名相同 |
SourceDebugExtension |
类文件 |
JDK1.6中新增的属性,这个属性用于存储额外的调试信息 |
Synthetic |
类、方法表、字段表 |
标识方法或字典为编译器自动生成的。1个u2类型的执行常量池的名称索引。1个u4类型,值固定为0的数据。 |
LocalVariableTypeTable |
类 |
JDK1.5新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations |
类、方法表、字段表 |
JDK1.5中新增的属性,为动态注解提供支持。这个属性用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的 |
RuntimeInvisibleAnnotations |
类、方法表、字段表 |
与RuntimeVisibleAnnotations相反,指明哪些注解运行时不可见 |
RuntimeVisibleParameterAnnotations |
方法表 |
和RuntimeVisibleAnnotations作用类似,只不过作用对象为方法参数 |
RuntimeInvisibleParameterAnnotations |
方法表 |
和RuntimeInvisibleAnnotations作用类似,只不过作用对象为方法参数 |
AnnotationDefault |
方法表 |
JDK1.5新增属性,用于记录注解元素的默认值 |
BootstrapMethods |
类文件 |
JDK1.7新增属性,用于保存invokedynamic指令所引用的引导方法限定符 |
表4:Code属性表的结构
类型 |
名称 |
数量 |
描述 |
u2 |
attribute_name_index |
1 |
一个指向CONSTANT_Utf8_info型常量的索引,固定值为“Code” |
u4 |
attribute_length |
1 |
属性值的长度,整个属性表长度减去6字节 |
u2 |
max_stack |
1 |
代表操作数栈深度的最大值 |
u2 |
max_locals |
1 |
代表局部变量表所需的存储空间,单位slot(slot是虚拟机为局部变量分配内存所使用的最小单位,4字节) |
u4 |
code_length |
1 |
字节码指令的个数 |
u1 |
code |
code_length |
用于存储字节码指令的一系列字节流,每个字节码指令是一个u1类型的单字节 |
u2 |
exception_table_length |
1 |
异常表长度 |
exception_info |
exception_table |
exception_table_length |
异常表包含4个字段,依次是:u2类型的start_pc、end_pc、handler_pc、catch_type。如果当字节码在start_pc行到end_pc之间出现类型为catch_type(指向CONSTANT_Class_info的索引)或者其子类型的异常,则转到handler_pc行进行处理 |
u2 |
attributes_count |
1 |
属性数量 |
attribute_info |
attributes |
attributes_count |
属性表,同上 |
class文件是按照上面约定的格式无分割组成的二进制文件。通过认为将二进制转化成ascii码,然后切分成人为可理解的描述行成本太大,我们只需要理解字节码的结构原理,Oracle公司已经帮我们准备好一个专门用于分析Class文件字节码的工具javap。通过使用javap工具的-verbose参数输出人为可以理解的文本描述。
字节码指令
java虚拟机的指令由一个字节长度u1、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。java虚拟机解释器执行模型的伪代码如下:
do{
自动计算PC寄存器的值加1;
根据PC寄存器的指示位置,从字节码流中取出操作码;
if(字节码存在操作数) 从字节码流中取出操作数;
执行操作码所定义的操作;
} while(字节码流长度>0)
对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表名专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。下表列举了Java虚拟机所支持的与数据类型相关的字节码指令,通过使用数据类型列所代表的特殊字符替换opcode列中的指令模板中的T,得到一个具体的字节码指令。
opcode |
byte |
short |
int |
long |
float |
double |
char |
reference |
描述 |
Tipush |
bipush |
sipush |
|
|
|
|
|
|
将一个常量加载到操作数栈 |
Tconst |
|
|
iconst |
lconst |
fconst |
dconst |
|
aconst |
将一个常量加载到操作数栈 |
Tload |
|
|
iload |
lload |
fload |
dload |
|
aload |
将一个局部变量加载到操作栈 |
Tstore |
|
|
istore |
lstore |
fstore |
dstore |
|
astore |
将一个操作数从操作数栈存储到局部变量表 |
Tinc |
|
|
iinc |
|
|
|
|
|
局部变量自增指令 |
Taload |
baload |
saload |
iaload |
laload |
faload |
daload |
caload |
aaload |
根据栈里内容来把name数组的第一项的值推至栈顶 |
Tastore |
bastore |
sastore |
iastore |
lastore |
fastore |
dastore |
castore |
aastore |
将栈顶值存入指定数组的指定索引位置 |
Tadd |
|
|
iadd |
ladd |
fadd |
dadd |
|
|
加法指令 |
Tsub |
|
|
isub |
lsub |
fsub |
dsub |
|
|
减法指令 |
Tmul |
|
|
imul |
lmul |
fmul |
dmul |
|
|
乘法指令 |
Tdiv |
|
|
idiv |
ldiv |
fdiv |
ddiv |
|
|
除法指令 |
Trem |
|
|
irem |
lrem |
frem |
drem |
|
|
求余指令 |
Tneg |
|
|
ineg |
lneg |
fneg |
dneg |
|
|
取反指令 |
Tshl |
|
|
ishl |
lshl |
|
|
|
|
位移指令 |
Tshr |
|
|
ishr |
lshr |
|
|
|
|
位移指令 |
Tushr |
|
|
iushr |
lushr |
|
|
|
|
位移指令 |
Tand |
|
|
iand |
land |
|
|
|
|
按位与指令 |
Tor |
|
|
ior |
lor |
|
|
|
|
按位或指令 |
Txor |
|
|
ixor |
lxor |
|
|
|
|
按位异或指令 |
i2T |
i2b |
i2s |
|
i2l |
i2f |
i2d |
|
|
类型转换指令 |
l2T |
|
|
l2i |
|
l2f |
l2d |
|
|
类型转换指令 |
f2T |
|
|
f2i |
f2l |
|
f2d |
|
|
类型转换指令 |
d2T |
|
|
d2i |
d2l |
d2f |
|
|
|
类型转换指令 |
Tcmp |
|
|
|
lcmp |
|
|
|
|
比较指令 |
Tcmpl |
|
|
|
|
fcmpl |
dcmpl |
|
|
比较指令 |
Tcmpg |
|
|
|
|
fcmpg |
dcmpg |
|
|
比较指令 |
if_TcmpOP |
|
|
if_icmpOP |
|
|
|
|
if_acmpOP |
条件分支,其中OP可以是eq、ne、gt、null等 |
Treturn |
|
|
ireturn |
lreturn |
freturn |
dreturn |
|
areturn |
返回指令 |
其他指令说明
指令 |
说明 |
pop、pop2 |
将操作数栈的栈顶一个或两个元素出栈 |
dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2 |
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶 |
swap |
将栈最顶端两个数值互换 |
goto、goto_w、jsr、jsr_w、ret |
无条件转移指令 |
tableswitch、lookupswitch |
复合条件分支转移指令 |
invokevirtual |
调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派) |
invokeinterface |
调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用 |
invokestatic |
调用类的静态方法 |
invokedynamic |
运行时动态解析出调用点限定符所引用的方法,并执行该方法 |
monitorenter、monitorexit |
synchronized关键字修饰语句块时对应的同步指令 |