目录
1、源码及class文件:
1.1、源码
public class ByteCode {
private String userName;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
1.2、class文件
CAFE BABE 0000 0034 0019 0A00 0400 1509
0003 0016 0700 1707 0018 0100 0875 7365
724E 616D 6501 0012 4C6A 6176 612F 6C61
6E67 2F53 7472 696E 673B 0100 063C 696E
6974 3E01 0003 2829 5601 0004 436F 6465
0100 0F4C 696E 654E 756D 6265 7254 6162
6C65 0100 124C 6F63 616C 5661 7269 6162
6C65 5461 626C 6501 0004 7468 6973 0100
284C 636F 6D2F 6465 6D6F 2F7A 7379 6465
6D6F 2F6A 766D 2F62 7974 6563 6F64 652F
4279 7465 436F 6465 3B01 000B 6765 7455
7365 724E 616D 6501 0014 2829 4C6A 6176
612F 6C61 6E67 2F53 7472 696E 673B 0100
0B73 6574 5573 6572 4E61 6D65 0100 1528
4C6A 6176 612F 6C61 6E67 2F53 7472 696E
673B 2956 0100 104D 6574 686F 6450 6172
616D 6574 6572 7301 000A 536F 7572 6365
4669 6C65 0100 0D42 7974 6543 6F64 652E
6A61 7661 0C00 0700 080C 0005 0006 0100
2663 6F6D 2F64 656D 6F2F 7A73 7964 656D
6F2F 6A76 6D2F 6279 7465 636F 6465 2F42
7974 6543 6F64 6501 0010 6A61 7661 2F6C
616E 672F 4F62 6A65 6374 0021 0003 0004
0000 0001 0002 0005 0006 0000 0003 0001
0007 0008 0001 0009 0000 002F 0001 0001
0000 0005 2AB7 0001 B100 0000 0200 0A00
0000 0600 0100 0000 0A00 0B00 0000 0C00
0100 0000 0500 0C00 0D00 0000 0100 0E00
0F00 0100 0900 0000 2F00 0100 0100 0000
052A B400 02B0 0000 0002 000A 0000 0006
0001 0000 000F 000B 0000 000C 0001 0000
0005 000C 000D 0000 0001 0010 0011 0002
0009 0000 003E 0002 0002 0000 0006 2A2B
B500 02B1 0000 0002 000A 0000 000A 0002
0000 0013 0005 0014 000B 0000 0016 0002
0000 0006 000C 000D 0000 0000 0006 0005
0006 0001 0012 0000 0005 0100 0500 0000
0100 1300 0000 0200 14
2、阅读字节码方式及工具
首先推荐给大家两个IDEA查看字节码的插件:
- BinEd:可以直接以十六进制的方式打开class文件
- Jclasslib:对字节码提供了一个可视化的界面
上述两款插件可以让你在阅读字节码文件时基本没有什么阻碍,下图是.class文件、BinEd的二进制文件以及Jclasslib打开的字节码文件:
我们也可以使用原生的javap命令来查看.class文件,通过javap -v可以看到详细的字节码信息,示例如下:
//表示我们通过反编译的来源是哪个字节码文件
Classfile /Users/yyyz/dailywork/daily-study/target/classes/com/demo/zsydemo/jvm/bytecode/ByteCode.class
//最后的修改日期;文件大小
Last modified 2023-2-19; size 601 bytes
//文件MD5值
MD5 checksum 8c4a7e6ec68a6f213328018bf3c1e324
//.class文件是通过哪个文件编译过来的
Compiled from "ByteCode.java"
//字节码的详细信息
public class com.demo.zsydemo.jvm.bytecode.ByteCode
//次版本信息
minor version: 0
//主版本信息
major version: 52
//访问权限
flags: ACC_PUBLIC, ACC_SUPER
//常量池
Constant pool:
#1 = Methodref #4.#21 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#22 // com/demo/zsydemo/jvm/bytecode/ByteCode.userName:Ljava/lang/String;
#3 = Class #23 // com/demo/zsydemo/jvm/bytecode/ByteCode
#4 = Class #24 // java/lang/Object
#5 = Utf8 userName
#6 = Utf8 Ljava/lang/String;
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/demo/zsydemo/jvm/bytecode/ByteCode;
#14 = Utf8 getUserName
#15 = Utf8 ()Ljava/lang/String;
#16 = Utf8 setUserName
#17 = Utf8 (Ljava/lang/String;)V
#18 = Utf8 MethodParameters
#19 = Utf8 SourceFile
#20 = Utf8 ByteCode.java
#21 = NameAndType #7:#8 // "<init>":()V
#22 = NameAndType #5:#6 // userName:Ljava/lang/String;
#23 = Utf8 com/demo/zsydemo/jvm/bytecode/ByteCode
#24 = Utf8 java/lang/Object
{
//构造方法
public com.demo.zsydemo.jvm.bytecode.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 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/demo/zsydemo/jvm/bytecode/ByteCode;
//get方法
public java.lang.String getUserName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field userName:Ljava/lang/String;
4: areturn
LineNumberTable:
line 15: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/demo/zsydemo/jvm/bytecode/ByteCode;
//set方法
public void setUserName(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field userName:Ljava/lang/String;
5: return
LineNumberTable:
line 19: 0
line 20: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/demo/zsydemo/jvm/bytecode/ByteCode;
0 6 1 userName Ljava/lang/String;
MethodParameters:
Name Flags
userName
}
下面我们来完整的分析一下字节码文件。
3、字节码文件结构
通过上述的BinEd插件打开.class或通过其他文本编辑工具打开.class文件,如下:
3.1、class文件结构
类型 | 名称 | 描述 | 数量 |
u4 | magic_number | 魔数 | 1 |
u2 | major_version | 次版本号 | 1 |
u2 | main_version | 主版本号 | 1 |
u2 | constant_pool_count | 常量池个数 | 1 |
cp_info | constant_pool | 表 | constant_pool_count - 1 |
u2 | access_flag | 访问标记符号 | 1 |
u2 | this_class_name | 当前类名称 | 1 |
u2 | super_class_name | 父类名称 | 1 |
u2 | interfaces_count | 接口个数 | 1 |
u2 | interfaces | 接口名称 | interface_count |
u2 | fields_count | 字段个数 | 1 |
fields_info | fields | 字段表 | fields_count |
u2 | method_count | 方法个数 | 1 |
methods_info | methods | 方法表 | method_count |
u2 | attruibute_count | 附加属性个数 | 1 |
attruibute_info | attruibutes | 附加属性表 | attruibute_count |
4、剖析字节码文件
4.1、魔数
文件开头的四个字节是固定值位 CA FE BA BE
4.2、次版本号
两个字节00 00表示jdk的次版本号
4.3、主版本号
两个字节00 34表示jdk的主版本号,34对应的是十进制的52,52代表的是jdk1.8.x版本,51代表的是1.7.x版本依次类推,现在最新的jdk版本出到了jdk19,他对应的主版本号就是00 3F
4.4、常量池
占用两个字节,表示常量池中的个数0x19,转为十进制为25,通过上述工具查看到常量池的个数为24,为什么少了一个,因为常量池中第0位被jvm占用了表示为null,所以常量池索引都是从1开始的。
4.4.1、常量池结构表
u1、u2、u4、u8分别代表1个字节、2个字节、4个字节和8个字节的无符号数
常量 | 项 | 类型 | 描述 |
CONSTANT_Utf8_info | tag | u1 | 值为1,16进制即为0x01 |
length | u2 | UTF-8编码的字符串占用了字节数 | |
bytes | u1 | 长度为length的UTF-8编码的字符串 | |
CONSTANT_Integer_info | tag | u1 | 值为3,16进制即为0x03 |
bytes | u4 | 按照高位在前存储的int值 | |
CONSTANT_Float_info | tag | u1 | 值为4,16进制即为0x04 |
bytes | u4 | 按照高位在前存储的float值 | |
CONSTANT_Long_info | tag | u1 | 值为5,16进制即为0x05 |
bytes | u8 | 按照高位在前存储的long值 | |
CONSTANT_Double_info | tag | u1 | 值为6,16进制即为0x06 |
bytes | u8 | 按照高位在前存储的double值 | |
CONSTANT_Class_info | tag | u1 | 值为7,16进制即为0x07 |
index | u2 | 指向 全限定名常量 的索引 | |
CONSTANT_String_info | tag | u1 | 值为8,16进制即为0x08 |
index | u2 | 指向 字符串字面量 的索引 | |
CONSTANT_Fieldref_info | tag | u1 | 值为9,16进制即为0x09 |
index | u2 | 指向 声明字段 的 类或接口描述符CONSTANT_InterfaceMethodref_info 的索引项 | |
index | u2 | 指向 字段描述符CONSTANT_NameAndType_info 的索引项 | |
CONSTANT_Methodref_info | tag | u1 | 值为10,16进制即为0x0A |
index | u2 | 指向 声明方法 的 类描述符CONSTANT_Class_info 的索引项 | |
index | u2 | 指向 名称及类型描述符CONSTANT_NameAndType_info 的索引项 | |
CONSTANT_IntegerfaceMethodref_info | tag | u1 | 值为11,16进制即为0x0B |
index | u2 | 指向 声明方法 的 类描述符CONSTANT_Class_info 的索引项 | |
index | u2 | 指向 名称及类型描述符CONSTANT_NameAndType_info 的索引项 | |
CONSTANT_NameAndType_info | tag | u1 | 值为12,16进制即为0x0C |
index | u2 | 指向该字段或方法名称 常量项 的索引 | |
index | u2 | 指向该字段或方法 描述符常量项 的索引 |
4.4.2、常量池项细化分类
常量池可以看做Java class类的资源仓库(比如Java类定的方法和变量信息),我们后面的方法、类的信息的描述信息都是通过索引去常量池中获取的,常量池主要存放两种常量,一种字面量一种是符号引用
4.4.3、数据类型字段
在JVM底层基本参数类型和void类型都是通过大写的字符来表示的,对象类型是通过L加全类名表示的,这样既可以保证JVM能读懂class文件也可以压缩class文件大小
基础数据类型:
数据类型 | 表示字符 |
---|---|
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
boolean | Z |
void() | V |
对象类型:大写L加类全名
对象 | 表示字符 |
---|---|
Object | Ljava/lang/Object; |
String | Ljava/lang/String; |
...... |
数组类型:每一个维度都是“[”来表示:
int[] i ---->[I
String[][] strArr---->[[Ljava/lang/String;
Long[][][][][] longArr---->[[[[[Ljava/lang/Long;
4.4.4、常量池分析
通过上述常量池的结构和描述,我们可以依次将字节码文件中常量池部分分解出来:
第一个常量 0A 00 04 00 15
0A:代表常量类型为CONSTANT_Methodref_info
00 04:表示方法所在类,指向常量池的索引位置为#4,然后发现#4的位置常量类型为CONSTANT_Class_info,也是符号引用类型,指向常量池#24的位置,而#24的位置的常量池类型为UTF-8字面量型结构体值为:java/lang/Object
00 15:表示方法的描述符,指向常量池索引#21的位置,#21的位置的常量类型为CONSTANT_NameAndType_info类型,属于引用类型,指向常量池中#7和#8的位置,#7是UTF-8字面量类型,值为<init>为构造方法,#8也是UTF-8字面量类型,值为()V
综上所述,第一个常量是:java/lang/Object."<init>":()V
第二个常量 09 00 03 00 16
09:代表常量类型为CONSTANT_Fieldref_info
00 03:表示字段所在类,指向#3的位置,#3位置的类型为CONSTANT_Class_info,指向常量池#23的位置,而#23的位置的常量池类型为UTF-8字面量型结构体值为:com/demo/zsydemo/jvm/bytecode/ByteCode
00 16:表示字段的描述符,指向常量池索引#22的位置,#22位置的类型为CONSTANT_NameAndType_info,指向常量池中#5和#6的位置,都属于UTF-8字面量类型,#5值为userName,#6值为Ljava/lang/String;
综上所述,第二个常量是:com/demo/zsydemo/jvm/bytecode/ByteCode.userName:Ljava/lang/String;
下图是图解两个第一和第二个常量
简单分析两个,就不一一分析了
4.5、访问标记符
访问修饰符占用两个字节,紧跟在常量池后面,我们可以看到该class文件访问标识字节为0x0021,我们可以通过下面标识符手册查询对应访问权限
标识 | 值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | public修饰符 |
ACC_FINAL | 0x0010 | final修饰符 |
ACC_SUPER | 0x0020 | jdk1.2之后通过invokenonspecical调用指令调用父类方法,jdk1.2之前是invokenonsvirtual |
ACC_INTERFACE | 0x0200 | 标识是一个接口 |
ACC_ABSTRACT | 0x0400 | 表示是一个抽象类 |
ACC_SYNTHETIC | 0x1000 | 表示是动态生成的,没有源文件 |
ACC_ANNOTATION | 0x2000 | 表示是一个注解 |
ACC_ENUM | 0x4000 | 表示是一个枚举 |
ACC_PRIVATE | 0x0002 | private修饰符 |
0x0021我们在手册中没有查到对应的值,为什么呢?因为手册中并未穷举出所有的修饰符类型,而是通过上述这些标识进行位运算中的“或”运算得到的。
计算逻辑:0x0020二进制为100000,0x0001二进制为1,100000|1=100001转换为16进制即为0x0021
所以我们可以得到0x0021对应的即为ACC_PUBLIC, ACC_SUPER
4.6、类名称
接着是由两个字节表示的当前类名称,是一个常量池索引,该class文件索引值为0x0003,指向常量池#3的位置
4.7、父类名称
是由两个字节表示的当前类名称,也是一个常量池索引,该class文件索引值为0x0004,指向常量池#4的位置
4.8、实现接口个数
也是由两个字节表示当前类实现的接口个数,因为该类中没有实现接口,所以该位置值为0x0000
4.9、字段表信息分析
4.9.1、字段表结构
类型 | 名称 | 描述 | 数量 |
---|---|---|---|
u2 | access_flag | 权限修饰符 | 1 |
u2 | name_index | 字段名称索引 | 1 |
u2 | descript_index | 字段类型索引 | 1 |
u2 | attributes_count | 属性表个数 | 1 |
attribute_info | attruibutes | 属性表 | attributes_count |
4.9.2、字段分析
字段部分是有两部分组成由两个字节表示字段的个数,后面会使用上面数据结构构成每个字段,该class文件字段部分内容为:
00 01 00 02 00 05 00 06 00 00
下图是字段表的图解:
我们可以看到属性表个数为0,所以后面是没有属性表集合
4.10、方法表信息分析
4.10.1、方法表和属性表结构
类型 | 名称 | 描述 | 数量 |
u2 | attribute_name_index | 属性字段名称索引 | 1 |
u4 | attribute_length | 属性字段内容长度 | 1 |
u1 | info[attribute_length] | 属性字段内容 | 1 |
方法表结构和字段表结构一样,只是access_flag类型比字段的access_flag多,下面我们来分析方法表,首先是有两个字节表示方法的个数,从源码中可以看到我们再类中写了get和set方法,但是为什么方法个数是0x0003,因为还有一个默认的构造方法。所以方法个数为3。
4.10.2、方法表分析
第一个方法:
//为了方便阅读,先分割好
00 01 00 07 00 08 00 01
00 09 00 00 00 2F
00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 02 00 0A 00 00 00 06 00 01 00 00 00 0A 00 0B 00 00 00 0C 00 01 00 00 00 05 00 0C 00 0D 00 00
前八个字节00 01 00 07 00 08 00 01,由上述结构表,可以分析出:
- 00 01表示public修饰符
- 00 07表示字段名称,这个指向常量池#7的位置
- 00 08表示字段类型,也指向常量池#8的位置
- 00 01表示方法属性表个数,0x0001表示有一个属性表
接着就是方法表的属性表字段分析
00 09 00 00 00 2F中前两个字节00 09表示属性字段类型,是一个索引指向常量池#9位置,接着由四个字节表示属性内容长度,00 00 00 2F表示接下来有47个字节来表示属性字段内容。
接下来就是属性字段内容的分析
00 01 //最大操作数栈深度为1
00 01 //局部变量表个数为1
00 00 00 05 //表示指令长度
2A B7 00 01 B1 //指令内容 每个字节对应一个操作指令 详情可以查看JVM规范,通过IDEA插件JClasslib查看可以直接跳转到JVM官方文档所对应的指令描述
00 00 //异常信息表个数 0x0000表示方法没有抛出异常
00 02 //表示该属性字段的属性表个数
00 0A 00 00 00 06 00 01 00 00 00 0A //00 0A两个字节是索引,指向常量池中#10的位置,00 00 00 06四个字节表示该属性字段的长度,00 01两个字节表示有几对指令码和源码的映射关系,00 00 00 0A四个字节表示指令码映射的是第10行源码
00 0B 00 00 00 0C 00 01 00 00 00 05 00 0C 00 0D 00 00 //00 0B两个字节是索引,指向常量池中#11的位置,00 00 00 0C四个字节表示该属性字段的长度,00 01表示局部本地变量表个数,00 00表示这个局部变量的生命周期偏移量,00 05表示作用范围覆盖长度,00 0C是索引指向常量池#12的位置,00 0D也是索引指向常量池#13的位置。00 00是指这个局部变量在栈帧的局部变量表中变量槽的位置。
剩余两个方法也可以基于上面的分解步骤进行拆分,在此就不一一赘述了
4.11、附加属性
在字节码文件的最后是
00 01 00 13 00 00 00 02 00 14
下图是class附加属性的图解:
5、总结
Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以在下图中的数据项,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,全部都不允许改变。
字节码文件结构图如下:
Tips:图中只是展示了部分属性表,自己写的测试类文件比较简单,其他的属性表字段没有展示出来,如果想要了解其他内容,可以参考一下周志明写的那本《深入理解Java虚拟机》,本文中一些内容就是参考这本书的第三版中第6章的6.3小节。