目录
前言
Java源程序被编译为class文件后,加载进虚拟机中,那么虚拟机是如何解析和分析class内容的呢?这里我们将以一个具体的java程序编译后的字节码流进行分析,学习Class类文件的结构。
1 Class类文件结构
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格排列,无任何分隔符
Class文件结构中有两种结构:
- 无符号数。
- 表。
1.1 无符号数属于基本数据类型:
以u1、u2、u4、u8来表示1个字节、2个字节、4个字节和8个字节的无符号数,可以用来表示数字、索引引用、数量值或者按照UTF-8编码的字符串值
1.2 表的说明如下:
表是由多个无符号数和其他表作为数据项构成的复合数据结构,习惯以_info结尾,Class文件本身就是一个表
1.3 Class文件格式
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 |
当需要描述一组同一类型数据时通常会前置一个容器计数器,以便有效分割定位Class文件各个数据项。
2 Class类文件具体分析
2.1 java示例程序
示例程序如下:
public class ExampleClass implements Serializable, Cloneable {
private String name;
public String showName(String prefix){
return prefix+name;
}
}
在分析字节码之前我们先用javap工具-verbose参数生成测试代码的字节码内容:
Last modified 2022-4-24; size 768 bytes
MD5 checksum 2cda1b2fa86b366fb17e80d44718cef7
Compiled from "ExampleClass.java"
public class com.example.springmvc.asm.ExampleClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#26 // java/lang/Object."<init>":()V
#2 = Class #27 // java/lang/StringBuilder
#3 = Methodref #2.#26 // java/lang/StringBuilder."<init>":()V
#4 = Methodref #2.#28 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#5 = Fieldref #7.#29 // com/example/springmvc/asm/ExampleClass.name:Ljava/lang/String;
#6 = Methodref #2.#30 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#7 = Class #31 // com/example/springmvc/asm/ExampleClass
#8 = Class #32 // java/lang/Object
#9 = Utf8 name
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/example/springmvc/asm/ExampleClass;
#18 = Utf8 showName
#19 = Utf8 (Ljava/lang/String;)Ljava/lang/String;
#20 = Utf8 prefix
#21 = Utf8 MethodParameters
#22 = Utf8 SourceFile
#23 = Utf8 ExampleClass.java
#24 = Utf8 RuntimeVisibleAnnotations
#25 = Utf8 Lorg/springframework/stereotype/Component;
#26 = NameAndType #11:#12 // "<init>":()V
#27 = Utf8 java/lang/StringBuilder
#28 = NameAndType #33:#34 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#29 = NameAndType #9:#10 // name:Ljava/lang/String;
#30 = NameAndType #35:#36 // toString:()Ljava/lang/String;
#31 = Utf8 com/example/springmvc/asm/ExampleClass
#32 = Utf8 java/lang/Object
#33 = Utf8 append
#34 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#35 = Utf8 toString
#36 = Utf8 ()Ljava/lang/String;
{
public com.example.springmvc.asm.ExampleClass();
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 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/springmvc/asm/ExampleClass;
public java.lang.String showName(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: aload_1
8: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
11: aload_0
12: getfield #5 // Field name:Ljava/lang/String;
15: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
21: areturn
LineNumberTable:
line 15: 0
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcom/example/springmvc/asm/ExampleClass;
0 22 1 prefix Ljava/lang/String;
MethodParameters:
Name Flags
prefix
}
SourceFile: "ExampleClass.java"
RuntimeVisibleAnnotations:
0: #25()
PS C:\files\idea-workspace\unit2\springmvc\target\classes\com\example\springmvc\asm> javap -verbose .\ExampleClass.class
Classfile /C:/files/idea-workspace/unit2/springmvc/target/classes/com/example/springmvc/asm/ExampleClass.class
Last modified 2022-4-24; size 738 bytes
MD5 checksum 3183c52ca80cd8c3534265a7064f521b
Compiled from "ExampleClass.java"
public class com.example.springmvc.asm.ExampleClass implements java.io.Serializable,java.lang.Cloneable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#26 // java/lang/Object."<init>":()V
#2 = Class #27 // java/lang/StringBuilder
#3 = Methodref #2.#26 // java/lang/StringBuilder."<init>":()V
#4 = Methodref #2.#28 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#5 = Fieldref #7.#29 // com/example/springmvc/asm/ExampleClass.name:Ljava/lang/String;
#6 = Methodref #2.#30 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#7 = Class #31 // com/example/springmvc/asm/ExampleClass
#8 = Class #32 // java/lang/Object
#9 = Class #33 // java/io/Serializable
#10 = Class #34 // java/lang/Cloneable
#11 = Utf8 name
#12 = Utf8 Ljava/lang/String;
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 Lcom/example/springmvc/asm/ExampleClass;
#20 = Utf8 showName
#21 = Utf8 (Ljava/lang/String;)Ljava/lang/String;
#22 = Utf8 prefix
#23 = Utf8 MethodParameters
#24 = Utf8 SourceFile
#25 = Utf8 ExampleClass.java
#26 = NameAndType #13:#14 // "<init>":()V
#27 = Utf8 java/lang/StringBuilder
#28 = NameAndType #35:#36 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#29 = NameAndType #11:#12 // name:Ljava/lang/String;
#30 = NameAndType #37:#38 // toString:()Ljava/lang/String;
#31 = Utf8 com/example/springmvc/asm/ExampleClass
#32 = Utf8 java/lang/Object
#33 = Utf8 java/io/Serializable
#34 = Utf8 java/lang/Cloneable
#35 = Utf8 append
#36 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#37 = Utf8 toString
#38 = Utf8 ()Ljava/lang/String;
0 5 0 this Lcom/example/springmvc/asm/ExampleClass;
public java.lang.String showName(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: aload_1
8: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
11: aload_0
12: getfield #5 // Field name:Ljava/lang/String;
15: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
21: areturn
LineNumberTable:
line 14: 0
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcom/example/springmvc/asm/ExampleClass;
0 22 1 prefix Ljava/lang/String;
MethodParameters:
Name Flags
prefix
}
SourceFile: "ExampleClass.java"
该内容有利于我们后续进行分析对比。
2.2 魔数和Class文件版本
每个文件的头4字节称为魔数,唯一作用是确保这个文件是否是一个能被虚拟机接受的Class文件。
2.2.1 魔数
在java中这个魔数(magic)固定为0xCAFEBABE
,查看字节码文件(以16进制打开):
头四个字节如下:
2.2.2 版本号
之后四个字节,前两个为次版本号(minor_version),后两个为主版本号(major_version),如下:
4-7字节如下:
0x34 = 52,因此该Class文件对应的java版本52.0是JDK 1.8 = 52
。
2.3 常量池
由表1-1可知,紧接着版本的是常量池:常量个数+常量项。
从表中可知常量索引是:1~constant_pool_count-1。
把索引0空出来是制定Class文件格式规范时,满足“某些指向常量池的数据在特定情况下不引用任何一个常量池项目”。
除了常量池,后续接口索引集合、字段表集合、方法表集合等都是从0开始。
2.3.1 常量池长度
查看对应位置字节码,常量池长度如下:
8-9字节
可见本测试demo有2*16+7-1=38个常量,参考上述javap反编译的内容可见一致。
2.3.2 常量池常量类型说明
2.3.2.1 常量池字面量和符号引用的类型转化描述
常量池主要存放两大类常量:字面量和符号引用。
字面量:文本字符串,final类型的常量。
符号引用包括下面几种:
1. 类和接口全限定名。
2. 字段名称和描述符。
3. 方法的名称和描述符。
内部名称
主要用于类或接口类型,这些类型表示为具有内部名称的编译类。类的内部名称就是该类的全限定名,其中点被斜线取代,以Object为例:
Object全限定名为java.lang.Object,内部将表示为:java/lang/Object
类型描述
Java类型在编译类中将表示为类型描述符,具体对应如下表:
Java类型 | 类型描述符 |
---|---|
boolean | Z |
char | C |
byte | B |
short | S |
int | I |
float | F |
long | J |
double | D |
Object | Ljava/lang/Object; |
int[] | [I |
Object[][] | [[Ljava/lang/Object; |
可以看出对应java基本类型,都描述为单一大写字符,对于class类型将描述为它内部的类名称前边修饰为L末尾添加分隔符;,对于数组类型需要在数组类型的前边添加[,几维就添加几个。
方法描述
方法描述符就是一系列类型描述符表示参数和一个方法返回类型,具体示例如下表所示:
源文件方法描述 | 编译后的方法描述符 |
---|---|
void m(int i, float f) | (IF)V,void返回使用V表示 |
int m(Object o) | (Ljava/lang/Object;)I |
int[] m(int i, String s) | (ILjava/lang/String;)[I |
Object m(int[] i) | ([I)Ljava/lang/Object; |
2.3.2.2 常量项分类和具体结构
常量池中每一项都是一个表,总共有11中类型,其具体定义如下:
类型 | 标志 | 描述 |
---|---|---|
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 | 字段或方法的部分符号引用 |
表2-3中具体每种常量的结构如下:
常量 | 项目 | 类型 | 描述 |
---|---|---|---|
CONSTANT_Utf8_info | tag | u1 | 值为1 |
length | u2 | utf8编码字符串占的字节数 | |
bytes | u1 | 长度为length的utf8编码的字符串 | |
CONSTANT_Intege_info | tag | u1 | 值为3 |
bytes | u4 | 按照高位在前存储的int值 | |
CONSTANT_Float_info | tag | u1 | 值为4 |
bytes | u4 | 按照高位在前存储的float值 | |
CONSTANT_Long_info | tag | u1 | 值为5 |
bytes | u8 | 按照高位在前存储的long值 | |
CONSTANT_Double_info | tag | u1 | 值为6 |
bytes | u8 | 按照高位在前存储的double值 | |
CONSTANT_Class_info | tag | u1 | 值为7 |
index | u2 | 指向全限定名常量项索引 | |
CONSTANT_String_info | tag | u1 | 值为8 |
index | u2 | 指向字符串字面量索引 | |
CONSTANT_Fieldref_info | tag | u1 | 值为9 |
index | u2 | 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向字段描述符CONSTANT_NameAndType_info的索引项 | |
CONSTANT_Methodref_info | tag | u1 | 值为10 |
index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向名称和类型描述符CONSTANT_NameAndType_info的索引项 | |
CONSTANT_InterfaceMethodref_info | tag | u1 | 值为11 |
index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向名称和类型描述符CONSTANT_NameAndType_info的索引项 | |
CONSTANT_NameAndType_info | tag | u1 | 值为12 |
index | u2 | 指向该字段或者方法名称常量项的索引项 | |
index | u2 | 指向该字段或方法描述符常量项的索引项 |
2.3.3 字节码常量分析
字节码第一个常量如下:
可见0x0a=10,对应为CONSTANT_Methodref_info,该常量后两个对应为声明类描述符索引和名称与类型描述符索引分别为8和26,结合javap内容可知对应索引项为:java/lang/Objec和()V
javap的对应内容如下:
第二个常量分析:
07对应类型为:CONSTANT_Class_info,后边2字节对应类全限定名的索引即:27,查看可知该项为:java/lang/StringBuilder
通过对上述两个常量项分析,我们知道后续的常量项都可以通过此类方法推断出来。分析时可以借助字节码分析工具javap。
2.4 访问标志
在常量池结束之后,紧跟两个字节是代表访问标志(access_flags),这个标志用于识别类或接口的访问信息,具体的标志位和标志含义定义如下表:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令,jdk1.2之后默认类这个字段为true |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对应接口和抽象类为true,其它为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
以测试demo为例,没有声明final和abstract,因此它的ACC_PUBLIC和ACC_SUPER应该为真,因此它的标志位应该是:
access_flags:0x0001|0x0020=0x0021
字节码如下:
与上述分析一致:地址0x00000220(值:0x0021)。
2.5 类索引、父类索引和接口索引集合
类索引和父类索引都是u2类型数据,指向常量池中具体索引,接口集合为:接口个数+各个接口索引。
以此分析字节码如下(地址:0x00000223):
根据上述分析类索引为0x0007第七项、父类索引0x0008第8项,接口索引:个数0x0002(索引分别为:第九项和第10项),根据javap分析内容如下:
2.6 字段表集合
字段表用于描述接口或者类中声明的变量。
field包括:类级变量和实例级变量,不包括方法内部声明的变量。
字段表的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段的access_flags与类的访问符类似,都是u2类型,具体定义如下表:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否public |
ACC_PRIVATE | 0x0002 | 字段是否为private |
ACC_PROTECTED | 0x0004 | 字段是否为protected |
ACC_STATIC | 0x0008 | 字段是否为static |
ACC_FINAL | 0x0010 | 字段是否为final |
ACC_VOLATILE | 0x0040 | 字段是否为volatile |
ACC_TRANSIENT | 0x0080 | 字段是否为transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生 |
ACC_ENUM | 0x4000 | 字段是否为enum |
根据上述这些信息,我们分析字节码如下:
上述分别对应字段个数1个,之后为每个字段具体信息:访问符+名称索引+描述符索引+属性个数
对应测试代码private String name
,
0x0002——ACC_PRIVATE
0x000b——索引11:name
0x000c——索引12:Ljava/lang/String;
0x0000——属性个数为0
2.7 方法表集合
和字段表类似,描述类定义的方法。
方法表结构定义如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法表访问标志也与字段类似,定义如下:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否public |
ACC_PRIVATE | 0x0002 | 方法是否为private |
ACC_PROTECTED | 0x0004 | 方法是否为protected |
ACC_STATIC | 0x0008 | 方法是否为static |
ACC_FINAL | 0x0010 | 方法是否为final |
ACC_BRIDGE | 0x0040 | 方法是否由编译器产生的桥接方法(由于泛型擦除,为了兼容1.5产生之前的字节码 ) |
ACC_VARARGST | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否是抽象的 |
ACC_STRICT | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动产生 |
继续以我们的测试demo进行分析(方法表入口地址为0x00000237):
0x0002:对应方法个数为2,构造器和showName。
下面对应第一个方法:<init>
0x0001:access_flags方法访问符即ACC_PUBLIC.
0x000d:名称索引13,对应为
0x000e:方法描述符索引14,对应为()V
0x0001:属性个数,也即是改方法有一个属性
2.8 属性表集合
在Class文件、字段表和方法表都可以出现属性。
虚拟机定义的属性有以下几种:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | java源码的行号和字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 源文件名称 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成。 |
属性表一般结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u2 | attribute_length | 1 |
u1 | info | attribute_length |
下面为具体属性的结构定义均符合表2-11的结构,下面是Code属性的结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index(常量索引固定为Code) | 1 |
u4 | attribute_length(固定为整个属性表长度-6字节) | 1 |
u2 | max_stack(操作数栈最大深度) | 1 |
u2 | max_locals(局部变量表所需的存储空间) | 1 |
u4 | code_length(字节码指令长度) | 1 |
u1 | code(具体的字节码指令) | code_length |
u2 | exception_table_length(异常表长度) | 1 |
exception | exception_table(具体异常表) | exception_table_length |
u2 | attributes_count(属性长度) | 1 |
attribute_info | attribute(具体属性) | attribute_count |
继续以代码为例进行分析:
0x000f:对应属性名称索引15,也就是Code。
0x0000002f:对应属性长度也即是47,对应图中竖线位置,因为只有一个属性,所以后边是就另一个方法了。
0x0001:最大栈深度为1。
0x0001:局部变量表存储空间为1。
0x00000005:字节码指令长度为5。
2a、b7、00、01、b1是具体指令代码流:查表可知分别对应aload、invokespecial、return。
1. 2a代表:aload这里指将局部变量表slot0:this加载到栈顶,查看oracle-jvm该指令无后续操作数跟随,进入下一个指令
2. b7代表:invokespecial指令后边跟随两个字节,表示需要调用方法的常量池索引,后续00 01也就是第一项:init方法
3. b1:retrun代表返回void,后续无操作数
0x0000:异常表长度,这里没有抛出也就是0
0x0002:属性数量,这里为2,后边也就是具体属性信息
由于Code属性中的属性只有LineNumberTable和LocalVariableTable这里说明以下:
LineNumberTable结构定义如下
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | lilne_number_table | line_number_table_length |
line_number_info包括:start_pc和line_number两个u2类型数据。前者为字节码行号,后者为java源码行号。
LocalVariableTable结构定义如下
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | local_variable_table_length | 1 |
local_variable_info | local_variable_table | local_variable_table_length |
其中local_variable_info结构如下
类型 | 名称 | 数量 |
---|---|---|
u2 | start_pc | 1 |
u2 | length | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | index | 1 |
start_pc:局部变量的生命周期开始的字节码偏移量。
length:其作用范围覆盖长度。和start_pc构成了局部变量的作用范围。
name_index和descriptor_index指向常量池,代表局部变量名称和描述符。
index:代表局部变量在栈帧局部变量表中的slot位置
下面是Code属性中唯一属性的分析:
第一个属性:
0x0010:属性名称索引,对应为常量池16,LineNumberTable
0x00000006:属性长度,对应为6。
0x0001:line_number_table_length对应为1,即只有一个。
0x0000-0x000a:line_number_info对应为:line10:0。
第二个属性:
0x0011:属性名索引,对应常量池17,LocalVariableTable
0x0000000c:属性长度,12。
0x0001:局部变量表长度1。
具体变量如下
0x0000:字节码偏移为0。
0x0005:作用范围覆盖长度为5。
0x0012:名字索引为18,对应为this。
0x0013:描述索引为19,对应为Lcom/example/springmvc/asm/ExampleClass;。
0x0000:index对应slot位置为0。
第一个方法javap内容截图如下:
对应内容与我们分析一致,至此第一个方法分析结束。
第二个方法分析与之相同,不在叙述,具体方法范围如下:
方法范围为(0x00000275~0x000002cc).
0x000002cd~0x000002da为:MethodParameters,本文不讨论。
0x000002db~0x000002e1为SourceFile类型。
0x000002e2为填充。
参考文献
[1]. 《深入理解Java虚拟机》
[2]. 《ASM 4.0 A Java bytecode engineering library》
[3]. 从一个class文件深入理解Java字节码结构
参考表
下表来自博主大佬四月葡萄总结:从一个class文件深入理解Java字节码结构).
字节码 | 助记符 | 指令含义 |
---|---|---|
0x00 | nop | 什么都不做 |
0x01 | aconst_null | 将null推送至栈顶 |
0x02 | iconst_m1 | 将int型-1推送至栈顶 |
0x03 | iconst_0 | 将int型0推送至栈顶 |
0x04 | iconst_1 | 将int型1推送至栈顶 |
0x05 | iconst_2 | 将int型2推送至栈顶 |
0x06 | iconst_3 | 将int型3推送至栈顶 |
0x07 | iconst_4 | 将int型4推送至栈顶 |
0x08 | iconst_5 | 将int型5推送至栈顶 |
0x09 | lconst_0 | 将long型0推送至栈顶 |
0x0a | lconst_1 | 将long型1推送至栈顶 |
0x0b | fconst_0 | 将float型0推送至栈顶 |
0x0c | fconst_1 | 将float型1推送至栈顶 |
0x0d | fconst_2 | 将float型2推送至栈顶 |
0x0e | dconst_0 | 将do le型0推送至栈顶 |
0x0f | dconst_1 | 将do le型1推送至栈顶 |
0x10 | bipush | 将单字节的常量值(-128~127)推送至栈顶 |
0x11 | sipush | 将一个短整型常量值(-32768~32767)推送至栈顶 |
0x12 | ldc | 将int, float或String型常量值从常量池中推送至栈顶 |
0x13 | ldc_w | 将int, float或String型常量值从常量池中推送至栈顶(宽索引) |
0x14 | ldc2_w | 将long或do le型常量值从常量池中推送至栈顶(宽索引) |
0x15 | iload | 将指定的int型本地变量 |
0x16 | lload | 将指定的long型本地变量 |
0x17 | fload | 将指定的float型本地变量 |
0x18 | dload | 将指定的do le型本地变量 |
0x19 | aload | 将指定的引用类型本地变量 |
0x1a | iload_0 | 将第一个int型本地变量 |
0x1b | iload_1 | 将第二个int型本地变量 |
0x1c | iload_2 | 将第三个int型本地变量 |
0x1d | iload_3 | 将第四个int型本地变量 |
0x1e | lload_0 | 将第一个long型本地变量 |
0x1f | lload_1 | 将第二个long型本地变量 |
0x20 | lload_2 | 将第三个long型本地变量 |
0x21 | lload_3 | 将第四个long型本地变量 |
0x22 | fload_0 | 将第一个float型本地变量 |
0x23 | fload_1 | 将第二个float型本地变量 |
0x24 | fload_2 | 将第三个float型本地变量 |
0x25 | fload_3 | 将第四个float型本地变量 |
0x26 | dload_0 | 将第一个do le型本地变量 |
0x27 | dload_1 | 将第二个do le型本地变量 |
0x28 | dload_2 | 将第三个do le型本地变量 |
0x29 | dload_3 | 将第四个do le型本地变量 |
0x2a | aload_0 | 将第一个引用类型本地变量 |
0x2b | aload_1 | 将第二个引用类型本地变量 |
0x2c | aload_2 | 将第三个引用类型本地变量 |
0x2d | aload_3 | 将第四个引用类型本地变量 |
0x2e | iaload | 将int型数组指定索引的值推送至栈顶 |
0x2f | laload | 将long型数组指定索引的值推送至栈顶 |
0x30 | faload | 将float型数组指定索引的值推送至栈顶 |
0x31 | daload | 将do le型数组指定索引的值推送至栈顶 |
0x32 | aaload | 将引用型数组指定索引的值推送至栈顶 |
0x33 | baload | 将boolean或byte型数组指定索引的值推送至栈顶 |
0x34 | caload | 将char型数组指定索引的值推送至栈顶 |
0x35 | saload | 将short型数组指定索引的值推送至栈顶 |
0x36 | istore | 将栈顶int型数值存入指定本地变量 |
0x37 | lstore | 将栈顶long型数值存入指定本地变量 |
0x38 | fstore | 将栈顶float型数值存入指定本地变量 |
0x39 | dstore | 将栈顶do le型数值存入指定本地变量 |
0x3a | astore | 将栈顶引用型数值存入指定本地变量 |
0x3b | istore_0 | 将栈顶int型数值存入第一个本地变量 |
0x3c | istore_1 | 将栈顶int型数值存入第二个本地变量 |
0x3d | istore_2 | 将栈顶int型数值存入第三个本地变量 |
0x3e | istore_3 | 将栈顶int型数值存入第四个本地变量 |
0x3f | lstore_0 | 将栈顶long型数值存入第一个本地变量 |
0x40 | lstore_1 | 将栈顶long型数值存入第二个本地变量 |
0x41 | lstore_2 | 将栈顶long型数值存入第三个本地变量 |
0x42 | lstore_3 | 将栈顶long型数值存入第四个本地变量 |
0x43 | fstore_0 | 将栈顶float型数值存入第一个本地变量 |
0x44 | fstore_1 | 将栈顶float型数值存入第二个本地变量 |
0x45 | fstore_2 | 将栈顶float型数值存入第三个本地变量 |
0x46 | fstore_3 | 将栈顶float型数值存入第四个本地变量 |
0x47 | dstore_0 | 将栈顶do le型数值存入第一个本地变量 |
0x48 | dstore_1 | 将栈顶do le型数值存入第二个本地变量 |
0x49 | dstore_2 | 将栈顶do le型数值存入第三个本地变量 |
0x4a | dstore_3 | 将栈顶do le型数值存入第四个本地变量 |
0x4b | astore_0 | 将栈顶引用型数值存入第一个本地变量 |
0x4c | astore_1 | 将栈顶引用型数值存入第二个本地变量 |
0x4d | astore_2 | 将栈顶引用型数值存入第三个本地变量 |
0x4e | astore_3 | 将栈顶引用型数值存入第四个本地变量 |
0x4f | iastore | 将栈顶int型数值存入指定数组的指定索引位置 |
0x50 | lastore | 将栈顶long型数值存入指定数组的指定索引位置 |
0x51 | fastore | 将栈顶float型数值存入指定数组的指定索引位置 |
0x52 | dastore | 将栈顶do le型数值存入指定数组的指定索引位置 |
0x53 | aastore | 将栈顶引用型数值存入指定数组的指定索引位置 |
0x54 | bastore | 将栈顶boolean或byte型数值存入指定数组的指定索引位置 |
0x55 | castore | 将栈顶char型数值存入指定数组的指定索引位置 |
0x56 | sastore | 将栈顶short型数值存入指定数组的指定索引位置 |
0x57 | pop | 将栈顶数值弹出 (数值不能是long或do le类型的) |
0x58 | pop2 | 将栈顶的一个(long或do le类型的)或两个数值弹出(其它) |
0x59 | dup | 复制栈顶数值并将复制值压入栈顶 |
0x5a | dup_x1 | 复制栈顶数值并将两个复制值压入栈顶 |
0x5b | dup_x2 | 复制栈顶数值并将三个(或两个)复制值压入栈顶 |
0x5c | dup2 | 复制栈顶一个(long或do le类型的)或两个(其它)数值并将复制值压入栈顶 |
0x5d | dup2_x1 | dup_x1 指令的双倍版本 |
0x5e | dup2_x2 | dup_x2 指令的双倍版本 |
0x5f | swap | 将栈最顶端的两个数值互换(数值不能是long或do le类型的) |
0x60 | iadd | 将栈顶两int型数值相加并将结果压入栈顶 |
0x61 | ladd | 将栈顶两long型数值相加并将结果压入栈顶 |
0x62 | fadd | 将栈顶两float型数值相加并将结果压入栈顶 |
0x63 | dadd | 将栈顶两do le型数值相加并将结果压入栈顶 |
0x64 | is | 将栈顶两int型数值相减并将结果压入栈顶 |
0x65 | ls | 将栈顶两long型数值相减并将结果压入栈顶 |
0x66 | fs | 将栈顶两float型数值相减并将结果压入栈顶 |
0x67 | ds | 将栈顶两do le型数值相减并将结果压入栈顶 |
0x68 | imul | 将栈顶两int型数值相乘并将结果压入栈顶 |
0x69 | lmul | 将栈顶两long型数值相乘并将结果压入栈顶 |
0x6a | fmul | 将栈顶两float型数值相乘并将结果压入栈顶 |
0x6b | dmul | 将栈顶两do le型数值相乘并将结果压入栈顶 |
0x6c | idiv | 将栈顶两int型数值相除并将结果压入栈顶 |
0x6d | ldiv | 将栈顶两long型数值相除并将结果压入栈顶 |
0x6e | fdiv | 将栈顶两float型数值相除并将结果压入栈顶 |
0x6f | ddiv | 将栈顶两do le型数值相除并将结果压入栈顶 |
0x70 | irem | 将栈顶两int型数值作取模运算并将结果压入栈顶 |
0x71 | lrem | 将栈顶两long型数值作取模运算并将结果压入栈顶 |
0x72 | frem | 将栈顶两float型数值作取模运算并将结果压入栈顶 |
0x73 | drem | 将栈顶两do le型数值作取模运算并将结果压入栈顶 |
0x74 | ineg | 将栈顶int型数值取负并将结果压入栈顶 |
0x75 | lneg | 将栈顶long型数值取负并将结果压入栈顶 |
0x76 | fneg | 将栈顶float型数值取负并将结果压入栈顶 |
0x77 | dneg | 将栈顶do le型数值取负并将结果压入栈顶 |
0x78 | ishl | 将int型数值左移位指定位数并将结果压入栈顶 |
0x79 | lshl | 将long型数值左移位指定位数并将结果压入栈顶 |
0x7a | ishr | 将int型数值右(符号)移位指定位数并将结果压入栈顶 |
0x7b | lshr | 将long型数值右(符号)移位指定位数并将结果压入栈顶 |
0x7c | iushr | 将int型数值右(无符号)移位指定位数并将结果压入栈顶 |
0x7d | lushr | 将long型数值右(无符号)移位指定位数并将结果压入栈顶 |
0x7e | iand | 将栈顶两int型数值作“按位与”并将结果压入栈顶 |
0x7f | land | 将栈顶两long型数值作“按位与”并将结果压入栈顶 |
0x80 | ior | 将栈顶两int型数值作“按位或”并将结果压入栈顶 |
0x81 | lor | 将栈顶两long型数值作“按位或”并将结果压入栈顶 |
0x82 | ixor | 将栈顶两int型数值作“按位异或”并将结果压入栈顶 |
0x83 | lxor | 将栈顶两long型数值作“按位异或”并将结果压入栈顶 |
0x84 | iinc | 将指定int型变量增加指定值(i++, i–, i+=2) |
0x85 | i2l | 将栈顶int型数值强制转换成long型数值并将结果压入栈顶 |
0x86 | i2f | 将栈顶int型数值强制转换成float型数值并将结果压入栈顶 |
0x87 | i2d | 将栈顶int型数值强制转换成do le型数值并将结果压入栈顶 |
0x88 | l2i | 将栈顶long型数值强制转换成int型数值并将结果压入栈顶 |
0x89 | l2f | 将栈顶long型数值强制转换成float型数值并将结果压入栈顶 |
0x8a | l2d | 将栈顶long型数值强制转换成do le型数值并将结果压入栈顶 |
0x8b | f2i | 将栈顶float型数值强制转换成int型数值并将结果压入栈顶 |
0x8c | f2l | 将栈顶float型数值强制转换成long型数值并将结果压入栈顶 |
0x8d | f2d | 将栈顶float型数值强制转换成do le型数值并将结果压入栈顶 |
0x8e | d2i | 将栈顶do le型数值强制转换成int型数值并将结果压入栈顶 |
0x8f | d2l | 将栈顶do le型数值强制转换成long型数值并将结果压入栈顶 |
0x90 | d2f | 将栈顶do le型数值强制转换成float型数值并将结果压入栈顶 |
0x91 | i2b | 将栈顶int型数值强制转换成byte型数值并将结果压入栈顶 |
0x92 | i2c | 将栈顶int型数值强制转换成char型数值并将结果压入栈顶 |
0x93 | i2s | 将栈顶int型数值强制转换成short型数值并将结果压入栈顶 |
0x94 | lcmp | 比较栈顶两long型数值大小,并将结果(1,0,-1)压入栈顶 |
0x95 | fcmpl | 比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶 |
0x96 | fcmpg | 比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶 |
0x97 | dcmpl | 比较栈顶两do le型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶 |
0x98 | dcmpg | 比较栈顶两do le型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶 |
0x99 | ifeq | 当栈顶int型数值等于0时跳转 |
0x9a | ifne | 当栈顶int型数值不等于0时跳转 |
0x9b | iflt | 当栈顶int型数值小于0时跳转 |
0x9c | ifge | 当栈顶int型数值大于等于0时跳转 |
0x9d | ifgt | 当栈顶int型数值大于0时跳转 |
0x9e | ifle | 当栈顶int型数值小于等于0时跳转 |
0x9f | if_icmpeq | 比较栈顶两int型数值大小,当结果等于0时跳转 |
0xa0 | if_icmpne | 比较栈顶两int型数值大小,当结果不等于0时跳转 |
0xa1 | if_icmplt | 比较栈顶两int型数值大小,当结果小于0时跳转 |
0xa2 | if_icmpge | 比较栈顶两int型数值大小,当结果大于等于0时跳转 |
0xa3 | if_icmpgt | 比较栈顶两int型数值大小,当结果大于0时跳转 |
0xa4 | if_icmple | 比较栈顶两int型数值大小,当结果小于等于0时跳转 |
0xa5 | if_acmpeq | 比较栈顶两引用型数值,当结果相等时跳转 |
0xa6 | if_acmpne | 比较栈顶两引用型数值,当结果不相等时跳转 |
0xa7 | goto | 无条件跳转 |
0xa8 | jsr | 跳转至指定16位offset位置,并将jsr下一条指令地址压入栈顶 |
0xa9 | ret | 返回至本地变量 |
0xaa | tableswitch | 用于switch条件跳转,case值连续(可变长度指令) |
0xab | lookupswitch | 用于switch条件跳转,case值不连续(可变长度指令) |
0xac | ireturn | 从当前方法返回int |
0xad | lreturn | 从当前方法返回long |
0xae | freturn | 从当前方法返回float |
0xaf | dreturn | 从当前方法返回do le |
0xb0 | areturn | 从当前方法返回对象引用 |
0xb1 | return | 从当前方法返回void |
0xb2 | getstatic | 获取指定类的静态域,并将其值压入栈顶 |
0xb3 | putstatic | 为指定的类的静态域赋值 |
0xb4 | getfield | 获取指定类的实例域,并将其值压入栈顶 |
0xb5 | putfield | 为指定的类的实例域赋值 |
0xb6 | invokevirtual | 调用实例方法 |
0xb7 | invokespecial | 调用超类构造方法,实例初始化方法,私有方法 |
0xb8 | invokestatic | 调用静态方法 |
0xb9 | invokeinterface | 调用接口方法 |
0xba | – | 无此指令 |
0xbb | new | 创建一个对象,并将其引用值压入栈顶 |
0xbc | newarray | 创建一个指定原始类型(如int, float, char…)的数组,并将其引用值压入栈顶 |
0xbd | anewarray | 创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶 |
0xbe | arraylength | 获得数组的长度值并压入栈顶 |
0xbf | athrow | 将栈顶的异常抛出 |
0xc0 | checkcast | 检验类型转换,检验未通过将抛出ClassCastException |
0xc1 | instanceof | 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶 |
0xc2 | monitorenter | 获得对象的锁,用于同步方法或同步块 |
0xc3 | monitorexit | 释放对象的锁,用于同步方法或同步块 |
0xc4 | wide | <待补充> |
0xc5 | multianewarray | 创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶 |
0xc6 | ifnull | 为null时跳转 |
0xc7 | ifnonnull | 不为null时跳转 |
0xc8 | goto_w | 无条件跳转(宽索引) |
0xc9 | jsr_w | 跳转至指定32位offset位置,并将jsr_w下一条指令地址压入栈顶 |