1

对一个class文件的字节码进行逐行的分析是理解class文件结构的最佳方式。但是往往复杂的二进制字节码会让人望而却步,或者只有仔细一点点盯着才能保证不花眼。本文的目的在于尽可能完整地拆解JVM的Class字节码并将其分块分析,最终得到的图解结构希望可以帮助到你。

本文参考自来自周志明《深入理解Java虚拟机(第2版)》,拓展内容建议读者可以阅读下这本书。

根据这个简单的例子来说明

以下的例子作为最简单的一个java程序,通过javac执行编译,javap来查看它的反编译结果,当然我们还会更刨根问底地直接使用二进制编辑器查看class文件的二进制字节排布。

-w881

> javap -v Test
Classfile /Users/jinhaoplus/Desktop/Test.class
  Last modified 2018-8-12; size 285 bytes
  MD5 checksum eac8f02f8ad176b09bfd89cf15e2ed3d
  Compiled from "Test.java"
public class top.jinhaoplus.demo.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // top/jinhaoplus/demo/Test.m:I
   #3 = Class              #17            // top/jinhaoplus/demo/Test
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               Test.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // m:I
  #17 = Utf8               top/jinhaoplus/demo/Test
  #18 = Utf8               java/lang/Object
{
  public int m;
    descriptor: I
    flags: ACC_PUBLIC

  public top.jinhaoplus.demo.Test();
    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 3: 0

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 6: 0
}
SourceFile: "Test.java"

-w972

图解概况

如上的字节码阅读起来有诸多障碍,因此我们把上面的字节码按照字节码规范定义的class结构分区为不同的颜色块,不同的分区颜色说明这个区域对应着class结构中的不同区域定义,表示一个整体概念的字节码在图中显示为同一行上:

class-format

下面的图是class文件结构的思维导图说明,可以跟上述的实际的一个class的分区作简单的对照:

-w578

详细解释一下class文件的每个分区

下面详细解释一下class文件的每个分区,括号内的数字表示当前区的占位情况,u是字节的意思,如u4表示占4个字节的空间,对应到图中就是4个方格。

1. magic

magic(u4):魔数,class文件的标识开头。

CAFEBABE是固定的JVM Class的魔数,也可以认为是众所周知的Java咖啡Logo的由来。

-w218

2. version

version:class版本,主次版本合起来即可确定版本号。

  • 2.1 minor_version(u2):次版本
  • 2.2 major_version(u2):主版本

Class文件的版本为次版本0X0000、主版本0X0034,对应的是10进制的52.0。说明此Class是在JDK_VERSION=52.0(JDK1.8)的编译器中生成的,同时又可以被版本在JDK_VERSION=52.0及以上的虚拟机上执行(JVM保持了向下兼容性,但是拒绝执行超过它的版本号的Class字节码)。

-w183

3. 常量池:注意是本处的常量池指class字节码中的常量池而非JVM中的常量池(但后者中的数据其实是加载于前者)。

3.1 constant_pool_count

constant_pool_count(u2):常量池大小,定义了常量池中保存的常量个数(准确说常量个数=constant_pool_count-1)。

0X0013表示constant_pool_count=19,常量池中保存的常量个数=18(编号为#1~#18)。

-w213

3.2 constant_pool

constant_pool(constant_pool_count-1个constant_pool_info):实际保存的常量,编号从1开始(将0位留空有特殊考量)。

常量有多种种类,我们这里只提一下我们的Class文件里涉及到的具体的类型。

-w991

3.2.1 CONSTANT_Utf8_info

由utf-8编码的二进制串,其字节码格式为

类型 名称 数量
u1 tag 1
u2 length 1
u1 bytes length

其中的tag=0X01即为CONSTANT_Utf8_info类型常量的标识。我们Class字节码中的#5#6#7#8#9#10#11#12#13#14#17#18都是CONSTANT_Utf8_info常量,因为它们的首位tag=0X01(橘色列),通过utf-8解码这些常量指定长度的二进制串可以得出下面的结果,比如#5号常量length=1(10进制的0X0001),而bytes为0X6D,utf-8解码后就是字符串m,同理可以得到这些二进制串的值(这就是javap反编译出结果的原理,可以参照javap得到的结果对照一下):

#5               m
#6               I
#7               <init>
#8               ()V
#9               Code
#10              LineNumberTable
#11              inc
#12              ()I
#13              SourceFile
#14              Test.java
#17              top/jinhaoplus/demo/Test
#18              java/lang/Object
3.2.2 CONSTANT_Class_info

类常量,其字节码格式为

类型 名称 数量
u1 tag 1
u2 index 1

其中的tag=0X07即为CONSTANT_Class_info类型常量的标识,index指向了常量池中类的全限定名的索引序号。

我们Class字节码中的#3#4CONSTANT_Class_info类型的类常量,它们的首位tag=0X07(橘色列),通过查找常量池中它们指向的索引序号,我们可以得出这两个类的全限定名:

#3              #17            // top/jinhaoplus/demo/Test
#4              #18            // java/lang/Object
3.2.3 CONSTANT_NameAndType_info

字段或方法的名称和类型常量,其字节码格式为

类型 名称 数量
u1 tag 1
u2 index 1
u2 index 1

其中的tag=0X0C即为CONSTANT_NameAndType_info类型常量的标识,第一个index指向了字段或方法名称在常量池中的索引序号,第二个index指向了字段或方法的描述符在常量池中的索引序号。

字段的描述符就是简单的字段类型,Class文件中的类型为了节省空间进行了简化:如基本类型int->Idouble->D,引用类型java/lang/Object -> Ljava/lang/Object

我们Class字节码中的#15#16CONSTANT_NameAndType_info类型的类常量,它们的首位tag=0X0C(橘色列),通过查找常量池中它们两个指向的索引序号,我们可以得出常量#15的名称为#7号常量即<init>,类型为#8号常量即()V。同理可以得到#16的意思。

#15        #7:#8          // "<init>":()V
#16        #5:#6          // m:I
3.2.4 CONSTANT_Fieldref_info

字段引用常量,其字节码格式为

类型 名称 数量
u1 tag 1
u2 index 1
u2 index 1

其中的tag=0X09即为CONSTANT_Fieldref_info类型常量的标识,第一个index指向了声明字段的类或接口的CONSTANT_Class_info常量在常量池中的索引序号,第二个index指向了字段的名称和类型信息CONSTANT_NameAndType_info常量在常量池中的索引序号。
我们Class字节码中的#2CONSTANT_Fieldref_info类型的类常量,它们的首位tag=0X09(橘色列),通过查找常量池中它指向的索引序号,我们可以得出这个字段的声明类的是top/jinhaoplus/demo/Test,字段的名称是m,类型是I(即int,Class将类型全称映射到成了单字母)。

#2           #3.#16         // top/jinhaoplus/demo/Test.m:I
3.2.5 CONSTANT_Methodref_info

方法引用常量,其字节码格式为

类型 名称 数量
u1 tag 1
u2 index 1
u2 index 1

其中的tag=0X0A即为CONSTANT_Methodref_info类型常量的标识,第一个index指向了声明方法的类或接口的CONSTANT_Class_info常量在常量池中的索引序号,第二个index指向了方法的名称和类型信息CONSTANT_NameAndType_info常量在常量池中的索引序号。
我们Class字节码中的#1CONSTANT_Fieldref_info类型的类常量,它们的首位tag=0X0A(橘色列),通过查找常量池中它指向的索引序号,我们可以得出这个方法的声明类是java/lang/Object,方法的名称是<init>,类型是()V(即无入参返回void类型的方法)。

#1          #4.#15         // java/lang/Object."<init>":()V

至此我们得到了这个Class中的常量池中全部的常量的含义。这些常量将被下面的其他部分引用到。

4.类信息:

4.1 access_flag

access_flag(u2):说明这个类或接口的访问标志,如private/public/interface/abstract/annotation/enum等,总之是说明了这个类的特征。以不同的特征给出特征位的方式来设置这个u2大小的区域。
如本Class的0X0021实际代表了特征位信息是0000000000110001,即ACC_SUPER|ACC_PUBLIC,表示它是public的class(ACC_SUPER是JDK1.0.2后的默认设置项)。

-w152

4.2 this_class

this_class(u2):说明本类的类索引,0X0003说明本类索引在常量池中的序号为3,上面常量池的分析可以看到本类的全限定名是top/jinhaoplus/demo/Test

-w149

4.3 super_class

super_class(u2):说明父类的类索引,0X0004说明父类索引在常量池中的序号为4,上面常量池的分析可以看到父类的全限定名是java/lang/Object(这也就是所有Java类的父类都是Object的原因,即使没有明确写出来编译后的Class文件中也会将这个父类声明定义出来)。

-w152

4.4 interface_info

4.4.1 interface_count(u2):说明实现的接口数量,0X0000说明本类没有实现接口,因此不再有接下来的interface信息。

-w172

4.4.2 interface(interface_count个u2):说明接口的类索引。

5.字段信息

5.1 field_count(u2):字段数量

-w153

5.2 field_info(field_count个field_info):字段信息,字段表的结构如下:

类型 名称 数量
u2 access_flag 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

-w324

5.2.1 access_flag(u2)用以记录字段的特征。

比如private/public/protected/static/final/volatile,以不同的特征给出特征位的方式来设置这个u2大小的区域。我们的Class中的0X0001(橙色列)实际代表了特征位信息是0000000000000001,即字段的特征是ACC_PUBLIC(public字段)。

5.2.2 name_index(u2)是字段名在常量池中的索引序号。

我们的Class中的0X0005(蓝色列)指向的常量池中的#5号常量即m

5.2.3 descriptor_index(u2)是字段描述符在常量池中的索引序号。

我们的Class中的0X0006(青色列)指向的常量池中的#6号常量即I

5.2.4 字段的属性表是本字段的属性表:
  • 5.2.4.1 attributes_count(u2):字段属性表的属性数量,我们的Class中的0X0000表示本字段无额外的属性表信息。
  • 5.2.4.1 attributes(attributes_count个attribute_info):字段属性表的属性信息,字段属性有自己定义的结构,字段中主要使用的属性包括ConstantValue(final修饰的常量值作为字段的值)、Depreciated(@Depreciated修饰的字段表示弃用)、Signature(泛型参数记录的泛型签名信息,否则编译后擦除类型就无法溯源了)等,他们都有各自定义的结构。

6.方法信息

6.1 method_count

method_count(u2):方法数量
我们的Class这个区的0X0002表示这个类有两个方法。

-w163

6.2 method_info

method_info(method_count个method_info):方法信息,方法表的结构如下:

类型 名称 数量
u2 access_flag 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

-w1271

6.2.1 access_flag(u2)用以记录方法的特征。

比如private/public/protected/static/final/synchronized,以不同的特征给出特征位的方式来设置这个u2大小的区域。我们的Class中两个方法的这个区域的0X0001(橙色列)实际代表了特征位信息是0000000000000001,即它们的特征都是ACC_PUBLIC(public方法)。

6.2.2 name_index(u2)是方法名在常量池中的索引序号。

我们的Class中,method_#10X0005(蓝色列)指向的常量池中的#7号常量即<init>,而。method_#20X000B(蓝色列)指向的常量池中的#11号常量即inc

6.2.3 descriptor_index(u2)是方法描述符在常量池中的索引序号。

我们的Class中,method_#10X0008(青色列)指向的常量池中的#8号常量即()V,而。method_#20X000C(青色列)指向的常量池中的#12号常量即()I

6.2.4 字段的属性表是该方法的属性表:
  • 6.2.4.1 attributes_count(u2):方法属性表的属性数量。
  • 6.2.4.2 attributes(attributes_count个attribute_info):方法属性表的属性信息,方法属性有自己定义的结构,方法中主要使用的属性包括最重要的Code(方法的字节码指令,没有方法执行体的接口和抽象类是没有这个属性的)、Exceptions(声明方法抛出的异常)、Depreciated(@Depreciated修饰的方法表示弃用)、Signature(泛型参数记录的泛型签名信息)等,他们都有各自定义的结构。这里我们具体来看一下最重要的Code属性。
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_table_length
u2 attribute_count 1
attribute_info attributes attribute_count
  • a. attribute_name_index(u2):属性名在常量池中的索引序号,Code属性最终找到的常量肯定是Code
  • b. attribute_length(u4):该属性的长度。
  • c. max_stack(u2):该方法的操作数栈最大深度。
  • d. max_locals(u2):该方法的局部变量表的大小。
  • e. code_length(u4):字节码指令的大小
  • f. code(exception_table_length个u1):字节码。
  • g. exception_table_length(u2):异常表大小。
  • h. exception_table(exception_table_length个exception_info):异常表大小。
  • i. attributes_count(u2):方法属性表的大小。
  • j. attributes(attribute_count个attribute_info):方法属性表。

接下来用我们Class的两个方法来详细说明Code属性:

-w1238

method_#1方法:

  • i. attributes_count = 1
  • ii. attributes:
  • a. attribute_name_index:常量0X0009Code
  • b. attribute_length:29(0X0000001D),即下一位起后的29u都是这个属性。
  • c. max_stack:1(0X0001)。
  • d. max_locals:1(0X0001)。
  • e. code_length:5(0X00000005)。
  • f. code:0X2AB70001B1。(字节码指令的具体含义鉴于与class结构是相对独立的主题不再详述,后续会再单独深入介绍)
  • g. exception_table_length:0(OX0000)。
  • h. exception_table:无。
  • i. attributes_count:1(0X0001)。
  • j. attributes:
  • attribute_name_index:LineNumberTable(0X000A),说明这是一个用于记录源码行号和字节码行号映射的属性表。
  • attribute_length:6(0X00000006).
  • attribute:LineNumberTable属性表的内部结构:

    • line_number_table_length:1(0X0001)。
    • line_number_index:0:3(0X00000003)。

method_#2方法的分析方式如上类似不再赘述。

Class字节码的结构为什么这么设计

乍一看来上面的结构让人很难快速理解,但是如果理解JVM的字节码结构的设计目的就可以加深理解了。

JVM的字节码结构其实是一种由字节码堆砌的表型结构,充分定义占位的结构可以无歧义地将它想要表达的原义还原回去。作为二进制结构主要的表达方式,只要定义好占位情况,表型结构可以通过层层嵌套定义来实现更为复杂的结构、并且可以实现良好的拓展。

比如上面的介绍的方法信息通过方法数量定义了这个表的大小,而每个表entry内部可以再有自己的定义,比如方法信息中还可以包含属性表(即在方法表内部再嵌套一层表),比如这里定义了Code属性表,而Code属性表自身又有良好的表结构定义,这个表内部除了一些一维的字段(比如index、count等不能拓展的字段)外,还有额外的exception_table,但是因为有exception_table_length的表大小限制就可以无歧义地还原回去,此外还有attribute_info,但是因为有attribute_count的表大小限制也可以无歧义地还原回去。用下面的思维导图我们可以直观地看出来这种良好的定义,图中加入了每个一维节点的占位大小:

class format


JinhaoPlus
1.5k 声望92 粉丝

扎瓦程序员