概念

字节码文件(.class)由JVM规范规定,主要包括魔数、JDK版本号(小版本和大版本)、常量池、类的访问标志、类的接口和父类信息、成员变量信息,方法信息等。

对于java中的数据类型,字节码中的对应关系为:

  1. byte->B,char->C,int->I,long->J,short->S,boolean->Z,folat->F,double->D
  2. 对于数组,以**[** 作为前缀,二位数组前缀为**[[,如:int[]表示为[I**
  3. 对于引用,以L开头,以分号结尾,将类的全限定名用**/**分割。如:Object类表示为Ljava/lang/Object;
  4. 对于方法签名,用**(方法参数类型列表)返回类型表示,其中方法参数列表要严格按照方法声明的顺序,返回类型如果是void,用V表示。如,Object getValue()表示为()Ljava/lang/Object;,Object mymethod(int i, double d, Object o)表示为(IDLjava/lang/Object;)Ljava/lang/Object;**

字节码中的数据类型包括两种,一种是无符号数的基本数据类型,其中字节长度u1表示一个字节,u2表示2个字节,u3表示3个字节。它们表示数值,索引和UTF8字面值。另一种是表,如常量池表,字段表,方法表,它们是基本数据类型的复合结构体。

描述符(Descriptor),指的是字段或者方法类型,它存放在constant pool中的CONSTANT_Utf8_info类型项中。分为字段描述符和方法描述符。

一个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_flag 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

通过javap -v Cat.class命令可以解析class文件,得到反编译文件,本文以Cat.java为例,源码和反编译文件如下:

 1package test.learn3;
 2// Cat.java
 3public class Cat {
 4private int age = 2;
 5
 6public int getAge() {
 7return age;
 8}
 9
10public void setAge(int age) {
11this.age = age;
12}
13}

反编译的内容如下:

 1D:\develop\workspace\MyTest\target\classes>javap -v test.learn3.Cat
 2Classfile /D:/develop/workspace/MyTest/target/classes/test/learn3/Cat.class
 3Last modified 2020-1-7; size 463 bytes
 4MD5 checksum 9c9b4378c58cb37e21cf1b918c3a64b2
 5Compiled from "Cat.java"
 6public class test.learn3.Cat
 7minor version: 0
 8major version: 52
 9flags: ACC_PUBLIC, ACC_SUPER
10Constant pool:
11#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
12#2 = Fieldref #3.#21 // test/learn3/Cat.age:I
13#3 = Class #22 // test/learn3/Cat
14#4 = Class #23 // java/lang/Object
15#5 = Utf8 age
16#6 = Utf8 I
17#7 = Utf8 <init>
18#8 = Utf8 ()V
19#9 = Utf8 Code
20#10 = Utf8 LineNumberTable
21#11 = Utf8 LocalVariableTable
22#12 = Utf8 this
23#13 = Utf8 Ltest/learn3/Cat;
24#14 = Utf8 getAge
25#15 = Utf8 ()I
26#16 = Utf8 setAge
27#17 = Utf8 (I)V
28#18 = Utf8 SourceFile
29#19 = Utf8 Cat.java
30#20 = NameAndType #7:#8 // "<init>":()V
31#21 = NameAndType #5:#6 // age:I
32#22 = Utf8 test/learn3/Cat
33#23 = Utf8 java/lang/Object
34{
35public test.learn3.Cat();
36descriptor: ()V
37flags: ACC_PUBLIC
38Code:
39stack=2, locals=1, args_size=1
400: aload_0
411: invokespecial #1 // Method java/lang/Object."<init>":()V
424: aload_0
435: iconst_2
446: putfield #2 // Field age:I
459: return
46LineNumberTable:
47line 3: 0
48line 4: 4
49LocalVariableTable:
50Start Length Slot Name Signature
510 10 0 this Ltest/learn3/Cat;
52
53public int getAge();
54descriptor: ()I
55flags: ACC_PUBLIC
56Code:
57stack=1, locals=1, args_size=1
580: aload_0
591: getfield #2 // Field age:I
604: ireturn
61LineNumberTable:
62line 7: 0
63LocalVariableTable:
64Start Length Slot Name Signature
650 5 0 this Ltest/learn3/Cat;
66
67public void setAge(int);
68descriptor: (I)V
69flags: ACC_PUBLIC
70Code:
71stack=2, locals=2, args_size=2
720: aload_0
731: iload_1
742: putfield #2 // Field age:I
755: return
76LineNumberTable:
77line 11: 0
78line 12: 5
79LocalVariableTable:
80Start Length Slot Name Signature
810 6 0 this Ltest/learn3/Cat;
820 6 1 age I
83}
84SourceFile: "Cat.java"

对应的十六进制数据为:

 1Offset:   00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   
 200000000: CA FE BA BE 00 00 00 34 00 18 0A 00 04 00 14 09 J~:>...4........
 300000010: 00 03 00 15 07 00 16 07 00 17 01 00 03 61 67 65 .............age
 400000020: 01 00 01 49 01 00 06 3C 69 6E 69 74 3E 01 00 03 ...I...<init>...
 500000030: 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E ()V...Code...Lin
 600000040: 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C eNumberTable...L
 700000050: 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C ocalVariableTabl
 800000060: 65 01 00 04 74 68 69 73 01 00 11 4C 74 65 73 74 e...this...Ltest
 900000070: 2F 6C 65 61 72 6E 33 2F 43 61 74 3B 01 00 06 67 /learn3/Cat;...g
1000000080: 65 74 41 67 65 01 00 03 28 29 49 01 00 06 73 65 etAge...()I...se
1100000090: 74 41 67 65 01 00 04 28 49 29 56 01 00 0A 53 6F tAge...(I)V...So
12000000a0: 75 72 63 65 46 69 6C 65 01 00 08 43 61 74 2E 6A urceFile...Cat.j
13000000b0: 61 76 61 0C 00 07 00 08 0C 00 05 00 06 01 00 0F ava.............
14000000c0: 74 65 73 74 2F 6C 65 61 72 6E 33 2F 43 61 74 01 test/learn3/Cat.
15000000d0: 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 ..java/lang/Obje
16000000e0: 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 05 ct.!............
17000000f0: 00 06 00 00 00 03 00 01 00 07 00 08 00 01 00 09 ................
1800000100: 00 00 00 38 00 02 00 01 00 00 00 0A 2A B7 00 01 ...8........*7..
1900000110: 2A 05 B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A *.5..1..........
2000000120: 00 02 00 00 00 03 00 04 00 04 00 0B 00 00 00 0C ................
2100000130: 00 01 00 00 00 0A 00 0C 00 0D 00 00 00 01 00 0E ................
2200000140: 00 0F 00 01 00 09 00 00 00 2F 00 01 00 01 00 00 ........./......
2300000150: 00 05 2A B4 00 02 AC 00 00 00 02 00 0A 00 00 00 ..*4..,.........
2400000160: 06 00 01 00 00 00 07 00 0B 00 00 00 0C 00 01 00 ................
2500000170: 00 00 05 00 0C 00 0D 00 00 00 01 00 10 00 11 00 ................
2600000180: 01 00 09 00 00 00 3E 00 02 00 02 00 00 00 06 2A ......>........*
2700000190: 1B B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 .5..1...........
28000001a0: 02 00 00 00 0B 00 05 00 0C 00 0B 00 00 00 16 00 ................
29000001b0: 02 00 00 00 06 00 0C 00 0D 00 00 00 00 00 06 00 ................
30000001c0: 05 00 06 00 01 00 01 00 12 00 00 00 02 00 13 ...............

下面逐一拆解上面这段二进制数据。

魔数

最开头的4个字节,它是固定值:0xCAFEBABE

版本号

次版本号是:0000 主版本号是:0034,十进制是52,表示采用的是jdk1.8

常量池

紧接着版本号后面的部分是常量池,常量池是字节码的资源库,并不只是类中常量。常量池由一个u2(2个字节)长度的常量池项目个数和常量池表构成的。常量池表的字节长度是变长的。

常量池中的元素,即常量池项目(item)从1开始,而是不从0开始,第0项目表示空引用(null),类中的变量什么都不指向。常量池项目的数据类型主要包括以下14种,每一个都是以单个字节的tag打头,用于标志这种元素的数据类型。

常量池项目的数据类型

本例中,常量池的长度是0018,也就是24项。紧接着是0A,对于十进制10,tag为10的类型是CONSTANT_Methodref_info,紧接着的4个字节是0004 0014,分别指向第4项和第20项索引

类访问标识

用于标识这个类的修饰符,是否为接口,是否为抽象类,是否为public等。访问标识说明表如下:

访问标志 标志值 含义
ACC_PUBLIC 0X0001 是否为public类型
ACC_FINAL 0X0010 是否被声明为final,只有类可设置
ACC_SUPER 0X0020 是否允许使用invokespecial字节码指令
ACC_INTERFACE 0X0200 标识为一个接口
ACC_ABSTRACT 0X0400 是否为abstract类型,对于接口或抽象类而言,此标志值为真,其他类值为假
ACC_SYNTHETIC 0X1000 标识这个类并非由用户代码产生的
ACC_ANNOTATION 0X2000 标识这是一个注解
ACC_ENUM 0X4000 标识这是一个枚举

在本例中,坐标为[000000e0, 02]和[000000e0, 03]这2个字节为0021,即为ACC_PUBLIC和ACC_SUPER的组合,代表为Public类型的类。

类、父类名称索引以及接口数量

紧接着的4个字节为0003 0004,分别标识this_class和super_class,分别引用了常量池中常量:

1#3 = Class #22 // test/learn3/Cat
2#4 = Class #23 // java/lang/Object

也就是当前类的全限定名称索引和父类的全限定名称索引。接着的两个字节00 00 (坐标为[000000e0, 08]和[000000e0, 09])表示接口数量为0个。

字段表

接口数量和接口信息之后的数据就是字段数量和字段表数组。字段数量(fields_count)表示声明的字段个数。字段表(fields)是字段表述信息结构体。

        ——**字段表**————
类型 名称 数量 备注
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_VOLATILE 0x0040 字段是否volatile
ACC_TRANSIENT 0x0080 字段是否transient
ACC_SYNTHETIC 0x1000 字段是否由编译器自动产生的
ACC_ENMU 0x4000 字段是否enmu

本例中,字段长度为1,字段数据为 ++00 02 00 05 00 06 00++。0002对应了字段标识ACC_PRIVATE,标识该字段为private,0005和0006对应于:

1#5 = Utf8 age
2#6 = Utf8 I

标识字段名称是age,字段类型是I,即整型。最后的一个字节00表示没有属性表。

方法表

方法表用于描述方法的名称,类型和属性等信息的结构体。

方法表结构

类型 名称 数量 备注
u2 access_flags 1 方法访问标志
u2 name_index 1 方法名称(对常量池的引用)
u2 descriptor_index 1 方法的描述符(对常量池的引用)
u2 attributes_count 1 attributes包含的项目数
attribute_info attributes[attributes_count] attributes_count

方法访问标志

标志名称 标志值 含义
ACC_PUBLIC 0x0001 方法是否为public
ACC_PRIVATE 0x0002 方法是否为private
ACC_PROTECTED 0x0004 方法是否为protected
ACC_STATIC 0x0008 方法是否为static
ACC_FINAL 0x0010 方法是否为final
ACC_SYNCHRONIZED 0x0020 方法是否为synchronized
ACC_BRIDGE 0x0040 方法是否是由编译器产生的 桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 方法是否为native
ACC_ABSTRACT 0x0400 方法是否为abstract
ACC_STRICTFP 0x0800 方法是否为strictfp
ACC_SYNTHETIC 0x1000 方法是否是由编译器自动产生的

本例中,从第000000f0行第04列到第0F列即为第一个方法:

100 03 00 01 00 07 00 08 00 01 00 09

0003表示方法数量,该类共有3个方法,接下来就是第一个方法,0001对应于ACC_PUBLIC,表示public,0007和0008表示方法名称索引和类型索引:

1#7 = Utf8 <init>
2#8 = Utf8 ()V

也就是说该方法为构造方法。接着0001表示只有一个属性,0009表示code属性。

属性表

在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量值
Deprecated 类、方法表、字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
SourceFile 类文件 源文件名称
Synthetic 类、方法表、字段表 标识方法或字段为编译器自动生成的

Code属性 Java方法里的代码被编译处理后,变为字节码指令存储在方法表的Code属性里,但并不是所有的方法表里都有Code属性,例如接口或抽象类中的方法就可能没有该属性。

Code属性数据结构:

类型 名称 含义
u2 attribute_name_index 属性名称索引
u4 attribute_length 属性长度
u2 max_stack 操作数栈深度的最大值
u2 max_locals 局部变量表所需的存储空间
u4 code_length 字节码长度
u1 code[code_length] 存储字节码指令的一系列字节流
u2 exception_table_length 异常表长度
exception_info exception_table
u2 attributes_count
attribute_info attributes[attributes_count]
可以看到Code属性数据结构里还包含有其他属性,主要有LineNumberTable、LocalVariableTable。

LineNumberTable

1LineNumberTable_attribute {
2    u2 attribute_name_index;        //属性名称索引
3    u4 attribute_length;            //属性长度
4    u2 line_number_table_length;
5    {   u2 start_pc;            //字节码行号
6        u2 line_number;         //java源码行号
7    } line_number_table[line_number_table_length];
8}

上面方法中code属性字节:

1 00 00 00 38 00 02 00 01 00 00 00 0A 2A B7 00 01 2A 05 B5 00 02 B1 00 00
2 00 02 00 0A

00000038表示code的属性长度为56个字节,0002表示栈的深度为2,0001表示局部变量数为1,0000000A表示方法代码指令长度为10个字节,即2A B7 00 01 2A 05 B5 00 02 B1。接着0000表示异常表的长度为0。

接着0002表示有2个属性,其中000A引用常量池的第10项:

1#10 = Utf8 LineNumberTable

参考上面LineNumberTable的数据结构可以继续分析。

code中2个属性信息如下截图: LineNumberTable: image

LocalVaribalTable: image