前言
我们都知道,Java程序最终是转换成class文件执行在虚拟机上的,那么class文件是个怎样的结构,虚拟机又是如何处理去执行class文件里面的内容呢,这篇文章带你深入理解Java字节码中的结构。
1.Demo源码
首先,编写一个简单的Java源码:
package com.april.test;
public class Demo {
private int num = 1;
public int add() {
num = num + 2;
return num;
}
}
12345678910
这段代码很简单,只有一个成员变量num
和一个方法add()
。
2.字节码
要运行一段Java源码,必须先将源码转换为class文件,class文件就是编译器编译之后供虚拟机解释执行的二进制字节码文件,可以通过IDE工具
或者命令行
去将源码编译成class文件。这里我们使用命令行去操作,运行下面命令:
javac Demo.java
1
就会生成一个Demo.class
文件。
我们打开这个Demo.class文件看下。这里用到的是Notepad++
,需要安装一个HEX-Editor
插件。
3.class文件反编译java文件
在分析class文件之前,我们先来看下将这个Demo.class反编译回Demo.java的结果,如下图所示:
可以看到,回编译的源码比编写的代码多了一个空的构造函数
和this关键字
,为什么呢?先放下这个疑问,看完这篇分析,相信你就知道答案了。
4.字节码结构
从上面的字节码文件中我们可以看到,里面就是一堆的16进制字节。那么该如何解读呢?别急,我们先来看一张表:
类型 | 名称 | 说明 | 长度 |
---|---|---|---|
u4 | magic | 魔数,识别Class文件格式 | 4个字节 |
u2 | minor_version | 副版本号 | 2个字节 |
u2 | major_version | 主版本号 | 2个字节 |
u2 | constant_pool_count | 常量池计算器 | 2个字节 |
cp_info | constant_pool | 常量池 | n个字节 |
u2 | access_flags | 访问标志 | 2个字节 |
u2 | this_class | 类索引 | 2个字节 |
u2 | super_class | 父类索引 | 2个字节 |
u2 | interfaces_count | 接口计数器 | 2个字节 |
u2 | interfaces | 接口索引集合 | 2个字节 |
u2 | fields_count | 字段个数 | 2个字节 |
field_info | fields | 字段集合 | n个字节 |
u2 | methods_count | 方法计数器 | 2个字节 |
method_info | methods | 方法集合 | n个字节 |
u2 | attributes_count | 附加属性计数器 | 2个字节 |
attribute_info | attributes | 附加属性集合 | n个字节 |
这是一张Java字节码总的结构表,我们按照上面的顺序逐一进行解读就可以了。
首先,我们来说明一下:class文件只有两种数据类型:无符号数
和表
。如下表所示:
数据类型 | 定义 | 说明 |
---|---|---|
无符号数 | 无符号数可以用来描述数字、索引引用、数量值或按照utf-8编码构成的字符串值。 | 其中无符号数属于基本的数据类型。 以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节 |
表 | 表是由多个无符号数或其他表构成的复合数据结构。 | 所有的表都以“_info”结尾。 由于表没有固定长度,所以通常会在其前面加上个数说明。 |
实际上整个class文件就是一张表,其结构就是上面的表一了。
那么我们现在再来看表一中的类型那一列,也就很简单了:
类型 | 说明 | 长度 |
---|---|---|
u1 | 1个字节 | 1 |
u2 | 2个字节 | 2 |
u4 | 4个字节 | 4 |
u8 | 8个字节 | 8 |
cp_info | 常量表 | n |
field_info | 字段表 | n |
method_info | 方法表 | n |
attribute_info | 属性表 | n |
上面各种具体的表的数据结构后面会详细说明,这里暂且不表。
好了,现在我们开始对那一堆的16进制进行解读。
4.1 魔数
从上面的总的结构图中可以看到,开头的4个字节表示的是魔数,其值为:
嗯,其值为0XCAFE BABE
。CAFE BABE??What the fxxk?
好了,那么什么是魔数呢?魔数就是用来区分文件类型的一种标志,一般都是用文件的前几个字节来表示。比如0XCAFE BABE
表示的是class文件,那么为什么不是用文件名后缀来进行判断呢?因为文件名后缀容易被修改啊,所以为了保证文件的安全性,将文件类型写在文件内部可以保证不被篡改。
再来说说为什么class文件用的是CAFE BABE呢,看到这个大概你就懂了。
4.2 版本号
紧跟着魔数后面的4位就是版本号了,同样也是4个字节,其中前2个字节表示副版本号
,后2个字节
表示主版本号
。再来看看我们Demo字节码中的值:
前面两个字节是0x0000
,也就是其值为0;
后面两个字节是0x0034
,也就是其值为52.
所以上面的代码就是52.0版本来编译的,也就是jdk1.8.0
。
4.3 常量池
4.3.1 常量池容量计数器
接下来就是常量池了。由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值。Demo的值为:
其值为0x0013
,掐指一算,也就是19。
需要注意的是,这实际上只有18项常量。为什么呢?
通常我们写代码时都是从0开始的,但是这里的常量池却是从1开始,因为它把第0项常量空出来了。这是为了在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。
Class文件中只有常量池的容量计数是从1开始的,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的。
4.3.2 字面量和符号引用
在对这些常量解读前,我们需要搞清楚几个概念。
常量池主要存放两大类常量:字面量
和符号引用
。如下表:
常量 | 具体的常量 |
---|---|
字面量 | 文本字符串 |
声明为final的常量值 | |
符号引用 | 类和接口的全限定名 |
字段的名称和描述符 | |
方法的名称和描述符 |
4.3.2.1 全限定名
com/april/test/Demo
这个就是类的全限定名,仅仅是把包名的".“替换成”/",为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。
4.3.2.2 简单名称
简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的add()
方法和num
字段的简单名称分别是add
和num
。
4.3.2.3 描述符
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,详见下表:
标志符 | 含义 |
---|---|
B | 基本数据类型byte |
C | 基本数据类型char |
D | 基本数据类型double |
F | 基本数据类型float |
I | 基本数据类型int |
J | 基本数据类型long |
S | 基本数据类型short |
Z | 基本数据类型boolean |
V | 基本数据类型void |
L | 对象类型,如Ljava/lang/Object |
对于数组类型,每一维度将使用一个前置的[
字符来描述,如一个定义为java.lang.String[][]
类型的二维数组,将被记录为:[[Ljava/lang/String;
,,一个整型数组int[]
被记录为[I
。
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“( )”之内。如方法java.lang.String toString()
的描述符为( ) LJava/lang/String;
,方法int abc(int[] x, int y)
的描述符为([II) I
。
4.3.3 常量类型和结构
常量池中的每一项都是一个表,其项目类型共有14种,如下表格所示:
类型 | 标志 | 描述 |
---|---|---|
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_MothodType_info | 16 | 标志方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
这14种类型的结构各不相同,如下表格所示:
注:此表格的类型的单位不对,不是bit,应该是byte(字节)。后面的同理。
从上面的表格可以看到,虽然每一项的结构都各不相同,但是他们有个共同点,就是每一项的第一个字节都是一个标志位,标识这一项是哪种类型的常量。
4.3.4 常量解读
好了,我们进入这18项常量的解读,首先是第一个常量,看下它的标志位是啥:
其值为0x0a
,即10,查上面的表格可知,其对应的项目类型为CONSTANT_Methodref_info
,即类中方法的符号引用。其结构为:
即后面4个字节都是它的内容,分别为两个索引项:
其中前两位的值为0x0004
,即4,指向常量池第4项的索引;
后两位的值为0x000f
,即15,指向常量池第15项的索引。
至此,第一个常量就解读完毕了。
我们再来看下第二个常量:
其标志位的值为0x09
,即9,查上面的表格可知