Java class文件探索

本文将通过对一个简单的Java源文件,编译成的class文件进行分析,以探索Java class文件的格式。

package ex3;

public class ConstantTest {
    int a;
    public void inc() {
        ++a;
    }
}

将这个Java代码保存为ConstantTest.java,使用javac ConstantTest.java,将其编译成ConstantTest.class字节码文件。使用十六进制查看器hexdump,查看其内容hexdump -C ConstantTest.class。现在看不懂没关系,下面我们逐字节分析。

00000000  ca fe ba be 00 00 00 34  00 12 0a 00 04 00 0e 09  |.......4........|
00000010  00 03 00 0f 07 00 10 07  00 11 01 00 01 61 01 00  |.............a..|
00000020  01 49 01 00 06 3c 69 6e  69 74 3e 01 00 03 28 29  |.I......()|
00000030  56 01 00 04 43 6f 64 65  01 00 0f 4c 69 6e 65 4e  |V...Code...LineN|
00000040  75 6d 62 65 72 54 61 62  6c 65 01 00 03 69 6e 63  |umberTable...inc|
00000050  01 00 0a 53 6f 75 72 63  65 46 69 6c 65 01 00 11  |...SourceFile...|
00000060  43 6f 6e 73 74 61 6e 74  54 65 73 74 2e 6a 61 76  |ConstantTest.jav|
00000070  61 0c 00 07 00 08 0c 00  05 00 06 01 00 10 65 78  |a.............ex|
00000080  33 2f 43 6f 6e 73 74 61  6e 74 54 65 73 74 01 00  |3/ConstantTest..|
00000090  10 6a 61 76 61 2f 6c 61  6e 67 2f 4f 62 6a 65 63  |.java/lang/Objec|
000000a0  74 00 21 00 03 00 04 00  00 00 01 00 00 00 05 00  |t.!.............|
000000b0  06 00 00 00 02 00 01 00  07 00 08 00 01 00 09 00  |................|
000000c0  00 00 1d 00 01 00 01 00  00 00 05 2a b7 00 01 b1  |...........*....|
000000d0  00 00 00 01 00 0a 00 00  00 06 00 01 00 00 00 03  |................|
000000e0  00 01 00 0b 00 08 00 01  00 09 00 00 00 27 00 03  |.............'..|
000000f0  00 01 00 00 00 0b 2a 59  b4 00 02 04 60 b5 00 02  |......*Y....`...|
00000100  b1 00 00 00 01 00 0a 00  00 00 0a 00 02 00 00 00  |................|
00000110  06 00 0a 00 07 00 01 00  0c 00 00 00 02 00 0d     |...............|
0000011f

先看看class文件的反汇编内容javap -v ContantTest.class

public class ex3.ConstantTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#14         // java/lang/Object."":()V
   #2 = Fieldref           #3.#15         // ex3/ConstantTest.a:I
   #3 = Class              #16            // ex3/ConstantTest
   #4 = Class              #17            // java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               SourceFile
  #13 = Utf8               ConstantTest.java
  #14 = NameAndType        #7:#8          // "":()V
  #15 = NameAndType        #5:#6          // a:I
  #16 = Utf8               ex3/ConstantTest
  #17 = Utf8               java/lang/Object
{
  int a;
    descriptor: I
    flags:

  public ex3.ConstantTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 3: 0

  public void inc();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field a:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field a:I
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 10
}

class文件结构

Class文件是一组以8bit字节为基础单位的二进制流, 各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。 当遇到需要占用8bit以上空间的数据项时,则会按照高位在前(大端序)的方式分割成若干个字节进行存储。

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

Class文件格式可以用上面这个结构体来表示,其中的u2/u4表示2字节和4字节的数据。我们将其分为以下几个部分来探讨。

  • magic
  • versioin
  • 常量池
  • access_flags
  • 类索引、父类索引与接口索引集合
  • 字段表
  • 方法表
  • 属性表

magic

magic是一个四字节的数据,它唯一的功能是确定这个文件是否为一个能被虚拟机接受的Class文件,它的值是固定的0xCAFEBABE,是Cafe Babe的意思,这是Java在设计之初就已经决定了的。

使用hexdump -C XXX.class | head -n 2 可以查看class文件的十六进制表示,可以看到它的前4字节是ca fe ba be。

00000000  ca fe ba be 00 00 00 34  00 12 0a 00 04 00 0e 09  |.......4........|

注意:大端序与我们的阅读顺序相同。例如0x12345678中12是高位字节,78是低位字节,而class文件中的第1个字节是低位地址,第4个字节是高位地址,大端序中高位字节放在低位地址,即12放在第1个字节,78放在第4个字节。如果是小端序,就要反过来,在文件中看到的就是78 56 45 12。

Linux中的file命令可以用来判断文件类型,它可以利用这个magic来判断一个文件是不是class文件。

version

紧接着magic的4个字节存储的是Class文件的版本号: 第5和第6个字节是minor_version , 第7和第8个字节是major_version

Java的版本号是从45开始的, JDK 1.1之后的每个JDK大版本发布主版本号向上加1( JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件, 但不能运行以后版本的Class文件, 因为《 Java虚拟机规范》 在Class文件校验部分明确要求了即使文件格式并未发生任何变化, 虚拟机也必须拒绝执行超过其版本号的Class文件。

在上面的例子中,minor_version为00 00,而major_version为00 34即52,因此该class文件版本号为52.0,是由JDK 8编译而来。

关于次版本号, 曾经在现代Java( 即Java 2)出现前被短暂使用过, JDK 1.0.2支持的版本45.0~45.3( 包括45.0~45.3),JDK 1.1支持版本45.0~45.65535, 从JDK 1.2以后, 直到JDK 12之前次版本号均未使用, 全部固定为零。 而到了JDK 12时期, 由于JDK提供的功能集已经非常庞大, 有一些复杂的新特性需要以“公测”的形式放出, 所以设计者重新启用了副版本号, 将它用于标识“技术预览版”功能特性的支持。 如果Class文件中使用了该版本JDK尚未列入正式特性清单中的预览功能, 则必须把次版本号标识为65535, 以便Java虚拟机在加载类文件时能够区分出来。

常量池

constant_pool_count是一个u2类型的数据,代表了常量池容量计数器, 这个容量计数是从1而不是0开始的。如上面例子中的00 12即十进制18,那么常量池的索引范围为1~17。在Class文件格式规范制定之时, 设计者将第0项常量空出来是有特殊考虑的, 这样做的目的在于, 如果后面某些指向常量池的索引值的数据在特定情况下
需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。

常量池中主要存放两大类常量: 字面量( Literal) 和符号引用( Symbolic References) 。

  • 字面量比较接近于Java语言层面的常量概念, 如文本字符串、 被声明为final的常量值等。
  • 而符号引用则属于编译原理方面的概念, 主要包括下面几类常量:
    • 被模块导出或者开放的包( Package)
    • 类和接口的全限定名( Fully Qualified Name)
    • 字段的名称和描述符( Descriptor)
    • 方法的名称和描述符
    • 方法句柄和方法类型( Method Handle、 Method Type、 Invoke Dynamic)
    • 动态调用点和动态常量( Dynamically-Computed Call Site、 Dynamically-Computed Constant)

以下是JDK 8支持的常量类型。

常量类型 标志 描述
CONSTANT_Class 7 类或接口的符号引用
CONSTANT_Fieldref 9 字段的符号引用
CONSTANT_Methodref 10 类中方法的符号引用
CONSTANT_InterfaceMethodref 11 接口中方法的符号引用
CONSTANT_String 8 字符串类型字面量
CONSTANT_Integer 3 整型字面量
CONSTANT_Float 4 浮点型字面量
CONSTANT_Long 5 长整形字面量
CONSTANT_Double 6 双精度浮点型字面量
CONSTANT_NameAndType 12 字段或方法的部分符号引用
CONSTANT_Utf8 1 UTF8编码的字符串
CONSTANT_MethodHandle 15 方法句柄
CONSTANT_MethodType 16 方法类型
CONSTANT_InvokeDynamic 18 动态计算常量

这些常量类型彼此都没有什么关系,它们是不同的结构体,对应着字节码中一段字节流,这段字节流的长度因常量类型而异。它们的共性是都由1字节的tag开头。

00000000  ca fe ba be 00 00 00 34  00 12 0a 00 04 00 0e 09  |.......4........|
00000010  00 03 00 0f 07 00 10 07  00 11 01 00 01 61 01 00  |.............a..|
00000020  01 49 01 00 06 3c 69 6e  69 74 3e 01 00 03 28 29  |.I......()|

上面例子中常量池的第1项的tag为0a,是一个Methodref,它的结构如下,一共占5字节0a 00 04 00 0e。

  • class_index表示常量池中一个Class类型的下标,其值为00 04即第4项。
  • name_and_type_index表示常量池中一个NameAndType类型的下标,其值为00 0e即第14项。
CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

紧接着第2项的tag为09,是一个Fieldref,它的结构与Methodref一样,占用5字节09 00 03 00 0f。

  • class_index值为00 03即第3项。
  • name_and_type_index值为00 0f即第15项。
CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

第3项的tag为07,是一个Class,它的结构如下,一共占用3字节07 00 10。

  • name_index表示常量池中一个Utf8类型的下标,其值为00 10即第16项。
CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

第4项的tag为07,同上,指向第17项。

第5项的tag为01,是一个Utf8,它的结构如下,长度是变化的。

  • length是字符串的字节数,其值为00 01即1。
  • bytes是一个字节数组,其值为0x61即97,是UTF-8编码表示的字符串a(英文字符的UTF-8编码与ASCII码相同)。看到这里,应该可以猜到,这个是代码中字段a的名称了。
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

第6项的tag01,同上,是一个字符串I

第7项的tag01,同上,是一个字符串

第8项的tag01,同上,是一个字符串()V

第9项的tag01,同上,是一个字符串Code

第10项也是字符串LineNumberTable

第11项也是字符串inc

第12项也是字符串SourceFile

第13项也是字符串ConstantTest.java

00000070  61 0c 00 07 00 08 0c 00  05 00 06 01 00 10 65 78  |a.............ex|
00000080  33 2f 43 6f 6e 73 74 61  6e 74 54 65 73 74 01 00  |3/ConstantTest..|
00000090  10 6a 61 76 61 2f 6c 61  6e 67 2f 4f 62 6a 65 63  |.java/lang/Objec|
000000a0  74 00 21 00 03 00 04 00  00 00 01 00 00 00 05 00  |t.!.............|

第14项tag0c,是一个NameAndType,其结构如下,一共占用5字节0c 00 07 00 08。

  • name_index是常量池中一个Utf8_info的下标,表示方法名称,其值为00 07即7,即
  • descriptor_index也是一个Utf8_info的下标,表示一个字段描述符(变量类型)或方法描述符(方法参数和返回值类型),其值为00 08即8,即()V表示方法没有参数,返回void类型。

这一项合起来就是:()V,描述的是该类中的一个方法,方法名称为,没有参数,没有返回值。

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

第15项tag0c,同上,合起来是a:I,描述的是一个字段,字段名称为a,类型为int

第16项tag01,是一个Utf8,长度为16,内容为65 78 33 2f 43 6f 6e 73 74 61 6e 74 54 65 73 74,即ex3/ConstantTest

第17项tag01,是一个Utf8,长度为16,内容为6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74,即java/lang/Object

access_flags

访问标志 描述
ACC_PUBLIC 0x0001 public
ACC_FINAL 0x0010 final
ACC_SUPER 0x0020 JDK 1.0.2之后所有类都必须带上这个标志
ACC_INTERFACE 0x0200 interface
ACC_ABSTRACT 0x0400 abstract
ACC_SYNTHETIC 0x1000 标识这个类不是由用户代码产生的
ACC_ANNOTATION 0x2000 annotation
ACC_ENUM 0x4000 enum

在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或 者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract 类型;如果是类的话,是否被声明为final;等等。

000000a0  74 00 21 00 03 00 04 00  00 00 01 00 00 00 05 00  |t.!.............|

上面例子中的access_flags值为00 21即,ACC_PUBLIC|ACC_SUPER,表示这是一个public类。

类索引、父类索引与接口索引集合

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合 (interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。类索 引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。

由于Java语言不允许多 重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了 java.lang.Object外,所有Java类的父类索引都不为0。

接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是extends关键字)后的接口顺序从左到右排列在接口索引集合中。

他们都是u2类型的数据,是指向常量池中一个Class类型的索引,而Class类型的索引则包含指向一个Utf8的索引。

this_class是u2类型的数据,是指向常量池中一个Class类型的索引。上面例子中的值为00 03,即常量池中第3项,指向第16项,内容为ex3/ConstantTest

super_class同上,值为00 04,即第4项,指向第17项,内容为java/lang/Object

接口索引集合首先包含一个u2类型的数据,表示接口数量,然后向后寻找接口数量个指向Class类型的索引。例子中值为00 00,表示没有实现接口。

字段表

000000a0  74 00 21 00 03 00 04 00  00 00 01 00 00 00 05 00  |t.!.............|
000000b0  06 00 00 00 02 00 01 00  07 00 08 00 01 00 09 00  |................|

fields_count是u2数据,表示类拥有多少个字段。在上面例子中值为00 01,表明有1个字段。

field_info结构用于描述接口或者类中声明的变量。Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段可以包括的修饰符有字段的作用域(public、private、protected修饰 符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否 强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、 字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫做什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

access_flags是方法的访问修饰符,与类的access_flags一样,都是u2类型。在上面例子中值为00 00,表明没有加修饰符。

标志 描述
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_ENUM 0x4000 enum

name_index是名称索引,指向常量池中一个Utf8,例子中值为00 05即第5项,是字符串a,表明这个字段名称为a

descriptor_index是描述符索引,指向常量池中一个Utf8,例子中值为00 06,是字符串I,表明这个字段类型为int

attributes_count是属性数量,值为00 00,表示没有属性。

方法表

000000b0  06 00 00 00 02 00 01 00  07 00 08 00 01 00 09 00  |................|
000000c0  00 00 1d 00 01 00 01 00  00 00 05 2a b7 00 01 b1  |...........*....|

方法表结构与字段表几乎一样。

methods_count表示方法数量,在上面例子中值为00 02,即有两个方法,后面有两个method_info项。

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

方法的access_flags与字段有所不同。

标志 描述
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_STRICT 0x0800 strictfp
ACC_SYNTHETIC 0x1000 方法由编译器生成

方法1

access_flags值为00 01,表示是public方法。

name_index值为00 07,指向字符串,这是方法名称。

descriptor_index值为00 08,指向字符串()V,表示方法没有参数,返回类型为void。

attributes_count值为00 01,表示有1个attribute_info

attribute_info结构如下,长度是变化的。

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

attribute_name_index值为00 09,指向字符串Code

attribute_length值为00 00 00 1d即29,表明后面的info字段长度为29字节。

000000c0  00 00 1d 00 01 00 01 00  00 00 05 2a b7 00 01 b1  |...........*....|
000000d0  00 00 00 01 00 0a 00 00  00 06 00 01 00 00 00 03  |................|

info是Code属性,其结构定义如下。

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 attributes_count;
    attribute_info attributes[attributes_count];
}

max_stack值为00 01,表明操作数栈的深度为1。

max_locals值为00 01,表明局部变量数量为1。

code_length值为00 00 00 05,表明代码长度为5。

code值为2a b7 00 01 b1,代表的指令分别为

  • 2a aload_0
  • b7 00 01``invokespecial #00.#01`
  • b1 return

exception_table_length值为00 00,代表异常表为空,即不抛出异常。

attrbutes_count值为00 01,表明属性表中有一个属性。

attribute_name_index值为00 0a,指向字符串LineNumberTable。LineNumberTable属性描述java源码和字节码的对应关系,它并不是运行时必备的属性,但默认会生成到class文件中。可以用javac -g:none来禁用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_length值为00 00 00 06,表明后面还有6字节。

line_number_table_length值为00 01,表明line_number_table中有一项。

这一项的start_pc值为00 00,line_number值为00 03,表明java源代码中行号为3的位置对应着class中该方法的起始位置。

方法2

000000e0  00 01 00 0b 00 08 00 01  00 09 00 00 00 27 00 03  |.............'..|
000000f0  00 01 00 00 00 0b 2a 59  b4 00 02 04 60 b5 00 02  |......*Y....`...|
00000100  b1 00 00 00 01 00 0a 00  00 00 0a 00 02 00 00 00  |................|
00000110  06 00 0a 00 07 00 01 00  0c 00 00 00 02 00 0d     |...............|
0000011f

access_flags为00 01,表明是public方法。

name_index值为00 0b,指向字符串inc,这是方法名称。

descriptor_index值为00 08,指向字符串()V,表示方法没有参数,返回类型为void。

attributes_count值为00 01,表示有1个attribute_info

attribute_name_index值为00 09,指向字符串Code,因此这个属性也是Code属性。

attribute_length值为00 00 00 27即39,表明后面的info字段长度为39字节。

max_stack值为00 03,表明操作数栈的深度为3。

max_locals值为00 01,表明局部变量数量为1。

code_length值为00 00 00 0b,表明代码长度为11。

code值为2a 59 b4 00 02 04 60 b5 00 02 b1,代表的指令分别为,显然这与inc方法相符。

  • 2a aload_0
  • 59 dup
  • b4 00 02 getfield #00.#02
  • 02 iconst_1
  • 60 iadd
  • b5 00 02 putfield #00.#02
  • b1 return

exception_table_length值为00 00,代表异常表为空,即不抛出异常。

attrbutes_count值为00 01,表明属性表中有一个属性。

attribute_name_index值为00 0a,指向字符串LineNumberTable,表明是一个LineNumberTable属性。

attribute_length值为00 00 00 0a,表明后面还有10字节。

line_number_table_length值为00 02,表明line_number_table中有2项。

第1项的start_pc值为00 00,line_number值为00 06,表明java源代码中行号为6的位置对应着code的起始位置。

第2项的start_pc值为00 0a,line_number值为00 07,表明java源代码中行号为7的位置对应着code第10字节的位置即return

属性表

00000110  06 00 0a 00 07 00 01 00  0c 00 00 00 02 00 0d     |...............|
0000011f

attributes_count值为00 01,表明有一个属性。

attribute_name_index值为00 0c,指向字符串SourceFile,表明是一个SourceFile属性。其结构如下,占用8字节。

SourceFile_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 sourcefile_index;
}

attribute_length值为00 00 00 02,表明attribute_length之后的属性内容一共占2字节。

sourcefile_index值为00 0d,指向常量池中第13向,即字符串ConstantTest.java

至此,字节码文件中的每一个字节都解析完了!

总结

  • 本文对class文件进行了逐字节分析,做了javap类似的工作,也得到了相符的结果。
  • Cafe babe很有趣,Java的设计者们太会了!
  • 各种定长结构体、变长结构体设计得非常巧妙,空间很紧凑,一点都没浪费。
  • 常量池非常重要,字节码指令中包含对常量池的引用,常量池中的数据也有递归引用。
  • 方法中的代码由一段Code属性描述,它就是一个字节数组。
  • 构造方法的名称为

了解了class文件结构之后,就要开始探索类加载机制了。

参考

  • Java字节码表
  • Java SE 8虚拟机规范

你可能感兴趣的:(Java class文件探索)