Class文件结构

一、概述

1.1 字节码文件的跨平台性

  • Java语言:跨平台的语言(write once, run anywhere)
    • 当]ava源代码成功编译成字节码后,如果想在不同的平台上面运行,则无须再次编译。
    • 这个优势不再那么吸引人了。 Python、PHP、Perl、Ruby、Lisp等有强大的解释器。
    • 跨平台似乎己经快成为一门语言必选的特性。
  • Java虚拟机:跨语言的平台
    • Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。无论使用何种语言进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。可以说,统一而强大的Class文件结构,就是Java虚拟机的基石、桥梁。
    • 所有的JVM全部遵守]ava虚拟机规范,也就是说所有的JVM环境都是一样的,这样一来字节码文件可以在各种JVM上运行。

1.2 前端编译器

  • 想要让一个Java程序正确地运行在JVM中,Java源码就必须要被编译为符合JVM规范的字节码。
    • 前端编译器的主要仼务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的宇节码文件。
    • Javac是一种能够将Java源码编译为字节码的前端编译器。
    • Javac编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法解析、语法解析、语义解析以及生成字节码。

1.3 字节码指令

  • Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数( operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。

例子1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class IntegerTest {
public static void main(String[] args) {
Integer x = 5;
int y = 5;
System.out.println(x == y);//true,引用数据类型和基本数据类型无法比较,所以此比较经历了拆箱过程。

Integer i1 = 10;
Integer i2 = 10;
System.out.println(i1 == i2);//true

Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);//false
}
}

对应的字节码指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 0 iconst_5 //定义常量5并放入操作数栈
1 invokestatic #2 <java/lang/Integer.valueOf> //如果int范围在-127~128间则返回cache数组里的同个Integer对象,否则就new Integer()后返回。
4 astore_1 //保存在局部变量表索引为1的位置
5 iconst_5 //定义常量5
6 istore_2 //保存在局部变量表索引为2的位置
7 getstatic #3 <java/lang/System.out>
10 aload_1
11 invokevirtual #4 <java/lang/Integer.intValue> //拆箱操作
14 iload_2
15 if_icmpne 22 (+7)
18 iconst_1
19 goto 23 (+4)
22 iconst_0
23 invokevirtual #5 <java/io/PrintStream.println>
26 bipush 10
28 invokestatic #2 <java/lang/Integer.valueOf>
31 astore_3
32 bipush 10
34 invokestatic #2 <java/lang/Integer.valueOf>
37 astore 4
39 getstatic #3 <java/lang/System.out>
42 aload_3
43 aload 4
45 if_acmpne 52 (+7)
48 iconst_1
49 goto 53 (+4)
52 iconst_0
53 invokevirtual #5 <java/io/PrintStream.println>
56 sipush 128
59 invokestatic #2 <java/lang/Integer.valueOf>
62 astore 5
64 sipush 128
67 invokestatic #2 <java/lang/Integer.valueOf>
70 astore 6
72 getstatic #3 <java/lang/System.out>
75 aload 5
77 aload 6
79 if_acmpne 86 (+7)
82 iconst_1
83 goto 87 (+4)
86 iconst_0
87 invokevirtual #5 <java/io/PrintStream.println>
90 return

例子2:

1
2
3
4
5
6
7
public class StringTest {
public static void main(String[] args) {
String str = new String("hello") + new String("world");
String str1 = "helloworld";
System.out.println(str == str1);
}
}

对应的字节码指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 0 new #2 <java/lang/StringBuilder()> //new StringBuilder()
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init>> //调用构造器进行初始化
7 new #4 <java/lang/String> //new String()
10 dup
11 ldc #5 <hello>
13 invokespecial #6 <java/lang/String.<init>> //调用构造器进行初始化
16 invokevirtual #7 <java/lang/StringBuilder.append> //向StringBuilder中添加String
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <world>
25 invokespecial #6 <java/lang/String.<init>>
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString> //调用StringBuilder.toString返回String
34 astore_1
35 ldc #10 <helloworld>
37 astore_2
38 getstatic #11 <java/lang/System.out>
41 aload_1
42 aload_2
43 if_acmpne 50 (+7)
46 iconst_1
47 goto 51 (+4)
50 iconst_0
51 invokevirtual #12 <java/io/PrintStream.println>
54 return

例子3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
成员变量(非静态的)的赋值过程: ① 默认初始化 - ② 显式初始化 /代码块中初始化 - ③ 构造器中初始化 - ④ 有了对象之后,可以“对象.属性”或"对象.方法"的方式对成员变量进行赋值。
*/
public class SonTest {
public static void main(String[] args) {
Father f = new Son();
System.out.println(f.x);//属性没有多态,打印的是父类的x
}
}

class Father {
int x = 10;
public Father() {
this.print();
x = 20;
}
public void print() {
System.out.println("Father.x = " + x);
}
}

class Son extends Father {
int x = 30;
public Son() {
this.print();
x = 40;
}
public void print() {
System.out.println("Son.x = " + x);
}
}

打印结果:

1
2
3
Son.x = 0
Son.x = 30
20

其中重点看Son类构造器的字节码指令:

1
2
3
4
5
6
7
8
9
10
11
 0 aload_0
1 invokespecial #1 <com/company/java11/Father.<init>> //调用父类的构造器方法,由于子类重写了print(),所以实际上在父类的构造器中调用的是子类的print(),但由于此时x还没有被赋值,从而第一次打印的是x=0(x的显式赋值操作在后面的bipush 30指令中)。
4 aload_0
5 bipush 30
7 putfield #2 <com/company/java11/Son.x>
10 aload_0
11 invokevirtual #3 <com/company/java11/Son.print> //上面的30已经赋值给x了,所以第二次打印出x = 30。
14 aload_0
15 bipush 40
17 putfield #2 <com/company/java11/Son.x>
20 return

二、Class文件结构

  • **Class类的本质:**任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在,Class文件是一组以8位字节为基础单位的二进制流。

  • **Class文件格式:**Class的结构不像xml等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

  • Class文件格式采用一种类似于C语言结枃体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表

    • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
    • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个C1ass文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明。
  • Class文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的。

    • class文件的总体结构如下:
      • 魔数(magic)
      • class文件版本(minor_version,major_version)
      • 常量池(constant_pool_count,constant_pool)
      • 访问标志(access_flags)
      • 类索引,父类索引,接口索引集合(this_class,super_class,interfaces_count,interfaces)
      • 字段表集合(fields_count,fields)
      • 方法表集合(methods_count,methods)
      • 属性表集合(attributes_count,attributes)
    类型 名称 说明 长度 数量
    u4 magic 魔数,识别Class文件格式 4个字节 1
    u2 minor_version 副版本号(小版本) 2个字节 1
    u2 major_version 主版本号(大版本) 2个字节 1
    u2 constant_pool_count 常量池计数器 2个字节 1
    cp_info constant_pool 常量池表 n个字节 constant_pool_count-1
    u2 access_flags 访问标识 2个字节 1
    u2 this_class 类索引 2个字节 1
    u2 super_class 父类索引 2个字节 1
    u2 interfaces_count 接口计数器 2个字节 1
    u2 interfaces 接口索引集合 2个字节 interfaces_count
    u2 fields_count 字段计数器 2个字节 1
    field_info fields 字段表 n个字节 fields_count
    u2 methods_count 方法计数器 2个字节 1
    method_info methods 方法表 n个字节 methods_count
    u2 attributes_count 属性计数器 2个字节 1
    attribute_info attributes 属性表 n个字节 attributes_count
  • 以以下代码的例子来解读对应的class文件:

    1
    2
    3
    4
    5
    6
    7
    8
    public class Demo {
    private int num = 1;

    public int add(){
    num = num + 2;
    return num;
    }
    }
    • 对应的字节码文件:

2.1 魔数(Magic Number)

  • 每个Class文件开头的4个字节的无符号整数称为魔数(Magic Number)。
  • 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即:魔数是 Class文件的标识符。
  • 魔数值固定为0xCAFEBABE,不会改变。
  • 如果一个class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出以下错误:Error: A JNI error has occurred, please check your installation and try again Exception in thread “main” java. lang. ClassFormatError: Incompatible magic value 1885430635 in class file……。
  • 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。

2.2 Class文件版本号

  • 紧接着魔数的4个字节存储的是 Class文件的版本号。同样也是4个字节。第5个和第6个宇节所代表的含义就是编译的副版本号minor_version,而第7个和第8个字节就是编译的主版本号中major_version。

  • 它们共同构成了class文件的格式版本号。譬如某个Class文件的主版本号为M,副版本号为m,那么这个Class文件的格式版本号就确定为M.m。

  • 版本号和Java编译器的对应关系如下表:

    主版本(十进制) 副版本(十进制) 编译器版本
    45 3 1.1
    46 0 1.2
    47 0 1.3
    48 0 1.4
    49 0 1.5
    50 0 1.6
    51 0 1.7
    52 0 1.8
    53 0 1.9
    54 0 1.10
    55 0 1.11
    • Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1。
    • 不同版本的Java编译器编译的Class文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器生成的class文件,但是低版本的]ava虚拟机不能执行由高版本编译器生成的class文件。否则JVM会抛出java.lang.UnsupportedClassVersionError异常。
    • 在实际应用中,由于开发环境和生产环境的不同,可能会导致该问题的发生。因此,需要我们在开发时,特别注意开发编译的JDK版本和生产环境中的JDK版本是否一致。
      • 虚拟机JDK版本为1.k(k>=2)时,对应的class文件格式版本号的范围为45.0~(44+k.0)。

2.3 常量池

  • 常量池是class文件中内容最为丰富的区域之一。常量池对于class文件中的字段和方法解析也有着至关重要的作用。

  • 随着Java虚拟机的不断发展,常量池的内容也日渐丰富。可以说,常量池是整个class文件的基石。

  • 在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。

  • 常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_ pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。

    类型 名称 说明 长度 数量
    u2 constant_pool_count 常量池计数器 2个字节 1
    cp_info constant_pool 常量池表 n个字节 constant_pool_count-1
    • 由上表可见,Class文件使用了一个前置的容量计数器(constant_ pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。
      • 常量池表项中,用于存放编译时期生成的各种字面量符号引用,这部分内容将在类加载后进入方法区的运行时常量池中。

2.3.1 常量池计数器

  • 由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值。
  • 常量池容量数值(u2类型):从1开始,表示常量池中有多少项常量。即 constant_ pool_count=1表示常量池中有0个常量项。
  • 通常我们写代码时都是从开始的,但是这里的常量池却是从1开始,因为它把第0项常量空出来了。这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。

2.3.2 常量池表

  • constant_poll是一种表结构,以1~constant_poll_count-1为索引。表明了后面有多少个常量项。

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

  • 它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第1个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte(标记字节、标签字节)。

  • 常量类型和结构细节如图:

    通过字节码文件可知常量池计数器有16,转换成10进制是22,而22-1=21,所以实际上常量池表有21项,通过查表得到下图:

    其中的21项常量池表对应如下图:

    以第一项常量池表为例子解释,从上面的图可知其对应的16进制数为:0a、00、04、00、12。0a对应的十进制是10,查表可知其表示一个方法引用(CONSTANT_Methodref_info);04对应的十进制是4,指向了常量池表的第4项,即CONSTANT_Class_info;12对应的十进制是18,指向了常量池表的第18项,即CONSTANT_NameAndType_info。第4和18项的常量池表也用同样的规则继续查找。如下图:

2.3.2.1 字面量和符号引用

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

    常量 具体的常量
    字面量 文本字符串
    声明为final的常量值
    符号引用 类和接口的全限定名
    字段的名称和描述符
    方法的名称和描述符
    • 全限定名:即把包的”.”替换成”/“,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个”;”表示全限定名结束。

    • 简单名称:指没有类型和参数修饰的方法或者字段名称,如上面例子的add()方法和num字段的简单名称分别是add和num。

    • 描述符:作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(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;
      [ 数组类型,代表一维数组。比如:double[][][] is [[[D

例子:

1
2
3
4
5
6
7
8
9
10
11
12
public class ArrayTest {
public static void main(String[] args) {
Object[] arr = new Object[10];
System.out.println(arr);//[Ljava.lang.Object;@74a14482

String[] arr1 = new String[10];
System.out.println(arr1);//[Ljava.lang.String;@1540e19d

long[][] arr2 = new long[10][];
System.out.println(arr2);//[[J@677327b6
}
}
  • 虚拟机在加载C1ass文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。
    • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
    • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

2.3.2.2 常量类型和结构

  • 这14种表(或者常量项结构)的共同点是:表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型。
  • CONSTANT_Utf8_info常量项是一种使用改进过的UTF-8编码格式来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息。
  • 这14种常量项结构还有一个特点是,其中13个常量项占用的字节固定,只有CONSTANT_Utf8info占用字节不固定,其大小由length决定。为什么呢?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,大小不固定,编译后,通过utf-8编码,就可以知道其长度。
  • 常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用C1ass文件空间最大的数据项目之一。
  • 常量池中为什么要包含这些内容?
    • Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态链接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

2.4 访问标识

  • 在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。各种访问标记如下所示:

    标志名称 标志值 含义
    ACC_PUBLIC 0x0001 标志为public类型
    ACC_FINAL 0x0010 标志被声明为final,只有类可以设置
    ACC_SUPER 0x0020 标志允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真。(使用增强的方法调用父类方法)
    ACC_INTERFACE 0x0200 标志这是一个接口
    ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
    ACC_SYNTHETIC 0x1000 标志此类并非由用户代码产生(即:由编译器产生的类,没有源码对应)
    ACC_ANNOTATION 0x2000 标志这是一个注解
    ACC_ENUM 0x4000 标志这是一个枚举
  • 类的访问权限通常为ACC_开头的常量。

  • 每一种类型的表示都是通过设置访问标记的32位中的特定位来实现的。比如,若是public final的类,则该标记为ACC_PUBLIC | ACC_FINAL。

  • 使用ACC_SUPER可以让类更准确地定位到父类的方法super.method(),现代编译器都会设置并且使用这个标记。

  • 带有ACC_INTERFACE标志的class文件表示的是接口而不是类,反之则表示的是类而不是接口。

    • 如果一个class文件被设置了ACC_INTERFACE标志,那么同时也得设置ACC_ABSTRACT标志。同时它不能再设置ACC_FINAL、ACC_SUPER 或ACC_ENUM标志。
    • 如果没有设置ACC_INTERFACE标志,那么这个class文件可以具有上表中除ACC_ANNOTATION外的其他所有标志。当然,ACC_FINAL和ACC_ABSTRACT这类互斥的标志除外。这两个标志不得同时设置。
  • ACC_SUPER标志用于确定类或接口里面的invokespecial指令使用的是哪一种执行语义。针对Java虚拟机指令集的编译器都应当设置这个标志。对于JavaSE8及后续版本来说,无论class文件中这个标志的实际值是什么,也不管class文件的版本号是多少,Java虚拟机都认为每个class文件均设置了ACC_SUPER标志。

    • ACC_SUPER标志是为了向后兼容由旧Java编译器所编译的代码而设计的。目前的ACC_SUPER标志在由JDK1.0.2之前的编译器所生成的access_flags中是没有确定含义的,如果设置了该标志,那么0racle的Java虚拟机实现会将其忽略。
  • ACC_SYNTHETIC标志意味着该类或接口是由编译器生成的,而不是由源代码生成的。

  • 注解类型必须设置ACC_ANNOTATION标志。如果设置了ACC_ANNOTATION标志,那么也必须设置ACC_INTERFACE标志。

  • ACC_ENUM标志表明该类或其父类为枚举类型。

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

  • 在访问标记后,会指定该类的类别、父类类别以及实现的接口,格式如下:

    长度 含义
    u2 this_class
    u2 super_class
    u2 interfaces_count
    u2 interfaces[interfaces_count]
    • 类索引用于确定这个类的全限定名。
    • 父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。
    • 接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。
  • this_class(类索引)

    • 2字节无符号整数,指向常量池的索引。它提供了类的全限定名。this_class的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为CONSTANT_Classinfo类型结构体,该结构体表示这个class文件所定义的类或接口。
  • super_class(父类索引)

    • 2字节无符号整数,指向常量池的索引。它提供了当前类的父类的全限定名。如果我们没有继承任何类,其默认继承的是java/lang/Object类。同时,由于Java不支持多继承,所以其父类只有一个。
    • superclass指向的父类不能是final。
  • interfaces

    • 指向常量池索引集合,它提供了一个符号引用到所有已实现的接口。
    • 由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的CONSTANT_Class(当然这里就必须是接口,而不是类)。
    • interfaces_count(接口计数器)
      • interfaces_count项的值表示当前类或接口的直接超接口数量。
    • interfaces[](接口索引集合)
      • interfaces[]中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为interfacescount。每个成员interfaces[il必须为CoNSTANT_Class_info结构,其中0≤i<interfaces_count。在interfaces[]中,各成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即interfaces[0]对应的是源代码中最左边的接口。

2.6 字段表集合

  • 用于描述接口或类中声明的变量。字段(field)包括类级变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量。

  • 字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

  • 它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)等。

  • 注意事项:

    • 字段表集合中不会列出从父类或者实现的接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
    • 在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。

2.6.1 字段计数器

  • 字段计数器(fields_count)的值表示当前class文件flelds表的成员个数。使用两个字节来表示。
  • fields表中每个成员都是一个field_info结构,用于表示该类或接口所声明的所有类字段或者实例字段,不包括方法内部声明的变量,也不包括从父类或父接口继承的那些字段。

2.6.2 字段表

  • fields表中的每个成员都必须是一个fields_info结构的数据项,用于表示当前类或接口中某个字段的完整描述。

  • 一个字段的信息包括如下这些信息。这些信息中,各个修饰都是布尔值,要么有,要么没有。

    • 作用域(public、private、protected修饰符)
    • 是实例变量还是类变量(static修饰符)
    • 可变性(final)
    • 并发可见性(volatile修饰符,是否强制从主内存读写)
    • 可否序列化(transient修饰符)
    • 字段数据类型(基本数据类型、对象、数组)
    • 字段名称
  • 字段表作为一个表,同样有他自己的结构:

    类型 名称 含义 数量
    u2 access_flags 访问标志 1
    u2 name_index 字段名索引 1
    u2 descriptor_index 描述符索引 1
    u2 attributes_count 属性计数器 1
    attribute_info attributes 属性集合 attributes_count

2.6.2.1 字段表访问标识

  • 我们知道,一个字段可以被各种关键字去修饰,比如:作用域修饰符(public、private、protected)、static修饰符、final修饰符、volatile修饰符等等。因此,其可像类的访问标志那样,使用一些标志来标记字段。字段的访问标志有如下这些:

    标志名称 标志值 含义
    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_TRANSTENT 0x0080 字段是否为transient
    ACC_SYNCHETIC 0x1000 字段是否为由编译器自动产生
    ACC_ENUM 0x4000 字段是否为enum

2.6.2.2 字段名索引

  • 根据字段名索引的值,查询常量池中的指定索引项即可。

2.6.2.3 描述符索引

  • 描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte,char,double,float,int,long,short,boolean)及代表无返回值的void类型都用一个大写字符来表示,而对象则用字符L加对象的全限定名来表示,如下所示:

    字符 类型 含义
    B byte 有符号字节型数
    C char Unicode字符,UTF-16编码
    D double 双精度浮点数
    F float 单精度浮点数
    I int 整型数
    J long 长整数
    S short 有符号短整数
    Z boolean 布尔值true/false
    L Classname; reference 一个名为Classname的实例
    [ reference 一个一维数组

2.6.2.4 属性表集合

  • 一个字段还可能拥有一些属性,用于存储更多的额外信息。比如初始化值、一些注释信息等。属性个数存放在attribute_count中,属性具体内容存放在attributes数组中。

    • 以常量属性为例,结构为:

      1
      2
      3
      4
      5
      ConstantValue_attribute{
      u2 attribute_name_index;
      u4 attribute_length;
      u2 constantvalue_index;
      }
    • 说明:对于常量属性而言,attribute_length值恒为2。

  • 上述例子只有个num字段,没有对应的属性信息,除非该字段设定为final。

2.7 方法表集合

  • methods:指向常量池索引集合,它完整描述了每个方法的签名。
    • 在字节码文件中,每一个method_info项都对应着一个类或者接口中的方法信息。比如方法的访问修饰符(public、private或protected),方法的返回值类型以及方法的参数信息等。
    • 如果这个方法不是抽象的或者不是native的,那么字节码中会体现出来。
    • 一方面,methods表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。另一方面,methods表有可能会出现由编译器自动添加的方法,最典型的便是编译器产生的方法信息(比如:类(接口)初始化方法()和实例初始化方法())。
  • 使用注意事项:
    • 在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个class文件中。
    • 也就是说,尽管Java语法规范并不允许在一个类或者接口中声明多个方法签名相同的方法,但是和Java语法规范相反,字节码文件中却恰恰允许存放多个方法签名相同的方法,唯一的条件就是这些方法之间的返回值不能相同。

2.7.1 方法计数器

  • methods_count的值表示当前class文件methods表的成员个数。使用两个字节来表示。
  • methods 表中每个成员都是一个method_info结构。

2.7.2 方法表

  • methods表中的每个成员都必须是一个method_info结构,用于表示当前类或接口中某个方法的完整描述。如果某个method_info结构的access_flags项既没有设置ACC_NATIVE标志也没有设置ACC_ABSTRACT标志,那么该结构中也应包含实现这个方法所用的Java虚拟机指令。

  • method_info结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法和类或接口初始化方法方法表的结构实际跟字段表是一样的,方法表结构如下:

    类型 名称 含义 数量
    u2 access_flags 访问标志 1
    u2 name_index 方法名索引 1
    u2 descriptor_index 描述符索引 1
    u2 attributes_count 属性计数器 1
    attribute_info attributes 属性集合 attributes_count

2.7.2.1 方法表访问标志

  • 跟字段表一样,方法表也有访问标志,而且他们的标志有部分相同,部分则不同,方法表的具体访问标志如下:

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

2.7.2.2 方法名索引

2.7.2.3 描述符索引

2.7.2.4 属性表集合

  • 方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合一个名为“Code”属性里,属性表作为Class文件格式中最具扩展性的一种数据项目,在后续会详细讲解。

2.8 属性表集合

  • 方法表集合之后的属性表集合,指的是class文件所携带的辅助信息,比如该class文件的源文件的名称。以及任何带有RetentionPolicy.CLASS 或者RetentionPolicy.RUNTIME的注解。这类信息通常被用于Java虚拟机的验证和运行,以及Java程序的调试,一般无须深入了解。
  • 此外,字段表、方法表都可以有自己的属性表。用于描述某些场景专有的信息。
    • 属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但Java虚拟机运行时会忽略掉它不认识的属性。

2.8.1 属性计数器

  • attributes_count的值表示当前class文件属性表的成员个数。属性表中每一项都是一个attribute_info结构。这里以方法表中的属性计数器为例:

2.8.2 属性表

  • 属性表的每个项的值必须是attribute_info结构。属性表的结构比较灵活,各种不同的属性只要满足以下结构即可。

  • 属性的通用格式:

    类型 名称 数量 含义
    u2 attribute_name_index 1 属性名索引
    u4 attribute_length 1 属性长度
    u1 info attribute_length 属性表
    • 即只需说明属性的名称以及占用位数的长度即可,属性表具体的结构可以去自定义。
  • 属性类型:

  • 以方法表中的属性表为例,找到两个方法中各自的属性名索引都为09,即为Code属性。属性表实际上可以有很多类型,Code属性只是其中一种,Java8里面定义了23种属性。

    • code属性表结构为:

    • 在字节码指令中标出两个方法(构造方法和add方法)的对应信息:

  • 其中,以第一个方法(构造方法为例),它的属性长度是00 00 00 38,转换为十进制即为56,即刻画此方法的属性总共有56个字节,如下图所画区域:

  • 字节码指令的长度是00 00 00 0a,转换为十进制即为10,即该方法字节码指令的长度总共有10个字节,如下图所画区域:

    • 其中的具体字节码指令为2a b7 00 01 2a 04 b5 00 02 b1,来源于下面各条指令:

  • Code的属性计数器是00 02,即Code中也有2个属性集合,根据上面的属性通用格式可知紧接着的2个字节表示属性名索引,即00 0a对应LineNumberTable,LineNumberTable属性是用来描述Java源码行号与字节码行号之间的对应关系。这个属性可以用来在调试的时候定位代码执行的行数,start_pc,即字节码行号;line_number,即Java源代码行号。如图:

    • 通过查询LineNumberTable属性表结构为:

    • 从而找到第一个方法(构造方法)对应的LineNumberTable属性表:

    • 和下面的信息一一对应:

  • 紧接着的2个字节表示Code的第二个属性集索引,即00 0b对应LocalVariableTable,如图:

    • 通过查询LocalVariableTable属性表结构为:

    • 从而找到第一个方法(构造方法)对应的LocalVariableTable属性表:

    • 和下面的信息一一对应:

  • 两个方法的属性表集合解析完毕后,来到最后的属性表集合,指的是class文件所携带的辅助信息。此附加属性计数器是00 01,即只有一个属性表;属性名索引为00 10,对应SourceFile。

    • 通过查询SourceFile的属性表结构为:

    • 对应以下字节码指令如下,其中的附加属性长度永远固定为00 00 00 02,源码文件索引为00 11对应十进制的17,即源文件的文件名:

三、总结

  • 附上上面例子对应的源文件:

    1
    2
    3
    4
    5
    6
    7
    8
    public class Demo {
    private int num = 1;

    public int add(){
    num = num + 2;
    return num;
    }
    }
  • 编译后的字节码文件:

  • 对字节码文件的分析结果: