一 什么是字节码
java中通过编译, 会将我们程序员编写的文件转换成为一种 .class
结尾的文件,我们称之为字节码文件.它是一种二进制文件(很明显是给计算机看的文件),是Java虚拟机中运行的文件.
1.1 如何在IDEA中查看标准的,未经过IDEA反编译过的 .calss文件
(1) javap
IDEA中在 Terminal 终端中,我们通过 javap 命令来查看某个java文件的字节码文件
(2) javap -c
javap -c
能够将字节码文件中助记符呈现出来,能够让我们更加详细的看到字节码文件的原貌
(3) javap -verbose
使用 javap -verbose
命令分析一个字节码文件时, 将会分析该字节码文件的魔数,版本号,类信息,类的构造方法,类中的方法信息,类变量与成员变量等信息;
1.2 Java字节码整体结构
字节数 | 名称 | 含义 |
---|---|---|
4个字节 | Magic Number | 魔数,值为0xCAFEBABE, 是Java的创始人 James Gosling 制定 |
2+2个字节 | Version | 包括minor_version和major_version; minor_version:1.1(45),1.2(46)…1.8(52),指令多年不变,但是版本号每次都发生变化 |
2+n个字节 | Constant Pool | 包括字符串常量和数值常量等 |
2个字节 | This Class Name | 当前类的名字 |
2个字节 | Super Class Name | 父类的名字 |
2+n个字节 | Interfaces | 接口相关的信息(2部分组成:前两个字节表示有几个接口,后n个字节表示具体的每一个接口) |
2+n个字节 | Fields | 当前类的成员变量信息,组成同上 |
2+n个字节 | Methods | 当前类的方法的信息 |
2+n个字节 | Attributes | 当前类的附加的属性 |
普及两个概念
- 字节数据直接量
这是基本的数据类型, 共细分为 u1, u2, u4, u8四种, 分别代表连续的1个字节,2个字节,4个字节,8个字节组成的整体数据;
- 表(数组)
表是由多个基本数据或其他表,按照既定顺序组成的大数据集合. 表是有结构的, 它的结构体现在: 组成表的分析所在的位置和顺序都是已经严格定义好的;
1.3 通过工具WinHex打开字节码文件,查看16进制文件内容
1.4 Java字节码结构解读
- 魔数
所有的
.class
字节码文件的前4个字节都是魔数, 魔数值为固定值:0xCAFEBABE(cafe babe)
- 版本信息
魔数之后的4个字节为版本信息, 前两个字节表示 minor version(次版本号), 后两个字节表示 major_version(主版本号). 这里 1.3中4,5,6,7 四个字节表示版本号,为
00 00 00 34
,换算成10进制,表示次版本号(0000) 为0, 主版本号(0034)为52(52对应java版本为1.8),所以该文件的Java版本号为:1.8.0(这个在1.2的javap -verbose
编译的信息中可以看到)
- 常量池(Constant Pool)
紧接着主版本号之后的就是常量池入口. 一个Java类中定义的很多信息都是由常量池来维护和描述的. 可以将常量池看做是 class文件的资源仓库. 比如说 Java类中定义的方法与变量信息. 都是存储在常量池中. 常量池中主要存储两类常量: 字面量与符号引用. 字面量如文本字符串, java中声明为final的常量值等, 而符号引用如类和接口的全限定名,字段的名称和描述符,方法的名称和描述符等.
每个常量其结构为, 1(个字节) + n(个字节), 表示 常量类型 + 具体常量内容
class字节码文件的 16进制文件中 2位 1字节
常量类型根据下图Class文件结构中常量池中11中数据类型的结构总表来确定, 每个数据类型的结构都有明确标出
- 常量池的总体结构
Java类所对应的常量池主要由常量池数量与常量池数组(常量表)这两部分共同构成. 常量池数量紧跟在主版本号后面,占据2个字节; 常量池数组则紧跟在常量池数量之后. 常量池数组与一般的数组不同的是, 常量池数组中不同的元素的类型,结构都是不同的,长度当然也就是不同; 但是, 每一种元素的第一个数据都是一个 u1 类型,该字节是个标志位,兼具一个字节. JVM在解析常量池时, 会根据这个 u1 类型来获取元素的具体类型. 指的注意的是,常量池数组中元素的个数=常量池数-1(其中0暂时不使用),目的是满足某些常量池引值的数据在特定情况下需要表达"不引用任何一个常量池"的含义;根本原因就在于索引为0 也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值,所以常量池的索引从1而非0开始;(1.2图示中从#1开始就是这么个意思)
举个栗子吧,分析一下常量池的第一个常量
具体的字节码内容我们在 1.3通过 winHex工具打开,我们 只分析第一个常量,后面的都是一样的
上图我们分别标记了字节码表的前15个字节,共分成4各部分, 分别表示为 魔数 主次版本号 常量池数量 第一个常量池信息(这个我是根据上图 “Class文件结构中常量池中11中数据类型的结构总表” 得到的),因此我们只需要分析字节码0A 0004 0015
根据上面所述,第一个字节0A
(换算成10进制为10),是该常量在 “常量池中11中数据类型的结构总表” 对应的一个类型, 为CONSTANT_Methodref_info
,其包含三个部分,如下表,我们直接将解读内容也列举出来
名称 | 对应字节码 | 解读 |
---|---|---|
tag(u1即占一个字节) | 0A | 这个值需要去结构总表中找,即我们已经找到的CONSTANT_Methodref_info |
index(u2)具体含义看"结构总表" | 0004 | 对应常量池内容 java/lang/Object |
index(u2) | 0015(换算成10进制为 21) | 对应常量池内容 <init> ()V |
表中说的常量池, 就是我们在 1.2中通过
javap -verbose
编译字节码文件,得到的内容中的 Constant pool里面的数据
解读第一个常量,就是
Object
类的构造方法. 因为<init>
的意思就是构造函数而()V
意思是不接受任何参数,无返回值; 结合一下,就是无参构造方法
其它常量池常量如上炮制即可
- 描述信息
在 JVM 规范中,每个变量/字段都有描述信息. 描述信息主要的作用是描述字段的数据类型,方法参数列表(包括数量,类型与顺序)与返回值. 根据描述符规则, 基本数据类型和代表无返回值的 void 类型都用一个大写字符 V 来表示, 对象类型则使用字符 L加对象的全限定名来表示. 为了压缩字节码文件的体积,对于基本数据类型, JVM都只使用一个大写字母来表示, 如下所示 👇
B - byte, C - char, D - duble, F - float, I - int, J - long, S - short, Z - boolean, V - void, L - 对象类型,如 Ljava/lang/String
- 数组类型表示方式
对于数组类型来说,每一个维度使用一个前置的
[
来表示,如int[]
被记录为[I
,String[][]
被记录为[[Ljava/lang/String
- 描述符描述方法
用描述符描述方法时, 按照先参数列表, 后返回值的顺序来描述, 参数列表按照参数的严格顺序放在 一组
()
之内,如方法:String getRealnamebyIdAndNickname(int id, String name)
的描述符为:(I,Ljava/lang/String;)Ljava/lang/String
- Access_Flag 访问标志
访问标志信息包括该 Class文件是类还是接口,是否被定义为
public
,是否是abstract
,如果是类,是否声明成final
下图为JVM规范预定义的attribute
访问标志紧跟常量池后面, 有两个字节,
我们可以看到访问标志两个字节是
0021
,可是根据图片 Table 4.1. Class access and property modifiers 我们判断 Test1 的访问标志应该为0x0001
因为我们的类是public
类型的,那为什么是0021
呢,原因其实很简单 👇
因为:0x0021是0x0020和0x0001的并集,表示 ACC_PUBLIC与ACC_SUPER
- 当前类的名字(this class name)
占两个字节
跟在 Access_Flag后面的就是 this class name 我们可以看到数值是0003
,这个时候我们根据javap -verbose
编译结果去看常量池#3
的值,发现它引用了#23
(看#3
的注释就很清晰了),很明显#23
的值为该类的名称
- 父类的名称(super class name)
占两个字节
跟在 this class name后面,值为0004
,具体解读方式跟this class name一样
- interfaces 相关接口信息
interfaces由两部分组成(2+n个字节), 前两个字节表示接口的个数,我们看到值为
0000
,就说明没有接口实现,后面的接口描述部分则不存在;
后续补充一个实现了接口的字节码
- 当前类的成员变量信息
2+n个字节, 由两部分组成
fields字段表示用于描述类和接口中声明的变量. 这里的字段包含了类别变量以及实例变量, 但是不包括方法内部声明的局部变量.
前两个字节表示字段的个数,0001
,表示当前类有一个字段;
后面的部分比较复杂,单独分析如下👇
第二部分(字段表(字段数组)),每一个字段都有自己的一个组成信息,包括以下
类型 | 名称 | 数量 |
---|---|---|
u2(表示两个字节) | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
根据上表我们来结合16进制文件和javap -verbose
信息得到如下结果
名称 | 字节 | 对应verbose信息 |
---|---|---|
access_flags | 0002 | private |
name_index | 0005 | a |
descriptor_index | 0006 | I |
attributes_count | 0000 | 说明没有属性 |
attributes | 就不会出现 |
以上表格内容,解读下来就是,
com.turnsole.myjvm.mycalss.Test1
类中的名称为a
的int
类型的变量
1.5 字节码解读之 Methods
注: 上面Fields 分析是在16进制文件
00000288
行的00060000
结束的
因为 Methods 内容较多,我们单独拿出来做分析
方法和变量一样也是由两部分组成, 头两个字节表示的是当前类的方法的个数(字节码为
0003
,表示总计有三个方法), 后面是方法的 方法表(方法数组),有每一个方法的具体信息,method_info
我们看下 method 在字节码中具体的结构
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index(名字的索引,指向常量池) | 1 |
u2 | descriptor_index(描述的索引,指向常量池) | 1 |
u2 | attributes_count(属性个数) | 1 |
attribute_info | attributes | attributes_count |
根据上句表格,结合ACCESS_FLAGS图,常量池数据图,我们来简单理解一下method在在字节码中的内容
名称 | 字节数值 | 对应数值 |
---|---|---|
access_flags | 0001 | public |
name_index | 0007 | <init> (注意只要是init,就说明这个方法是构造方法) |
descriptor_index | 0008 | ()V (表示不需要任何参数,并且无返回值) |
attributes_count | 0001 | 属性个数总计一个 |
attribute_info | 下面解读👇 |
attribute_info的结构如下:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
方法的每个属性都是一个attribute_info结构
名称 | 字节数值 | 对应数值 |
---|---|---|
attribute_name_index 属性名索引(对应常量池) | 0009 | Code |
attribute_length | 0000 002F(换算成10进制,值为47) | 表示后面47个字节为该方法的具体执行代码,即该属性的值(下图中选中的字节) |
info | 0001 |
JVM 预定义了部分 attribute, 但是编译器自己也可以实现自己的 attribute 写入 class 文件里,供运行时使用;
不同的 attribute 通过 attribute_name_index 来区分
1.5.1 说一下Code
Code attribute 的作用是保存该方法的结构,下图为所对应的字节码构成
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attribute_count;
attribute_info {
lineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{
u2 start_pc;
u2 line_number;
}line_number_table[line_number_table_length];
}
};
}
- attribute_length
表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段,即不包含上图中的
0009
和0000002F
,也就是我们截取的字节码文件中蓝色选中的部分的总长度
- max_stack
表示这个方法运行的任何时刻所能达到的操作数栈的最大深度, 占2个字节
示例 值为
0001
- max_locals
表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量,占2个字节
示例 值为
0001
- code_length
表示该方法所包含的字节码的字节数以及具体的指令码,具体字节码即是该方法被调用时,虚拟机所执行的字节码,占4个字节
示例 值为
00000005
换算为十进制值也是5,也就说后面的5个字节表示当前方法的具体执行内容,每个字节都有与之匹配的助记符, 我们借助 下面的 jclasslib 工具 将会更好做分析
- exception_table
这里存放的是处理异常的信息; 每个exception_table 都是由 start_pc, end_pc, handler_pc, catch_type 组成
- start_pc 和 end_pc
表示所在数组中的从 start_pc 到 end_pc 处(包含 start_pc, 不包含 end_pc) 的指令抛出的异常会由这个表项来处理
- handler_pc
表示处理异常的代码的开始处
- catch_type
表示会被处理的异常类型, 它指向常量池里的一个异常类. 当catch_type为0时, 表示处理所有的异常
具体分析方式是一样的,这里就不再挨个分析了
1.6 jclasslib工具 验证以上分析结果是否正确
jclasslib 工具, IDEA中已经做了集成,我们只需要把该插件下载下来就好
重启IDEA之后,如下图
是不是很清晰的展示出了字节码文件的全部信息呢. 6到飞起~~~
1.7 结合工具 jclasslib 分析一个 method
下面我们结合前面学习的东西以及工具 jclasslib 来分析 方法表(methods)中 第一个 method
我们在1.5 说一下code
中, 已经找到了这第一个 method 所对应的字节码 👇
2A B7 00 01 B1
,结合我们的工具,得到的可视化信息或者 javap -verbose
中的信息
以上第一张图为工具可视化图, 第二张为javap -verbose
编译所得,结果都是一样的,但是使用工具我们可以快速定位到, Oracle官网中对助记符的文档详情中. 以下我们就对我们要解读的第一个 method 中所使用的 aload_0
,invokespecial
,return
三个助记符做一个学习.
注意,如果通过工具快速链接打不开的话,不妨使用这个链接,然后自己找~~~
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
如果阅读英文文档比较吃力,如我, 那可以参考下面的链接
https://www.cnblogs.com/cwane/p/6097838.html
aload_0
对应十六进制数为
2A
,表示 把局部变量第一个引用型局部变量推到操作数栈
invokespecial
对应十六进制数为
B7
,表示 调用父类构造方法,实例初始化方法,私有方法
B7
后面两个字节0001
对应常量池#1 = Methodref
在截图中就能看出// java/lang/Object."<init>":()V
,表示当前类的父类构造器的内容
return
对应十六进制数为
B1
,表示 从当前方法返回voiid
继续往下分析
紧跟在code
后面的是exception_table_length
,占据 2个字节, 示例值为 0000
,也就是没有内容
再下面就是 attribute_count
, 占据2个字节, 示例值为0002
, 表示该方法有两个属性(切记莫和类的成员变量混淆,这里的属性是编译器自动生成的)
继续查看class字节码后面两个字节000A
,对应常量池中#10
,值为LineNumberTable
(表示字节码行号与源代码行号的关系),这是第一个属性的名字
LineNumberTable
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{
u2 start_pc;
u2 line_number;
}line_number_table[line_number_table_length];
}
直接上分析表
属性名 | 字节值 | 解读 |
---|---|---|
attribute_name_index(占2字节),值在常量池中 | 000A | LineNumberTable |
attribute_length(占4个字节) | 00 00 00 06 | 意思就是这个属性占字节码中的6位字节,也就是说后面6个字节就是这个属性的内容 |
line_number_table_length(占2个字节)表示当前属性有几对映射 | 0001 | 意思就是有一对映射(每个映射占2个字节) |
start_pc(占2个字节) 字节码偏移量 | 0000 | 偏移量为0 |
line_number(占2个字节) 源代码行号 | 000D | 行号为13 |
jclasslib工具信息验证一下
源代码
JVM会在上图中圈红的部分 生成 这一个属性值,提前剧透 是
this
继续往下分析
LineNumberTable
结束之后, 后面两个字节000B
就是我们要分析的第二个属性,在常量池中的值为LocalVariableTable
(局部变量表)
LocalVariableTable
局部变量表的结构 与 行号表 的结构差不多
首先
前4个字节0000000C
,表示当前属性值所占字节码的长度,值为10进制的12
后面12个字节码为👇
然後
前2个字节0001
,表示局部变量的个数,此处为1
再后
后面4个字节,0000
和0005
,就表示局部变量的开始位置和结束位置,此处值为0和5
然后
后面一个字节,00
表示局部变量的索引, 此处值为0
再然后
后面一个字节0C
表示该局部变量对应常量池中的值,对应常量池值为#12
,即值为this
,即当前对象
再再然后
后面2个字节000D
,表示对该局部变量的描述,值也是在常量池中寻找,#13
,值为Lcom/turnsole/myjvm/mycalss/Test1
意思就是 this
表示上面这个对象
最后
最后两个字节0000
,主要是用来做校验检查的,这里没找到具体的资料…
思考:this这个局部变量为什么会在无参构造函数中呢?
从字节码角度来说,如果类中的方法是非静态方法,编译器会将this作为第一个参数给隐式的传进来
1.8 最后一部分,整个字节码文件的attrubite
信息
直接上分析表
类型 | 字节码值 | 解读 |
---|---|---|
attribute_count(占2个字节)属性个数 | 0001 | 一共有1个属性 |
attribute_name(占两个字节),常量池中寻找对应值 | 0013 | #19 SourceFile (源文件) |
attribute_length(占4个字节),属性长度,即后面多少字节码代表该属性信息 | 000002 | 后面2个字节表示该属性信息 |
attribute_dec(n个字节)属性描述信息 | 0014 | 常量池中对应 #20 Test1.java(源文件名称) |
以上
END