8753 2017-10-08 2020-06-25
前言:Java是一门平台无关性语言,在刚刚诞生之初提出过一个非常著名的口号:一次编写,到处运行。而这种平台无关性的依赖基础就是程序的存储格式——字节码(ByteCode)。
一、Class文件的结构
1、概述
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
2、伪结构
根据Java虚拟机的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪接结构只有两种数据类型:无符号数和表。其中
- 无符号数:属于基本数据类型,以u1、u2、u4、u8来代表1个字节、2个字节、4个字节和8个字节的无符号数(每个字节有8位),无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
- 表:由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据。
3、数据项
整个Class文件本质上就是一张表,由多个数据项,具体如下
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u4 | magic | 1 | 标识Class文件的魔数,为0xCAFEBABE |
u2 | minor_version | 1 | 次版本号 |
u2 | major_version | 1 | 主版本号 |
u2 | constant_pool_count | 1 | 常量池大小 |
cp_info | constant_pool | constant_pool_count - 1 | 计量数从1开始,0用于标识不指向任何常量池数据 |
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 | 字段、方法的属性表信息 |
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列的某一类型的数据为某一类型的集合。
二、常量池
1、概述
紧接着主次版本之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是Class文件中第一个出现的表类型数据项目。
2、常量
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符合引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
这与下文是相对应的。
3、空指向
由于常量池中常量的数量不是固定的,所以在常量池入口需要放置一项u2类型的数据,代表常量池容量记数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数从1开始而不是从0开始,这样设计的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何常量池项目”的含义,这种情况可以把索引值置为0来表示。Class文件结构中只有常量池的容量记数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的。
4、项目类型
Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存每个方法、字段的最终布局信息,因此这些字段、方法的符号引用不经过运行期转换的话,是无法得到真正的内存入口地址,也就无法直接被虚拟机使用。所以需要先分析常量池中常量的数据类型。
在常量池中,每个常量都是一个表,如下
类型 | 标志 | 描述 |
---|---|---|
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 | 表示一个动态方法调用点 |
这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag,取值见上表中的标志列),代表当前这个常量属于哪种常量类型。之所以说常量池是最繁琐的数据,是因为这14种常量类型各自均有自己的结构。例如
- CONSTANT_Class_info型常量的结构
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
这里的name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型的常量,此常量代表了这个类(或者接口)的全限定名。假设该值为0x0002,也即是指向了常量池中的第二项常量。
- CONSTANT_Utf8_info型常量的结构
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
length值说明了这个UTF-8编码的字符串长度是多少字节,它后面紧跟着的长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。 由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以length的最大值就是Java中方法、字段名的最大长度,即2的16次方 - 1=65535。
5、javap工具
通过上面的介绍,我们通过计算,可以获得常量池中的所有信息,只是过程有点痛苦。在JDK的bin目录下,Oracle公司已经为我们准备好了一个专门用于分析Class文件字节码的工具:javap。使用方法如下:
javap -verbose TestClass
三、访问标志
在常量池结束之后,紧接着两个字节代表访问标志(access_flag),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类,是否被声明为final等。这里知道概念即可,不做细究。
四、类信息
- 类信息包括:类索引、父类索引与接口索引集合。
- 类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引结合(interfaces)是一组u2类型的数据的集合,Class文件中由这个三项数据来确定这个类的继承关系。
- 类索引用于确定这个类的全限定名,父类索引用于这个类的父类的全限定名。由于Java不允许多重继承,所以父类索引只有一个,除了java.lang.Object类之外,所有Java类的父类索引都不为0。
- 接口索引用来描述这个类实现了哪些接口,这些被实现的接口将按照implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。有一个接口计数器(interfaces_count)。
五、字段表集合
1、字段表
字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。变量可以包括信息有:字段作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本数据类型、对象、数组)、字段名称。字段表结构如下
类型 | 名称 | 数量 | 类型 | 名称 | 数量 | |
---|---|---|---|---|---|---|
u2 | access_flags | 1 | u2 | attributes_count | 1 | |
u2 | name_index | 1 | attribute_info | attributes | attriutes_count | |
u2 | descriptor_index | 1 |
其中,修饰符都是布尔值,适合用标志位来表示,而字段叫什么名字,字段被定义为什么数据类型,都是无法固定的,只能引用常量池中的常量来描述。
2、修饰符
这里先介绍几个概念:
- 全限定名:格式形如org/spring/framework/test/Test,是把类全名中的“.”换成了“/”。在使用时一般会加入一个“;”表示全限定名结束。
- 简单名称:指没有类型和参数修饰符的方法或字段名称。
修饰符稍稍复杂一些,它是用来描述字段的数据类型、方法的参数列表(包括数量、类型、以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,如下表
表示字符 | 含义 | 表示字符 | 含义 | |
---|---|---|---|---|
B | 基本类型byte | J | long | |
C | char | S | short | |
D | double | Z | boolean | |
F | float | V | void | |
I | int | L | 对象类型,如Ljava/lang/Object |
对于数组类型,每一维度将使用一个前置“[”字符来描述,如一个定义为“java.lang.String[]”类型的二维数组,将被记录为:“[[Ljava/lang/String;”,一个整型数组“int[]“将被记录为”[I“。
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数严格顺序放在一组小括号”()“之内。如方法void inc()的描述符为”()V“,方法java.lang.String toString()的描述符为”()Ljava/lang/String“,方法int indexOf(char[] source, int sourceOffset, int[] source)的描述符为”([CI[I)I“。
3、补充
字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持外部类的访问行,会自动添加指向外部类实例的字段。另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。
六、方法表集合
Class文件存储格式中队方法的描述与对字段的描述集合采用了完全一致的方式,方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。
与字段表集合相对应的,如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“< clinit >”方法和实例构造器“< init >”方法。
七、属性表集合
1、概述
属性表(attribute_info)在前面的讲解之中已经出现过数次,在Class文件、字段表、方法表都可以携带自己的属性表集合,用以描述某些场景专有的信息。
与Class文件中其他数据项目要求严格的顺序、长度、内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,虚拟机运行时会忽略掉它不认识的属性。
2、属性表属性
属性表中的常用的部分属性及其说明,如下表
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量池 |
Deprecated | 类,方法表,字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | 供新的类型检查验证器检查处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
Signature | 类,方法表,字段表 | 记录泛型签名信息 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 |
Synthetic | 类,方法表,字段表 | 标志方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类文件 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持,标识可见 |
RuntimeInvisibleAnnotations | 类,方法表,字段表 | 用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法参数 |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokeddynamic指令引用的引导方式限定符 |
对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。一个符合规则的属性表应该满足如下所定义的结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
八、属性表属性
Code属性很重要,因此把它单独拿出来进行讲解。
1、Code属性
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在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”,它代表了该属性的属性名称。
- attribute_length:代表了属性值的长度,由于属性名称与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去6个字节。
- max_stack:代表了操作数栈(Operand Stacks)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行时需要根据这个值来分配栈桢中的操作栈深度。
- max_locals:代表了局部变量所需的存储空间,单位为Slot,Slot是虚拟机为局部变量分配内存的最小单位。对于byte、char、float、int、short、boolean和returnAdress等长度不超过32位的数据类型,每个局部变量占用一个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。方法参数(包括实例方法中的隐藏参数“this”)、显示异常处理器的参数(Exception Handler Parameter,就是try-catch语句中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。另外,并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之后作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,Java编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_local的大小(下文有验证)。
- code_length、code:两者用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用来存储字节码指令的一系列字节流。既然叫字节码指令,那么每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及参数应当如何理解。u1数据类型取值为0x00~0xFF,对应十进制0~255(注意区分不是2的7次方,即byte所表示的范围),也就是一个可以表达256条指令,具体编码与指令的对应关系请参考相关资料。
Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义以及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。了解Code属性是学习后面关于字节吗执行引擎内容的必要基础,能直接阅读字节码也是工作中分析Java语义问题的必要工具和基本技能。下面是一个用于分析的例子
1、Java代码
package site.xiaokui.common.hk.jvm;
/**
* @author HK
* @date 2018-12-28 21:41
*/
public class ByteCode {
private int m;
private int inc() {
return m + 1;
}
private int add(int a, int b) {
return a + b;
}
private static int add1(int a, int b) {
return a + b;
}
public int inc1() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
}
2、字节码
使用Javac编译上述java文件,用vi打开(此时乱码,切换到命令模式,输入:%!xxd,以16进制方式查看,退出输入%!xxd -r)。下面是结果,是有点反人类哈。
00000000: cafe babe 0000 0034 001d 0a00 0500 1709 .......4........
00000010: 0004 0018 0700 1907 001a 0700 1b01 0001 ................
00000020: 6d01 0001 4901 0006 3c69 6e69 743e 0100 m...I...<init>..
00000030: 0328 2956 0100 0443 6f64 6501 000f 4c69 .()V...Code...Li
00000040: 6e65 4e75 6d62 6572 5461 626c 6501 0003 neNumberTable...
00000050: 696e 6301 0003 2829 4901 0003 6164 6401 inc...()I...add.
00000060: 0005 2849 4929 4901 0004 6164 6431 0100 ..(II)I...add1..
00000070: 0469 6e63 3101 000d 5374 6163 6b4d 6170 .inc1...StackMap
00000080: 5461 626c 6507 0019 0700 1c01 000a 536f Table.........So
00000090: 7572 6365 4669 6c65 0100 0d42 7974 6543 urceFile...ByteC
000000a0: 6f64 652e 6a61 7661 0c00 0800 090c 0006 ode.java........
000000b0: 0007 0100 136a 6176 612f 6c61 6e67 2f45 .....java/lang/E
000000c0: 7863 6570 7469 6f6e 0100 2373 6974 652f xception..#site/
000000d0: 7869 616f 6b75 692f 636f 6d6d 6f6e 2f68 xiaokui/common/h
000000e0: 6b2f 6a76 6d2f 4279 7465 436f 6465 0100 k/jvm/ByteCode..
000000f0: 106a 6176 612f 6c61 6e67 2f4f 626a 6563 .java/lang/Objec
00000100: 7401 0013 6a61 7661 2f6c 616e 672f 5468 t...java/lang/Th
00000110: 726f 7761 626c 6500 2100 0400 0500 0000 rowable.!.......
00000120: 0100 0200 0600 0700 0000 0500 0100 0800 ................
00000130: 0900 0100 0a00 0000 1d00 0100 0100 0000 ................
00000140: 052a b700 01b1 0000 0001 000b 0000 0006 .*..............
00000150: 0001 0000 0007 0002 000c 000d 0001 000a ................
00000160: 0000 001f 0002 0001 0000 0007 2ab4 0002 ............*...
00000170: 0460 ac00 0000 0100 0b00 0000 0600 0100 .`..............
00000180: 0000 0c00 0200 0e00 0f00 0100 0a00 0000 ................
00000190: 1c00 0200 0300 0000 041b 1c60 ac00 0000 ...........`....
000001a0: 0100 0b00 0000 0600 0100 0000 1000 0a00 ................
000001b0: 1000 0f00 0100 0a00 0000 1c00 0200 0200 ................
000001c0: 0000 041a 1b60 ac00 0000 0100 0b00 0000 .....`..........
000001d0: 0600 0100 0000 1400 0100 1100 0d00 0100 ................
000001e0: 0a00 0000 8800 0100 0500 0000 1804 3c1b ..............<.
000001f0: 3d06 3c1c ac4d 053c 1b3e 063c 1dac 3a04 =.<..M.<.>.<..:.
00000200: 063c 1904 bf00 0400 0000 0400 0800 0300 .<..............
00000210: 0000 0400 1100 0000 0800 0d00 1100 0000 ................
00000220: 1100 1300 1100 0000 0200 0b00 0000 2e00 ................
00000230: 0b00 0000 1a00 0200 1b00 0400 2000 0600 ............ ...
00000240: 1b00 0800 1c00 0900 1d00 0b00 1e00 0d00 ................
00000250: 2000 0f00 1e00 1100 2000 1500 2100 1200 ....... ...!...
00000260: 0000 0a00 0248 0700 1348 0700 1400 0100 .....H...H......
00000270: 1500 0000 0200 160a ........
使用javap -verbose ByteCode,查看结果如下
hk@hk-pc:~$ javap -help
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=gasp
用法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
hk@hk-pc:~$ javap -p -v ByteCode
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=gasp
警告: 二进制文件ByteCode包含site.xiaokui.common.hk.jvm.ByteCode
Classfile /home/hk/ByteCode.class
Last modified 2018-12-28; size 631 bytes
MD5 checksum 4e2651670101c6a5e5b67b7675133949
Compiled from "ByteCode.java"
public class site.xiaokui.common.hk.jvm.ByteCode
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#24 // site/xiaokui/common/hk/jvm/ByteCode.m:I
#3 = Class #25 // java/lang/Exception
#4 = Class #26 // site/xiaokui/common/hk/jvm/ByteCode
#5 = Class #27 // java/lang/Object
#6 = Utf8 m
#7 = Utf8 I
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 inc
#13 = Utf8 ()I
#14 = Utf8 add
#15 = Utf8 (II)I
#16 = Utf8 add1
#17 = Utf8 inc1
#18 = Utf8 StackMapTable
#19 = Class #25 // java/lang/Exception
#20 = Class #28 // java/lang/Throwable
#21 = Utf8 SourceFile
#22 = Utf8 ByteCode.java
#23 = NameAndType #8:#9 // "<init>":()V
#24 = NameAndType #6:#7 // m:I
#25 = Utf8 java/lang/Exception
#26 = Utf8 site/xiaokui/common/hk/jvm/ByteCode
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/Throwable
{
private int m;
descriptor: I
flags: ACC_PRIVATE
public site.xiaokui.common.hk.jvm.ByteCode();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
private int inc();
descriptor: ()I
flags: ACC_PRIVATE
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 12: 0
private int add(int, int);
descriptor: (II)I
flags: ACC_PRIVATE
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: ireturn
LineNumberTable:
line 16: 0
private static int add1(int, int);
descriptor: (II)I
flags: ACC_PRIVATE, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: iadd
3: ireturn
LineNumberTable:
line 20: 0
public int inc1();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1
1: istore_1
2: iload_1
3: istore_2
4: iconst_3
5: istore_1
6: iload_2
7: ireturn
8: astore_2
9: iconst_2
10: istore_1
11: iload_1
12: istore_3
13: iconst_3
14: istore_1
15: iload_3
16: ireturn
17: astore 4
19: iconst_3
20: istore_1
21: aload 4
23: athrow
Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
17 19 17 any
LineNumberTable:
line 26: 0
line 27: 2
line 32: 4
line 27: 6
line 28: 8
line 29: 9
line 30: 11
line 32: 13
line 30: 15
line 32: 17
line 33: 21
StackMapTable: number_of_entries = 2
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
}
SourceFile: "ByteCode.java"
3、分析
1)首先是对于构造方法的分析
public site.xiaokui.common.hk.jvm.ByteCode();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
上述代码表示该构造方法的Code属性,其操作数栈的最大深度和本地变量表的容量都是1,字节码区域所占空间的长度为4 + 1 = 5。翻译字节码得到如下语义:
- aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量(this)推送至操作数栈顶。
- invokespecial #1,这个指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接受者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法(故没有2和3,被#1代替了),它指向常量池的一个CONSTANT_Methodref_info类型常量,即此方法的符号引用。
- return,含义是返回此方法且返回值为void。这条指令执行后,当前方法结束。注意这里的this已经是这个对象的引用了。
2)对于inc方法
private int m;
private int inc() {
return m + 1;
}
private int inc();
descriptor: ()I
flags: ACC_PRIVATE
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 12: 0
上述Code属性其操作数栈最大深度是2,本地变量表的容量是1,字节码区域所占空间的长度为6 + 1 = 7。翻译字节码得到如下语义:
- aload_0,首先将第0个Slot中的this推送至操作数栈顶。
- getfield,获取指定类的实例域,并将其压入栈顶。
- iconst_1,将int型1推送至栈顶。
- iadd,将栈顶两int型数值相加并将结果压入栈顶。
- ireturn,从当前方法返回int。
3)对于add方法
private int add(int a, int b) {
return a + b;
}
private static int add1(int a, int b) {
return a + b;
}
private int add(int, int);
descriptor: (II)I
flags: ACC_PRIVATE
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: ireturn
LineNumberTable:
line 16: 0
private static int add1(int, int);
descriptor: (II)I
flags: ACC_PRIVATE, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: iadd
3: ireturn
LineNumberTable:
line 20: 0
有了之前的分析,相信对于这里的字节码理解起来是很简单的。这里引入一个问题:两者的args_size不同,且为iload开始的索引也不同,且args_size有时于参数个数相等,有时又不相等。其实这里隐藏了this。
在任何实例方法里面,都可以通过“this”关键字访问到此方法所属的对象。这个访问机制对于Java程序的编写很重要,而它的实现却非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的方法转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表也会预留第一个Slot位来存放对象实例的引用,方法参数从1开始计算。这个处理只对实例方法有效,对于static方法,还是等于原参数个数。
4)显示异常处理表
在字节码指令之后的是这个方法的显式异常处理表(下文简称异常表)集合,异常表对于Code属性来说并不是必须存在的。
异常表的格式如表所示,它包含4个字段,这些字段的含义为:如果当字节码在第start_pc行到第end_pc行之间(不含end_pc)出现类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type值为0时,代表任意异常情况都需要转向到handler_pc处进行处理。
类型 | 名称 | 数量 | 类型 | 名称 | 数量 | |
---|---|---|---|---|---|---|
u2 | start_pc | 1 | u2 | handler_pc | 1 | |
u2 | end_pc | 1 | u2 | catch_type | 1 |
下面是代码,由于数据比较多,我们直接在代码旁边加注释
public int inc1() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
public int inc1();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1 // 将int型1推送至栈顶,即x = 1
1: istore_1 // 将栈顶int型数值存入第二个本地变量
2: iload_1 // 将第二个int型本地变量推送至栈顶
3: istore_2 // 将栈顶int型数值存入第三个本地变量
4: iconst_3 // 将int型3推送至栈顶
5: istore_1 // 将栈顶int型数值存入第二个本地变量
6: iload_2
7: ireturn
8: astore_2 // 将栈顶引用型数值存入第三个本地变量表
9: iconst_2
10: istore_1
11: iload_1
12: istore_3
13: iconst_3
14: istore_1
15: iload_3
16: ireturn
17: astore 4 // 将栈顶引用型数值存入指定本地变量表
19: iconst_3
20: istore_1
21: aload 4 // 将指定的引用类型本地变量推送至栈顶
23: athrow // 将栈顶的异常抛出
Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
17 19 17 any
LineNumberTable:
line 26: 0
line 27: 2
line 32: 4
line 27: 6
line 28: 8
line 29: 9
line 30: 11
line 32: 13
line 30: 15
line 32: 17
line 33: 21
StackMapTable: number_of_entries = 2
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
编译器会为这段Java源码生成4条异常表记录,对应4跳可能出现的代码执行路径。从Java代码语言上讲,这4跳执行路径分别为:
- 如果try语句块中出现了Exception类或其子类的异常,则转到catch语句块处理。
// 如果在0~4行字节码出现Exception异常或其子类,则跳转至8行
from to target type
0 4 8 Class java/lang/Exception
0: iconst_1 // 将int型1推送至栈顶
1: istore_1 // 将栈顶int型数值存入第二个本地变量
2: iload_1 // 将第二个int型本地变量推送至栈顶,即x = 1
3: istore_2 // 将栈顶int型数值存入第三个本地变量,即保存了x = 1的一个副本
4: iconst_3 // 将int型3推送至栈顶
// 接第一个假设
5: istore_1 // 将栈顶int型数值存入第二个本地变量,即保存了x = 3的一个副本
6: iload_2
7: ireturn
// 接第二个假设
8: astore_2 // 将栈顶引用型数值存入第三个本地变量表,即保存了Exception e
9: iconst_2 // 将int型2推送至栈顶
10: istore_1 // 将栈顶int型数值存入第二个本地变量
11: iload_1 // 将其推送至栈顶
12: istore_3 // 保存x = 2至本地第四个变量
13: iconst_3 // 将int3推送至栈顶
14: istore_1 // 保存至第二个Slot
15: iload_3 // 加载第4个
16: ireturn
这里我们假设不出现异常,那么会走到7行。此时结果为返回iload_2,即返回1。这里又假设出现Exception类异常,那么会跳转至8行继续处理,此时结果为iload_3,即返回2。
- 如果try语句块中出现了非Exception类或其子类的异常,则转到finnally语句块处理。
// 如果在0~17行出现非Exception类或其子类异常,则跳转至17行
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
17: astore 4 // 将栈顶引用型数值存入指定本地变量表,即将异常e保存进Slot4
19: iconst_3 // 推送int3
20: istore_1 // 保存至Slot1
21: aload 4 // 将指定的引用类型本地变量推送至栈顶
23: athrow // 将栈顶的异常抛出
从字节码上来看,方法做的是抛出未捕获异常。
- 如果catch语句块中出现任何异常,则转到finnally语句块处理。
// 如果8~13语句块中出现任何异常,则跳转至17行
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
8: astore_2 // 将栈顶引用型数值存入第三个本地变量表
9: iconst_2
10: istore_1
11: iload_1
12: istore_3
13: iconst_3
- 方法非正常结束。
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
17 19 17 any
17: astore 4 // 将栈顶引用型数值存入指定本地变量表
19: iconst_3
20: istore_1
21: aload 4 // 将指定的引用类型本地变量推送至栈顶
23: athrow // 将栈顶的异常抛出
作者说的:尽管大家都知道这段代码出现异常的概率非常小,但并不影响它为我们演示异常表的作用。如果大家到这里仍然对字节码的运作过程比较模糊,其实也不要紧,关于虚拟机执行字节码的过程,本书第8章将会有更详细的讲解。
我:会去拜读的。注:第8章—虚拟机字节码执行引擎。
2、其他属性
1)Exceptions属性
这里的Exceptions属性是在方法表中与Code属性平级的一项属性。它的作用是列举出方法中可能抛出的受查异常(Checked Exceptions),也就是方法描述时在throws关键字后面列举的异常。其结构如下
类型 | 名称 | 数量 | 类型 | 名称 | 数量 | |
---|---|---|---|---|---|---|
u2 | attribute_name_index | 1 | u2 | number_of_exceptions | 1 | |
u4 | attribute_length | 1 | u2 | exception_index_table | number_of_exceptions |
补充: number_of_exceptions表示方法可能抛出number_of_exceptions种受查异常,每一种受查异常使用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。
2)LineNumberTable属性
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要影响就是当抛出异常时,堆栈中奖不会显示出错的行号,并且在调试程序时,也无法按照源码行来设置断点。
还是以前面的代码为例,如下
package site.xiaokui.common.hk.jvm;
/**
* @author HK
* @date 2018-12-28 21:41
*/
public class ByteCode {
private int m;
private int inc() {
return m + 1;
}
private int add(int a, int b) {
return a + b;
}
private static int add1(int a, int b) {
return a + b;
}
public int inc1() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
}
// 对于构造方法
LineNumberTable:
line 7: 0
// 对于inc方法
LineNumberTable:
line 12: 0
// 对于add方法
line 16: 0
// 对于add1方法
line 20: 0
// 对于inc1方法
LineNumberTable:
line 26: 0
line 27: 2
line 32: 4
line 27: 6
line 28: 8
line 29: 9
line 30: 11
line 32: 13
line 30: 15
line 32: 17
line 33: 21
具体什么规律,我还真没观察出个所以然,哈哈,有机会再来回顾吧。
3)LocalVariableTable属性
LocalVariableTable属性用于描述栈桢中局部变量表中的变量与Java源代码中定义的变量之间的关系。他也不是运行时必需的属性,但默认会生成到Class文件之中,可以分别使用-g:none或-g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。
4)SourceFile属性
SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可以分别使用Javac的-g:none或-g:source选项来关闭或要求生成这项信息。在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况例外(如内部类)。如果不生产这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。
5)ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。
6)InnerClasses属性
InnerClasses属性用于记录内部类与宿主类之间的关联。
7)Deprecated及Synthetic属性
Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只有存在和没有的区别,没有属性值的概念。其中
- Deprecated属性:用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@deprecated注解进行设置。
- Synthetic属性:代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的。
8)StackMapTable属性
StackMapTable属性是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用(确保字节码行为逻辑的合法性),目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
9)Signature属性
Signature属性是一个可选的定长属性,可以出现于类、字段表和方法表结构的属性表中,用于记录泛型信息。
总访问次数: 252次, 一般般帅 创建于 2017-10-08, 最后更新于 2020-06-25
欢迎关注微信公众号,第一时间掌握最新动态!