class文件存在哪(class文件内容)

2.2 class 文件内容

内容导视:

  • 魔数与版本号
  • 常量池
  • 访问标志
  • 类索引、父类索引、接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合

java 文件经 javac 编译器编译后生成 class 文件。

class 文件是一组以 8 个字节为基础单位的二进制流[1],只有两种数据类型:无符号数、表。

无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1、2、4、8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以 “_info” 结尾。一系列连续的同一类型、但数量不定的数据称为某类型的集合。

整个 class 文件本质上可以视作是一张表,由所示的数据项按严格顺序排列构成。

分别为:魔数、次版本号、主版本号、常量数量、常量表的集合、访问标志、类索引、父类索引、接口索引集合、字段数量、字段表集合、方法数量、方法表集合、属性数量、属性表集合。

表 2.2-1 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_flags

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

attribute_count

1

attribute_info

attributes

attribute_count

[1] 一个字节 8 位,每位都是 0 或 1,例 11001010,转为十六进制显示:CA。常常加上前缀表明进制,如 0xCA 代表按十六进制显示的一个字节。

2.2.1 魔数与版本号

使用十六进制编辑器打开 class 文件,提取一部分:

​表 2.2.1-1 魔数与版本号​

Address

00

01

02

03

04

05

06

07

08

09

0A

0B

0C

0D

0E

0F

00000000:

CA

FE

BA

BE

00

00

00

34

00 ~ 03:0xCAFEBABE,这 4 个字节称为魔数(magic),用来确定此文件是否为一个能被虚拟机接受的 class 文件。[1]

04 ~ 05:次版本号(Minor Version)0x0000。

06 ~ 07:主版本号(Major Version)0x0034,对应的十进制为 52,代表 JDK8;JDK1 从 45 开始,每隔一个大版本加 1;高版本的 JDK 可以向下兼容以前版本的 class 文件,反之则不行。

例:JDK1 无法执行版本号为 46 及以上的 class 文件。

强行运行会报错,抛出 UnsupportedClassVersionError:

​码 2.2.1-1 错误信息​

Error: A JNI error has occurred, please check your installation and try again Exception in thread "main" java.lang.UnsupportedClassVersionError: com/cqh/arr3/FieldResolution has been compiled by a more recent version of the Java Runtime (class File version 60.0), this version of the Java Runtime only recognizes class file versions up to 52.0

意思大概是 class 文件被 JDK16(60.0)编译,使用 JDK8(52.0)运行只能识别 52.0 以下的版本。

[1] 使用魔数作为文件格式的标识,因为扩展名可以随意改动,不安全。文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过而且不会引起混淆。

2.2.2 常量池

​表 2.2.2-1 常量池​

Address

00

01

02

03

04

05

06

07

08

09

0A

0B

0C

0D

0E

0F

00000000:

00

27

0A

00

09

00

16

09

00000010:

00

08

00

17

08

00

18

09

00

08

00

19

09

00

1A

00

00000020:

1B

08

00

1C

0A

00

1D

00

1E

07

00

1F

07

00

20

01

...

00000150:

69

6E

74

6C

6E

01

00

15

28

4C

6A

61

76

61

2F

6C

00000160:

61

6E

67

2F

53

74

72

69

6E

67

3B

29

56

由于常量池中,常量的数量不固定,需要一项 u2 类型的数据,记录下数量;

08 ~ 09:常量的数量(constant_pool_count),0x0027,十进制为 39,代表常量池中有 38 项常量,索引值为 1 ~ 38。[1]

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量即文本字符串、被声明为 final 的常量值等。

符号引用主要包括下面几类常量:

  • 被模块导出或者开放的包(Package)
  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符
  • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
  • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

class 文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。

当虚拟机加载类型时,将会从常量池获得对应的符号引用,再在类创建时或运行时,解析、翻译到具体的内存地址之中。[2]

常量池中每一项常量都是一个表,它们都有一个共同的特点,表结构起始的第一位是个 u1 类型的标志位(tag),代表着当前常量属于哪种常量类型。

​表 2.2.2-2 常量类型​

类型

标志

描述

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_MethodType_info

16

表示方法类型

CONSTANT_Dynamic_info

17

表示一个动态计算常量

CONSTANT_InvokeDynamic_info

18

表示一个动态方法调用点

CONSTANT_Module_info

19

表示一个模块

CONSTANT_Package_info

20

表示一个模块中开放或导出的包

0A:第 1 项常量的标志位 0x0A,十进制为 10;查表可知这个常量属于 CONSTANT_Methodref_info 类型,代表类中方法的符号引用。

​表 2.2.2-3 CONSTANT_Methodref_info 常量的结构​

类型

名称

数量

描述

u1

tag

1

标志位 10

u2

class_index

1

此索引指向 CONSTANT_Class_info 类型的常量,表示方法所属的类型

u2

name_and_type_index

1

此索引 CONSTANT_NameAndType_info 类型的常量,表示方法名称及描述符

0B ~ 0C:0x0009,指向第 9 项常量,第 9 项常量属于 CONSTANT_Class_info 类型,此常量的 name_index 值为 32,指向第 32 项常量,第 32 常量属于 CONSTANT_Utf8_info 类型,此常量的 bytes 值经 UTF-8 解码后,为 “java/lang/Object”。所以此方法所属类型已然确定。

0D ~ 0E:0x0016,十进制为 22,指向第 22 项常量,第 22 项常量属于...略。

0F:第 2 项常量的标志位 0x09,十进制为 9,查表可知这个常量属于 CONSTANT_Fieldref_info 类型,代表字段的符号引用。

​表 2.2.2-4 CONSTANT_Fieldref_info 型常量的结构​

类型

名称

数量

描述

u1

tag

1

标志位 9

u2

class_index

1

此索引指向 CONSTANT_Class_info 类型的常量,表示字段所属类型

u2

name_and_type_index

1

此索引指向 CONSTANT_NameAndType_info 类型的常量,表示字段名及描述符

10 ~ 11:0x0008,指向第 8 项常量。

12 ~ 13:0x0017,指向第 23 项常量。

14:第 3 项常量池的标志位 0x08,属于 CONSTANT_String_info 类型,代表字符串类型的字面量。

​表 2.2.2-5 CONSTANT_String_info 型常量的结构​

类型

名称

数量

描述

u1

tag

1

标志位 8

u2

string_index

1

此索引指向 CONSTANT_Utf8_info 类型的常量,表示字符串字面量

15 ~ 16:0x0018,指向第 24 项常量。

...

155:第 38 项常量的标志位 0x01,属于 CONSTANT_Utf8_info 类型,代表 UTF-8 编码的字符串,用来表示字符串字面量、类型的全限定名、字段名、...[3]

​表 2.2.2-6 CONSTANT_Utf8_info 型常量的结构​

类型

名称

数量

描述

u1

tag

1

标志位 1

u2

length

1

UTF-8 编码的字符串占用字节数

u1

bytes

length

UTF-8 编码的字符串

2 个字节能够表示的最大值为 65535,占用字节数最大不能超过 65535,即要小于 64 KB,否则无法通过编译。

156 ~ 157:0x0015,此字符串字面量占用 21 个字节。

158 ~ 16C:0x284C6A6176612F6C616E672F537472696E673B2956,经 UTF-8 解码,得 (Ljava/lang/String;)V。

使用 javap 反编译工具,加 -verbose 参数输出 class 文件的部分内容:

​码 2.2.2-1 常量表集合​

final class Happy minor version: 0 major version: 52 flags: ACC_FINAL, ACC_SUPER Constant pool: #1 = Methodref #9.#22 // java/lang/Object."<init>":()V #2 = Fieldref #8.#23 // Happy.i:I #3 = String #24 // java #4 = Fieldref #8.#25 // Happy.name:Ljava/lang/String; #5 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream; #6 = String #28 // Hello World #7 = Methodref #29.#30 // java/io/PrintStream.println:(Ljava/lang/String;)V #8 = Class #31 // Happy #9 = Class #32 // java/lang/Object #10 = Utf8 i #11 = Utf8 I #12 = Utf8 name #13 = Utf8 Ljava/lang/String; #14 = Utf8 <init> #15 = Utf8 ()V #16 = Utf8 Code #17 = Utf8 LineNumberTable #18 = Utf8 main #19 = Utf8 ([Ljava/lang/String;)V #20 = Utf8 SourceFile #21 = Utf8 Happy.java #22 = NameAndType #14:#15 // "<init>":()V #23 = NameAndType #10:#11 // i:I #24 = Utf8 java #25 = NameAndType #12:#13 // name:Ljava/lang/String; #26 = Class #33 // java/lang/System #27 = NameAndType #34:#35 // out:Ljava/io/PrintStream; #28 = Utf8 Hello World #29 = Class #36 // java/io/PrintStream #30 = NameAndType #37:#38 // println:(Ljava/lang/String;)V #31 = Utf8 Happy #32 = Utf8 java/lang/Object #33 = Utf8 java/lang/System #34 = Utf8 out #35 = Utf8 Ljava/io/PrintStream; #36 = Utf8 java/io/PrintStream #37 = Utf8 println #38 = Utf8 (Ljava/lang/String;)V

第 1 项常量是方法的符号引用,代表调用 Object 的 <init> 方法;<init> 是构造方法,内部调用父类构造器(Object 除外)、显示初始化实例变量和执行实例语句块(如果有)、执行本类的构造器中的语句。

第 2 项常量是字段的符号引用,说明此字段在 Happy 类中,变量名为 i,int 类型。

第 3 项常量是字符串字面量 “java”,可表示为 java.lang.String 的实例。

第 4 项常量是字段的符号引用,说明此字段在 Happy 类中,变量名为 name,java.lang.String 类型。

...

[1] 空出的一项为 0,将索引值设为 0,代表不引用任何一个常量池项目。

[2] 解析阶段的前期绑定与后期绑定。

[3] 受篇幅原因,其余类型常量的结构,可在 《Java 虚拟机规范》的 4.4 节查看。

2.2.3 访问标志

​表 2.2.3-1 标志​

Address

00

01

02

03

04

05

06

07

08

09

0A

0B

0C

0D

0E

0F

00000160:

00

30

16D ~ 16E:0x0030,访问标志(access_flags),表示类型的修饰符。

​表 2.2.3-2 访问标志​

标志名称

标志值

含义

ACC_PUBLIC

0x0001

public

ACC_FINAL

0x0010

final

ACC_SUPER

0x0020

使用 invokespecial 字节码指令的新语义,JDK 1.0.2 后都为真

ACC_INIERFACE

0x0200

接口类型

ACC_abstract

0x0400

abstract

ACC_SYNIHETIC

0x1000

这个类型并非由用户代码生成

ACC_ANNOTATION

0x2000

注解类型

ACC_ENUM

0x4000

枚举类型

ACC_MODULE

0x8000

模块

例:

0x0001 的二进制为 0000 0000 0000 0001,若类是 public 类型,则第 1 位应为 1,否则为 0。

0x0010 的二进制为 0000 0000 0001 0000,若类是 final 类型,则第 5 位应为 1,否则为 0。

0x0020 的二进制为 0000 0000 0010 0000,若类使用 invokespecial 指令的新语义,则第 6 位应为 1,否则为 0。

...

0x0030 的二进制为 0000 0000 0011 0000,第 5、6 位为 1,代表它是使用 invokespecial 新语义的 final 类,其它位为 0,说明此类型没有 public、abstract 修饰...

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

​表 2.2.4-1 类索引、父类索引、接口索引集合​

Address

00

01

02

03

04

05

06

07

08

09

0A

0B

0C

0D

0E

0F

00000160:

00

00000170:

08

00

09

00

00

16F ~ 170:0x0008 这 2 个字节称为类索引(this_class),用于确定此类的全限定名,指向第 8 项常量。

再回过头来;

29:0x07,十进制为 7,第 8 项常量的标志位,属于 CONSTANT_Class_info 类型。

​表 2.2.4-2 CONSTANT_Class_info 型常量的结构​

类型

名称

数量

描述

u1

tag

1

标志位 7

u2

name_index

1

此索引指向 CONSTANT_Utf8_info 类型的常量,表示类或接口的全限定名

2A ~ 2B:0x001F,十进制为 31,指向第 31 项常量。

E9:0x01,第 31 项常量的标志位,属于 CONSTANT_Utf8_info 类型。

EA ~ EB:0x0005,此字符串字面量占用 5 个字节。(每个英文字符占用 1 个字节)

EC ~ F0:0x4861707079,使用 UTF-8 解码,得 Happy;所以此类的全限定名为 Happy,在默认包下。

​图 2.2.4-1 类索引的查找过程​

class文件存在哪(class文件内容)(1)

171 ~ 172:0x0009 这两个字节称为父类索引(super_class),用于确定父类的全限定名,除了 java.lang.Object 外,所有类都有父类,索引都不为 0。[1]

0x0009,指向第 9 项常量;第 9 项常量属于 CONSTANT_Class_info 类型,它的 name_index 指向第 32 项常量,第 32 项常量属于 CONSTANT_Utf8_info 类型,它的 bytes 经 UTF-8 解码后,为 java/lang/Object。[2]

173 ~ 174:接口数量(interfaces_count),0x0000,转十进制为 0,没有实现任何接口。

若实现了接口,后面每 2 个字节称为一个接口索引,每个接口索引指向某项 CONSTANT_Class_info 类型的常量,对应一个接口的全限定名。

[1] 若是接口类型,父类索引对应的全限定名为 java/lang/Object,接口索引对应的才是此接口继承的父接口全限定名。

[2] class 文件里,所有的 “.” 都被斜杆 “/” 代替,例 “java.lang.Object” 变为 “java/lang/Object”。

2.2.5 字段表集合

​表 2.2.5-1 字段表集合​

Address

00

01

02

03

04

05

06

07

08

09

0A

0B

0C

0D

0E

0F

00000170:

00

02

00

00

00

0A

00

0B

00

00

00

00000180:

00

00

0C

00

0D

00

00

175 ~ 176:字段数量(fields_count),0x0002,类型中声明了 2 个字段。

接下来是字段表(field_info)结构,用于描述字段。

​表 2.2.5-2 字段表结构​

类型

名称

数量

u2

access_flags

1

u2

name_index

1

u2

descriptor_index

1

u2

attributes_count

1

attribute_info

attributes

attributes_count

attributes 是 attribute 的集合,用来存储一些额外的信息,如 java 代码编译成的字节码指令、final 常量值、方法抛出的异常列表...

177 ~ 178:字段访问标志(access_flags),识别字段的修饰符,0x0000。

​表 2.2.5-3 字段访问标志​

标志名称

标志值

含义

ACC_PUBLIC

0x0001

public

ACC_PRIVATE

0x0002

private

ACC_PROTECTED

0x0004

protected

ACC_STATIC

0x0008

static

ACC_VOLATILE

0x0040

volatile

ACC_TRANSIENT

0x0080

transient

ACC_FINAL

0x0010

final

ACC_SYNIHETIC

0x1000

字段由编译器自动生成

ACC_ENUM

0x4000

枚举类型

0x0000 的每位都是 0,此字段没有修饰符。

179 ~ 17A:字段名索引(name_index),用于确定字段名;0x000A,指向第 10 项常量;第 10 项常量是 CONSTANT_Utf8_info 类型,它的 bytes 经解码后为 i,所以此字段名为 i。

17B ~ 17C:字段的描述符索引(descriptor_index),代表字段的描述符,用来描述字段的数据类型。

​表 2.2.5-4 描述符字符含义​

标识字符

含义

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”。

0x000B,指向第 11 项常量,第 11 项常量是 CONSTANT_Utf8_info 类型,它的 bytes 经解码后为 I,所以此字段类型为 int。

17D ~ 17E:属性数量(attributes_count),0x0000,这里为 0,自然就没有属性表了。

综合,此字段为 int i;。

接下来是第 2 个字段;

17F ~ 180:0x0000,此字段无修饰符。

181 ~ 182:0x000C,此字段的字段名索引,指向第 12 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 “name”,字段名为 name。

183 ~ 184:0x000D,此字段的描述符索引,指向第 13 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 “Ljava/lang/String;”,此字段的类型为 java.lang.String。

185 ~ 186:0x0000,此字段没有多余的属性信息。

综合,此字段为 String name;。

​码 2.2.5-1 class 反编译后的字段表内容​

int i; descriptor: I flags: java.lang.String name; descriptor: Ljava/lang/String; flags:

2.2.6 方法表集合

​表 2.2.6-1 方法表集合​

Address

00

01

02

03

04

05

06

07

08

09

0A

0B

0C

0D

0E

0F

00000180:

00

02

00

01

00

0E

00

0F

00

00000190:

01

00

10

00

00

00

30

00

02

00

01

00

00

00

10

2A

000001A0:

B7

00

01

2A

04

B5

00

02

2A

12

03

B5

00

04

B1

00

000001B0:

00

00

01

00

11

00

00

00

0E

00

03

00

00

00

01

00

000001C0:

04

00

02

00

09

00

03

00

09

00

12

00

13

00

01

00

000001D0:

10

00

00

00

25

00

02

00

01

00

00

00

09

B2

00

05

000001E0:

12

06

B6

00

07

B1

00

00

00

01

00

11

00

00

00

0A

000001F0:

00

02

00

00

00

05

00

08

00

06

187 ~ 188:方法数量(methods_count),0x0002,类型中声明了 2 个方法。

接下来是方法表(method_info)结构,用于描述方法。

​表 2.2.6-2 方法表结构​

类型

名称

数量

u2

access_flags

1

u2

name_index

1

u2

descriptor_index

1

u2

attributes_count

1

attribute_info

attributes

attributes_count

189 ~ 18A:方法访问标志,识别方法的修饰符,0x0001,此方法是 public 的。

​表 2.2.6-3 方法访问标志​

标志名称

标志值

含义

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_TRANSIENT

0x0080

transient

ACC_SYNIHETIC

0x1000

由编译器自动生成

18B ~ 18C:0x000E,此方法的方法名索引,指向第 14 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 “<init>”。

18D ~ 18E:0x000F,此方法的描述符索引,指向第 15 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 ()V。

当描述符用来描述方法的参数列表和返回值时,按照先参数列表,后返回值的顺序;如 void some() 的描述符为 ()V,java.lang.String toString() 的描述符为 ()Ljava/lang/String;,void main(String[] args) 的描述符为 ([Ljava/lang/String;)V。

所以 ()V 代表此方法没有形参,返回值类型为 void。

18F ~ 190:0x0001,方法的属性数量为 1。

每个属性应满足如下结构:

​表 2.2.6-4 属性结构​

类型

名称

数量

u2

attribute_name_index

1

u4

attribute_length

1

u1

...

attribute_length

​表 2.2.6-5 不同属性的含义​

属性名称

使用位置

含义

Code

方法表

Java 代码编译成的字节码指令

ConstantValue

字段表

由 final 关键字定义的常量值

Deprecated

类、方法表、字段表

被声明为 deprecated 的方法和字段

Exceptions

方法表

方法抛出的异常列表

EnclosingMethod

类文件

仅当一个类为局部内部类或匿名类时才能拥有此属性,用于标识这个类所在的外围方法

InnerClasses

类文件

内部类列表

...

191 ~ 192:属性名称的索引(attribute_name_index),0x0010,指向第 16 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 “Code”,Code 属性是方法体中的代码的字节码描述。

方法体里面的代码经过 javac 编译后,最终变为字节码指令存储在 Code 属性内,结构如下:

​表 2.2.6-6 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

attributes_count

193 ~ 196:属性值占用的长度(attribute_length),0x00000030,转十进制为 48;从 197 ~ 1C6 都是 Code 属性的内容,这个属性表的长度为 48 6 = 54 个字节。

197 ~ 198:0x0002,操作数栈(Operand Stack)深度的最大值(max_stack)。JVM 运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。

199 ~ 19A:0x0001,局部变量表所需的存储空间(max_locals)。max_locals 的单位是变量槽(Slot),变量槽是 JVM 为局部变量分配内存所使用的最小单位。

不超过 32 位的数据类型:byte、char、float、int、short、boolean、returnAddress 等,每个局部变量占用一个变量槽。

double、long 这两种 64 位的数据类型,使用两个变量槽存放。[1]

19B ~ 19E:0x00000010,字节码长度(code_length)为 16。[2]

19F ~ 1AE:存储字节码指令的一系列字节流(code)[3],0x2AB700012A04B500022A1203B50004B1,每个字节码代表的指令:

​表 2.2.6-7 指令含义​

字节码

助记符

含义

0x2A

aload_0

将局部变量表中的第 1 个变量槽中的引用类型的本地变量推送至栈顶[4]

0xB7

invokespecial

调用栈顶数据所指向的对象的实例初始化方法、父类方法、私有方法,后跟一个 u2 类型的参数说明调用哪个方法

0x0001

u2 类型

这是 invokespecial 指令的参数,代表一个符号引用,指向第 1 项 CONSTANT_Methodref_info 类型的常量:java/lang/Object."\<init>":()V,说明应调用 Object 的无参构造

0x2A

aload_0

将第 1 个变量槽中为引用类型的本地变量推送至栈顶

0x04

iconst_1

将 int 类型的 1 推送至栈顶

0xB5

putfield

为栈顶数据指向的对象的实例变量赋值(显式初始化)

0x0002

u2 类型

这是 putfield 指令的参数,代表一个符号引用,指向第 2 项 CONSTANT_Fieldref_info 型常量:Happy.i:I,应为 Happy 类的 int 类型的变量名为 i 的变量赋值

0x2A

aload_0

将第 1 个变量槽中为引用类型的本地变量推送至栈顶

0x12

ldc

将 int、float 或 String 类型的常量值从常量池中推送至栈顶

0x03

u1 类型

ldc 指令的参数,代表某字面量;此时推送的是第 3 项常量:“java”

0xB5

putfield

为栈顶数据指向的对象的实例变量赋值(显式初始化)

0x0004

u2 类型

这是 putfield 指令的参数,代表一个符号引用,指向第 4 项 CONSTANT_Fieldref_info 型常量:Happy.name:Ljava/lang/String;,应为 Happy 类的 String 类型的变量名为 name 的变量赋值

0xB1

return

从当前方法返回 void

这段字节码代表的含义:调用 Object 的无参构造方法,为 this 指向的对象的 int 类型的实例变量 i 赋值 1,为 this 指向的对象的 String 类型的实例变量 name 赋值 “java”,然后方法结束,返回值为 void。

1AF ~ 1B0:异常表长度(exception_table_length),0x0000,即方法抛出的异常个数为 0。

如果存在异常表,则结构如下:

​表 2.2.6-8 异常表结构​

类型

名称

数量

描述

u2

start_pc

1

开始位置(相对于方法体的位置,下同)

u2

end_pc

1

结束位置

u2

handler_pc

1

处理位置

u2

catch_type

1

此索引指向一个 CONSTANT_Class_info 型的常量

如果当字节码从第 i 行抛出了类型为 catch_type 或其子类的异常,i \in [start_pc,end_pc) ,则转到第 handler_pc 行继续处理;当 catch_type 为 0 时,表示任何异常情况都需要转到 handler_pc 处处理。

以下为例:

​码 2.2.6-1 异常处理源码及反编译后的指令​

public int some() throws Exception { int x; try { x = 1; return x; } catch (Exception e) { x = 2; return x; } finally { x = 3; } }

public int some() throws java.lang.Exception; descriptor: ()I flags: ACC_PUBLIC Code: // {操作数栈最大深度 1} {局部变量表同时生存的局部变量所占最大的槽数 5} {参数个数 1} stack=1, locals=5, args_size=1 // --- try --- // x = 1;对应两个指令 0: iconst_1 // 将 int 类型的常量 1 加载到操作数栈顶 {1} {this} 1: istore_1 // 将栈顶 1 从操作数栈取出,存储到局部变量表的第 2 个变量槽 {} {this, 1} // 将 1 重新读到操作数栈顶,准备返回 2: iload_1 // 将局部变量表的第 2 个变量槽中的值复制到操作数栈顶 {1} {this, 1} // --- finally --- // x = 3; 3: istore_2 // 将 1 取出,存储到第 3 个变量槽中 {} {this, 1, 1} 4: iconst_3 // 将 int 类型的 3 复制到操作数栈顶 {3} {this, 1, 1} 5: istore_1 // 将 3 取出,存储到第 2 个变量槽 {} {this, 3, 1} // 将 1 重新读到操作数栈顶,准备返回 6: iload_2 // 将第 3 个变量槽的值复制到操作数栈顶 {1} {this, 3, 1} 7: ireturn // 方法结束,返回操作数栈顶数 1 // --- catch --- // 捕获异常,给 catch 中定义的异常 e 赋值 8: astore_2 // 将异常取出,存储到第 3 个变量槽中 {} {this, ?, e} // x = 2; 9: iconst_2 // 将常量 2 复制到操作数栈顶 {2} {this, ?, e} 10: istore_1 // 将 2 取出,存储到第 2 个变量槽 {} {this, 2, e} // 将 2 重新读到操作数栈顶,准备返回 11: iload_1 // 将第 2 个变量槽的值复制到操作数栈顶 {2} {this, 2, e} // --- finally --- // x = 3; 12: istore_3 // 将 2 取出,存储到第 4 个变量槽 {} {this, 2, e, 2} 13: iconst_3 // 将常量 3 复制到操作数栈顶 {3} {this, 2, e, 2} 14: istore_1 // 将 3 取出,存储到第 2 个变量槽 {} {this, 3, e, 2} // 将 2 重新读到操作数栈顶,准备返回 15: iload_3 // 将第 4 个变量槽的值复制到操作数栈顶 {2} {this, 3, e, 2} 16: ireturn // 方法结束,返回 2 // --- throw 异常 --- 17: astore 4 // 将异常 e 取出,存储到第 5 个变量槽 {} {this, ?, ?, ?, e} // --- finally --- // x = 3; 19: iconst_3 // 将常量 3 复制到操作数栈顶 {3} {this, ?, ?, ?, e} 20: istore_1 // 将 3 取出,存储到第 2 个变量槽 {} {this, 3, ?, ?, e} // 将异常 e 重新读到操作数栈顶,准备上抛给调用者 21: aload 4 // 将第 5 个变量槽的值复制到操作数栈顶 {e} {this, 3, ?, ?, e} 23: athrow // 抛出异常 Exception table: from to target type // 如果 [0, 4) 出现 Exception 异常,跳转至 8: 0 4 8 Class java/lang/Exception // 如果 [0,4) 出现 Exception 以外的异常,捕获不了,跳转至 17: 0 4 17 any // 如果 [8, 13) 出现任意异常,转到 17: 8 13 17 any

若 try 中没有抛出异常,返回 1;若 try 中抛出的异常可被 catch 捕获,跳转至 8:,返回 2;若抛出的异常没有被捕获,跳转至 17,将异常上抛给调用者;无论是否抛出异常,finally 都会执行。

紧接着是 Code 属性的属性;

1B1 ~ 1B2:0x0001,Code 的属性数量为 1。

1B3 ~ 1B4:属性名称的索引(attribute_name_index),0x0011,指向第 17 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 “LineNumberTable”,此属性描述 Java 源码行号与字节码行号(字节码的偏移量)之间的对应关系。

不是运行时必须的属性,可以编译时使用 -g:none 或 -g:lines 选项取消这项信息。

如果不选择生成 LineNumberTable 属性,当抛出异常时,堆栈中将不会显示出错的行号;在调试程序时,也无法按照源码行来设置断点。

​表 2.2.6-9 LineNumberTable 属性结构​

类型

名称

数量

u2

attribute_name_index

1

u4

attribute_length

1

u2

line_number_table_length

1

line_number_info

line_number_table

line_number_table_length

1B5 ~ 1B8:0x0000000E,属性值长度为 14;从 1B9 ~ 1C6 都是此属性的内容。这个属性表的长度为 14 6 = 20 个字节,从 1B3 ~ 1C6。

1B9 ~ 1BA:0x0003,行号表长度为 3。

​表 2.2.6-10 line_number_info 结构​

类型

名称

数量

u2

start_pc

1

u2

line_number

1

1BB ~ 1BC:0x0000,字节码行号 0。

1BD ~ 1BE:0x0001,源码行号 1。

1BF ~ 1C0:0x0004,字节码行号 4。

1C1 ~ 1C2:0x0002,源码行号 2。

1C3 ~ 1C4:0x0009,字节码行号 9。

1C5 ~ 1C6:0x0003,源码行号 3。

Code 属性结束,此方法结束。

接着是下一个方法表;

1C7 ~ 1C8:方法的访问标志 0x0009,第 1、4 位为 1,此方法是 public、static 修饰的。

1C9 ~ 1CA:方法名索引 0x0012,指向第 18 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 “main”。

1CB ~ 1CC:0x0013,此方法的描述符索引,指向第 19 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 ([Ljava/lang/String;)V,一个返回值类型为 void,形参为 String[] 类型的方法。

1CD ~ 1CE:0x0001,方法的属性数量为 1。

1CF ~ 1D0:0x0010,属性名称的索引,指向第 16 项 CONSTANT_Utf8_info 类型的常量,此常量的 bytes 经解码后为 “Code”。

1D1 ~ 1D4:0x00000025,属性值长度为 37:1D5 ~ 1F9。

1D5 ~ 1D6:0x0002,操作数栈的深度的最大值为 2。

1D7 ~ 1D8:0x0001,局部变量表的存储空间为 1。

1D9 ~ 1DC:0x00000009,存储字节码指令的字节长度为 9。

1DD ~ 1E5:0xB200051206B60007B1;

​表 2.2.6-11 一系列字节码指令代表的含义​

字节码

助记符

含义

0xB2

getstatic

获取指定类的类变量,并将其值压入操作数栈顶

0x0005

u2

getstatic 指令的参数,代表一个符号引用,指向第 5 项 CONSTANT_Fieldref_info 类型的常量:java/lang/System.out:Ljava/io/PrintStream;,说明获取的是 System 类型的 out 变量

0x12

ldc

将 int、float、String 型常量值从常量池中复制到操作数栈顶

0x06

u1

ldc 指令的参数,代表某字面量,指向第 6 项 CONSTANT_String_info 类型的常量:“Hello World”

0xB6

invokevirtual

调用栈顶引用类型的数据的实例方法

0x0007

u2

invokevirtual 指令的参数,指向第 7 项 CONSTANT_Methodref_info 类型的常量:java/io/PrintStream.println:(Ljava/lang/String;)V,代表调用 PrintStream 类的 println(String)方法。

0xB1

return

从当前方法返回 void

综上,这段字节码对应的源码应是 System.out.println("Hello World");

1E6 ~ 1E7:异常表长度,0x0000,即方法抛出的异常个数为 0。

紧接着是 Code 属性的属性;

1E8 ~ 1E9:0x0001,属性数量为 1。

1EA ~ 1EB:0x0011,属性名称的索引,指向第 17 项常量:“LineNumberTable”。

1EC ~ 1EF:0x0000000A,属性值长度为 10:1EF ~ 1F8。

1F0 ~ 1F1:0x0002,行号表长度为 2。

1F2 ~ 1F3:0x0000,字节码行号 0。

1F4 ~ 1F5:0x0005,源码行号 5。

1F6 ~ 1F7:0x0008,字节码行号 8。

1F8 ~ 1F9:0x0006,源码行号 6。

​码 2.2.6-2 方法表集合​

public Happy(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field i:I 9: aload_0 10: ldc #3 // String java 12: putfield #4 // Field name:Ljava/lang/String; 15: return LineNumberTable: line 1: 0 line 2: 4 line 3: 9 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #6 // String Hello World 5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 5: 0 line 6: 8 } SourceFile: "Happy.java"

[1] 并不是在方法中用了多少个局部变量,就把这些局部变量所占变量槽数量之和作为 max_locals 的值。

操作数栈和局部变量表直接决定一个该方法的栈帧所耗费的内存,不必要的操作数栈深度和变量槽数量会造成内存的浪费。

Java虚拟机的做法是将局部变量表中的变量槽进行重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的变量槽可以被其他局部变量所复用,javac 编译器会根据变量的作用域来分配变量槽给各个变量使用,根据同时生存的最大局部变量数量和类型计算出 max_locals 的大小。

[2] 虽然它是一个 u4 类型的长度值,理论上最大值可以达到 232,但是《Java虚拟机规范》中明确限制了一个方法不允许超过 65535 条字节码指令,即它实际只使用了 u2 的长度,如果超过这个限制,javac 编译器就会拒绝编译。

[3] code 存储源代码编译后生成的字节码指令。每个字节码指令就是一个 u1 类型的单字节,当虚拟机读取到 code 中的一个字节时,就可以对应找出这个字节代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及后续的参数应当如何解析。

1 个字节最多表示 256 条指令。目前,《Java虚拟机规范》已经定义了其中约 200 条编码值对应的指令含义,编码与指令之间的对应关系可查阅《深入理解 Java 虚拟机》的附录 C:“虚拟机字节码指令表”。

[4] 表面上无参,其实在任何实例方法里,都可以通过 “this” 关键字访问到此方法所属的对象。

在 javac 编译时,把对 this 关键字的访问转变为对一个普通方法参数的访问,在 JVM 调用实例方法时自动传入此参数。

因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留第一个变量槽来存放对象实例的引用。

2.2.7 属性表集合

接下来是类文件的属性。

​表 2.2.7-1 属性表集合​

Address

00

01

02

03

04

05

06

07

08

09

0A

0B

0C

0D

0E

0F

000001F0:

00

01

00

14

00

00

000001E0:

00

02

00

15

1FA ~ 1FB:0x0001,属性个数为 1;

1FC ~ 1FD:0x0014,属性名称的索引(attribte_name_index),指向第 20 项 CONSTANT_Utf8_info 类型的常量:“SourceFile”,此属性用于记录生成 class 文件的源码文件名称。

此属性是可选的,可以使用 -g:none 或 -g:source 选项取消这项信息。

如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。

​表 2.2.7-2 SourceFile 属性结构​

类型

名称

数量

u2

attribute_name_index

1

u4

attribute_length

1

u2

sourcefile_index

1

1FE ~ 1E1:0x00000002,属性值长度(attribute_length)为 2。

1E2 ~ 1E3:0x0015,源文件名索引(sourcefile_index),指向第 21 项 CONSTANT_Utf8_info 型常量:“Happy.java”;所以源文件名应为 “Happy”。

对 class 文件的解析到此结束,下面是从 《深入理解 Java 虚拟机》摘抄的一部分属性表。

Exceptions 属性

例举方法声明抛出的异常,位于方法表结构的属性表中。

​表 2.2.7-3 Exceptions 属性结构​

类型

名称

数量

u2

attribute_name_index

1

u4

attribute_length

1

u2

number_of_exceptions

1

u2

exception_index_table

number_of_exceptions

number_of_exceptions:方法声明抛出的异常的数量

exception_index_table 中的每个成员都是对常量池的有效索引,指向 CONSTANT_Class_info 型常量,表示异常的类型。

LocalVariableTable 属性

描述栈帧中局部变量表的变量与源码定义的变量之间的关系,在 Code 属性的属性表中。

不是运行必需的属性,可以编译时使用参数 -g:none 或 -g:vars 取消此项信息。

影响:所有参数名称都会丢失,IDE 会使用 arg0、arg1 之类的占位符代替原有参数名,在调试期间无法根据参数名称从上下文中获取参数值。

​表 2.2.7-4 LocalVariableTable 属性结构​

类型

名称

数量

u2

attribute_name_index

1

u4

attribute_length

1

u2

local_variable_table_length

1

local_variable_info

local_variable_table

local_variable_table_length

local_variable_table_length:局部变量个数。

​表 2.2.7-5 local_variable_table 表结构​

类型

名称

数量

u2

start_pc

1

u2

length

1

u2

name_index

1

u2

descriptor_index

1

u2

index

1

start_pc:局部变量开始的字节码偏移量

length:作用范围覆盖的长度

局部变量的作用域在 [start_pc, start_pc length) 内。

name_index:局部变量名索引

descriptor_index:局部变量的描述符索引

index:局部变量在栈帧的局部变量表中变量槽的位置。当变量数据类型大于 32 位时,它占用的变量槽为 index、index 1 这两个。

JDK5 引入泛型后,新增属性 LocalVariableTypeTable,仅将 descriptor_index 替换了字段的特征签名(Signature)[1],signature_index 是一个索引,指向 CONSTANT_Utf8_info 型常量。

ConstantValue 属性

被 final 修饰的字段,在声明时使用字面量的方式赋值,field_info 结构的属性表中会生成此项属性,目的是通知 JVM 自动为类变量赋值。[2]

​码 2.2.7-1 声明时不赋值​

final static int I; static { I = 4; }

此字段表不会有 ConstantValue 属性,在 <clinit> 方法中赋值。

​码 2.2.7-2 非字面量形式赋值:调用构造器​

final static String STR = new String("Hello");

也不会有 ConstantValue 属性,在 <clinit> 方法中赋值。

​码 2.2.7-3 非字面量形式赋值:调用方法​

final static Integer I = 3;

隐式调用 Integer.valueOf(3)方法,不会生成 ConstantValue 属性,在 <clinit> 方法中赋值。

访问变量,必须通过指向 CONSTANT_Fieldref_info 的索引,定位到对象地址,再获取此对象的某个字段值。

访问常量,直接从常量池中获取值(只包括基本数据类型和字符串字面量,如 CONSTANT_Integer_info、CONSTANT_String_info 等字面量类型)。

​码 2.2.7-4 变量赋值​

int i1 = 3; public void some() { int i2 = i1; }

0: aload_0 1: getfield #2 // Field i1:I 4: istore_1 5: return

​码 2.2.7-5 常量赋值​

final int i1 = 3; public void some() { int i2 = i1; }

0: iconst_3 1: istore_1 2: return

​表 2.2.7-6 ConstantValue 属性结构​

类型

名称

数量

u2

attribute_name_index

1

u4

attribute_length

1

u2

constantvalue_index

1

constantvalue_index:此索引指向常量池中一个字面量常量,根据字段类型不同,可以是 CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、 CONSTANT_Integer_info和CONSTANT_String_info 常量中的一种。

BootstrapMethods 属性

此属性用于保存 invokedynamic 指令引用的引导方法限定符。

​表 2.2.7-7 BootstrapMethods 属性结构​

类型

名称

数量

描述

u2

attribute_name_index

1

指向 CONSTANT_Utf8_info 型常量,值为 “BootstrapMethods”

u4

attribute_length

1

属性长度,不包括开始的 6 个字节

u2

num_bootstrap_methods

1

引导方法的数量

bootstrap_method_info

bootstrap_method_table

num_bootstrap_methods

引导方法表

​表 2.2.7-8 bootstrap_method_table 结构​

类型

名称

数量

描述

u2

bootstrap_method_ref

1

指向 CONSTANT_MethodHandle_info 型常量

u2

num_bootstrap_arguments

1

引导方法参数数量

u2

bootstrap_argument

num_bootstrap_arguments

索引指向某项常量

0x0035:转十进制为 53,指向第 53 项 CONSTANT_Utf8_info 型常量,bytes 解码为 “BootstrapMethods”。

0x0000000C:转十进制为 12,属性剩余长度为 12 个字节。

0x0001:引导方法数量为 1。

接下来是第一个引导方法:

0x0036:转十进制为 54,指向第 54 项 CONSTANT_MethodHandle_info 型常量,代表一个方法句柄,REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(...)... 代表调用某静态方法。

0x0003:引导方法参数数量为 3。

0x003D:转十进制为 61,指向第 61 项 CONSTANT_MethodType_info 型常量,代表一个方法类型,(I)V 一个int 类型的形参、返回值类型为 void 的方法。

0x003E:转十进制为 62,指向第 62 项 CONSTANT_MethodHandle_info 型常量,REF_invokeStatic com/cqh/arr3/Test.lambda$main$0:(I)V 调用 Test 类型的 lambda... 方法。

0x003D:转十进制为 61...

[1] 引入泛型后,字段的描述符中泛型的参数化类型被擦除,不能准确描述泛型类型。如 List<String> list 的描述符 descriptor 为 “Ljava/util/List”;

特征签名(Signature)多了一项参数化类型的信息: “Ljava/util/List<Ljava/lang/String;>;”。

扩展:

在 Java 代码层面上的方法特征签名只包括方法名称和参数的个数、类型、顺序;而在字节码中的特征签名还包括方法返回值及受检异常表,方法的描述符包括参数列表和返回值。

方法特征签名最重要的任务就是作为方法独一无二不可重复的 ID。重载(Overload)即方法名相同,但特征签名不同。

[2] 如果 field_info 结构表示的非静态字段(如实例变量)包含了 ConstantValue 属性,那么这个属性必须被虚拟机所忽略。

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页