java class文件常量池_Java Class文件结构:常量池

Java Class文件结构:常量池

1 概念的引入

Java的Class文件在描述类、属性、方法等时要用到一系列的“常量”。比如下面的属性:

int var;

描述这个属性,需要两个常量:

属性名称“var”是一个字符串常量;

属性的数据类型是int,在class文件中通过单个大写字母I(这也是一个字符串常量)表示int;

因此,上述的属性就可以通过两个字符串常量来描述(不包括冒号):

var:I

通过常量来描述类、属性、方法等的好处大概有三个

1

who knows?

复用常量,压缩class文件的尺寸。比如描述int m;这个属性时,就可以复用I(表示int)这个字符串了。

可使用精简的符号表示复杂的含义,比如上述的I表示int,进一步压缩class文件的尺寸。

方便扩展。当需要定义新的常量时,只需要在常量池中添加即可。

2 一些术语

2.1 全限定名(Fully Qualified Name)

全限定名=包名+类/接口的名称。显然,只有全限定名才能唯一定位类或者接口的位置,因此在class文件中一律使用全限定名。

但是,由于当初设计class文件时考虑不周,class文件内部使用的全限定名被定义为使用/分割,比如java/lang/Object,这和大家习惯的包名表达方式(java.lang.Object)不一致。不得已,JVM规范将class内部使用的全限定名称为“internal form”(内部形式)的全限定名。由于本文仅涉及class的内部结构,因此引用到全限定名时皆指诸如java/lang/Object的内部形式。

2.2 描述符(Descriptor)

在表达属性、方法时,除了其名称(全限定名)外,其他的特性刻画需要依靠描述符(Descriptor)。

2.2.1 属性描述符

属性描述符仅表达了属性的数据类型。1列出了属性描述符中数据类型的表示方式。大部分的数据类型描述方式一目了然,和数据类型的首字符是一一对应的。下面重点阐述对象、数组的描述符。

对象的描述符要特别注意两点:一是类名使用全限定名,二是最后的分号(;)

2

为什么对象的描述符最后要有一个这样的小尾巴呢?因为在属性描述符中,其他数据类型都是单个字母,很容易确定整个属性描述符的界限。但是在对象描述符中,类的全限定名长度是可变的,无法事先确定,因此只好使用分号(;)来表示对象的描述符的结束。其实在这里,能够表示对象的描述符结束的符号很多,只要不是常规字母和/即可,至于为什么选择了分号作为结束符,尚未见考证材料。

。比如属性定义:String str的描述符为:Ljava/lang/String;。

多维数组的描述符使用了多个[来表达,比如double[][]的描述符为[[D。

我们在方法描述符中会大量使用到属性描述符来描述参数列表。

描述符

数据类型

说明

B

byte

字节

C

char

字符

D

double

双精度

F

float

单精度

I

int

整数

J

long

长整数

L ClassName ;

reference

ClassName对象

S

short

短整数

Z

boolean

true/false

[

reference

一维数组

表 1: 属性描述符的数据类型定义

2.2.2 方法描述符

方法的描述符是指描述方法的返回值和参数列表的字符串,其中参数列表在前,返回值紧跟其后。其中,参数列表为空只给出()即可。返回值的类型除了常规的数据类型如1之外,如果为void使用V表示,这一点比较特殊,需要注意一下。2给出了几个示例。

方法定义

方法描述符

String doSomething(int a, double b,Thread t){...}

(IDLjava/lang/Thread;)Ljava/lang/String

void doSomething(int a,double b,Thread t){...}

(IDLjava/lang/Thread;)V

void doSomething(int[][] a)

([[I)V

表 2: 方法描述符示例

这里引申出一个比较有意思的问题:Java的方法允许有多少个参数[1, p92][2]?结论是,Java的方法最多允许255个单位的参数,其中int等类型的参数算作一个单位,long和double类型的参数算作2个单位

3

为什么long和double算作两个2单位?这大概和JVM设计之初的一个不合理规定有关,参见xxx。

。这个问题作者还没有搞的太清楚,应该需要从Java虚拟机调用方法的栈机制去调查,从方法描述符的限制中找不到线索。

3 常量池的总体结构

常量池的总体结构如3所示,一个u2表示常量池的表项数量,后面紧跟着constant_pool_length个表项。这里要注意两点:第一,constant_pool_length不是常量池的总字节尺寸,而是表项数量。表项结构各不相同,因此常量池的总长度由所有表项的长度相加计算而得;第二,constant_pool_length的索引是从1开始的,因此真正的表项数量是constant_pool_length – 1。如1所示,我们的示例代码Person.class中,常量池表项的数量应为:0x0014 – 1项,即19项。

38c0dd11c70fcbc51fa85aba780be80a.png

图 1: 常量池表项数量

数据类型

意义

数量

u2

constant_pool_length

1

u1

info[]

constant_pool_length – 1

表 3: 常量池的总体结构

到目前为止(Java SE 11),常量池共有17种常量表项,见4,每一种常量表项都有特定的数据结构。常量表项数据结构的第一个字节用于标识不同的表项,称为“标志”(Tag),比如Utf8编码的字符串表项(CONSTANT_Utf8_info)的标志为1,参见5。

常量表项类型

标志

Java SE

描述

CONSTANT_Utf8_info

1

1.0.2

Utf8编码的字符串

CONSTANT_Integer_info

3

1.0.2

整数

CONSTATN_Float_info

4

1.0.2

单精度浮点数

CONSTANT_Long_info

5

1.0.2

长整数

CONSTANT_Double_info

6

1.0.2

双精度浮点数

CONSTANT_Class_info

7

1.0.2

CONSTANT_String_info

8

1.0.2

字符串

CONSTANT_Fieldref_info

9

1.0.2

属性

CONSTANT_Methodref_info

10

1.0.2

方法

CONSTANT_InterfaceMethodref_info

11

1.0.2

接口方法

CONSTANT_NameAndType_info

12

1.0.2

名称和类型

CONSTANT_MethodHandler_info

15

7

方法句柄?

CONSTANT_MethodType_info

16

7

方法类型

CONSTANT_Dynamic_info

17

11

CONSTANT_InvokeDynamic_info

18

7

CONSTANT_Module_info

19

9

模块

CONSTANT_Package_info

20

9

表 4: 常量池的表项类型

常量池实际上是class文件的“符号体系”,所有在表达class文件的结构、意义和Java语法时会用到的“词汇”,均需要在常量池中定义,因此常量池的分量很重,往往是一个class文件的主要内容。常量池的这种设计也非常巧妙,通过扩展表项即可扩展Java语言的表达能力。

4 Utf8字符串常量

字符串常量是常量池中使用量最大的常量,其他的常量表项也经常引用字符串常量,比如CONSTANT_NameAndType_info,CONSTANT_Class_info等都会引用到字符串常量。众所周知,Java中的字符串是Utf8编码的,而Utf8是可变长度字符编码,使用一到六个字节为每个字符编码

4

参见:https://en.wikipedia.org/wiki/UTF-8

。因此Utf8编码的字符串表项(CONSTANT_Utf8_info)结构如5所示,需要使用一个u2类型的数据说明字符串的实际长度。

数据类型

描述

备注

u1

tag

标签=1

u2

length

字符串的实际长度

u1

bytes[length]

字符串

表 5: Utf8编码的字符串表项结构

Person.class中的其中一个(第一个)Utf8字符串常量如2所示。实际上,根据ghex这个软件右栏的提示,我们很容易找到其他的Utf8编码的字符串常量:本例中的Utf8编码常量均是一个字节的ASCII码字符,因此即很方便的显示在了ghex的右栏,也很容易通过数字节的方式识别出来。

a7cbdea1d8d7e0510a2f829ac703219c.png

图 2: Person.class的Utf8字符串常量示例

实际上,通过java -p更容易识别常量池项目。4是java -p的输出结果中关于常量池的部分,可以一目了然的看到,在常量池中共有19个表项,其中13个是Utf8编码的字符串常量,这些常量也很容易从hgex的右栏找出,如3所示。

26c6fcc9cdb4280d6fe8ecf87ef076a7.png

图 3: 所有的Utf8编码的字符串常量

Constant pool:

#1 = Methodref #4.#16 // java/lang/Object."":()V

#2 = Fieldref #3.#17 // Person.age:I

#3 = Class #18 // Person

#4 = Class #19 // java/lang/Object

#5 = Utf8 age

#6 = Utf8 I|\longremark{属性的数据类型:int}|

#7 = Utf8 |\longremark{构造方法}|

#8 = Utf8 ()V|\longremark{方法的descriptor,这里是一个参数列表为空,返回值为void的方法描述符}|

#9 = Utf8 Code

#10 = Utf8 LineNumberTable

#11 = Utf8 isAdult

#12 = Utf8 ()Z|\longremark{方法的描述符(descriptor),参数列表为空,返回值为boolean}|

#13 = Utf8 StackMapTable

#14 = Utf8 SourceFile

#15 = Utf8 Person.java

#16 = NameAndType #7:#8 // "":()V|\longremark{由\#7和\#8号Utf8字符串组成的名称和类型常量表项}|

#17 = NameAndType #5:#6 // age:I|\longremark{由\#5和\#6号Utf8字符串常量表项组成的名称和类型常量表项}|

#18 = Utf8 Person

#19 = Utf8 java/lang/Object

|\showremarks|

5 整数常量(Integer)和单精度浮点数(Float)常量

CONSTANT_Integer_info和CONSTANT_Float_info的结构类似,如6所示。

数据类型

描述

备注

u1

tag

标签=3

u4

bytes

整数

表 6: 整数常量的表项结构

数据类型

描述

备注

u1

tag

标签=4

u4

bytes

单精度浮点数

表 7: 单精度浮点数常量的表项结构

Person.class中没有整数常量。

6 长整数(Long)和双精度浮点数(Double)常量

长整数(Long)和双精度浮点数(Double)都是8个字节存储的,因此其在常量池中的数据结构分别如8和9所示。

数据类型

描述

备注

u1

tag

标签=5

u4

high_bytes

u4

low_bytes

表 8: 长整数常量的表项结构

数据类型

描述

备注

u1

tag

标签=6

u4

high_bytes

u4

low_bytes

表 9: 双精度浮点数常量的表项结构

Person.class中没有长整数常量和双精度浮点数常量。

7 类(Class)常量

整个class文件不就表示了一个类吗?这里的类常量只是表达了类的名称而已,其结构如10所示。类名的索引指向了一个Utf8编码的字符串常量,因此Person.class的类常量如4所示,其中Person是类本身,java/lang/Object是Person的父类。我们知道,如果没有特别声明父类,则每一个类(Object本身除外)都会自动从Object继承下来,所以在常量池中一定会存在Object这个类常量。

48ab91e1c2941219fb75bed392f301d5.png

图 4: Person.class中的类常量示例

数据类型

描述

备注

u1

tag

标签=7

u2

name_index

类名的索引

表 10: 类常量的表项结构

接口也是通过类常量来表示的。在《从C到Java》一书中,作者曾有阐述,接口就是纯的抽象类,即接口其实是类的一种。类常量可以用来表达接口更加确认了这一点。比如7所示的简单的接口定义,其javap -v的输出如2所示,其常量池有两个类常量,一个是IPerson,一个是java/lang/Object,即IPerson接口是从Object继承下来的:IPerson是一个经过包装的抽象类。

public interface IPerson{

boolean isAdult();

}

Classfile /home/subaochen/git/blog/src/java/IPerson.class

Last modified 2018年12月9日; size 119 bytes

MD5 checksum 2b865ff3610a20333b6df52384c1c38d

Compiled from "IPerson.java"

public interface IPerson

minor version: 0

major version: 55

flags: (0x0601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT

this_class: #1 // IPerson

super_class: #2 // java/lang/Object

interfaces: 0, fields: 0, methods: 1, attributes: 1

Constant pool:

#1 = Class #7 // IPerson

#2 = Class #8 // java/lang/Object

#3 = Utf8 isAdult

#4 = Utf8 ()Z

#5 = Utf8 SourceFile

#6 = Utf8 IPerson.java

#7 = Utf8 IPerson

#8 = Utf8 java/lang/Object

{

public abstract boolean isAdult();

descriptor: ()Z

flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT

}

SourceFile: "IPerson.java"

8 字符串(String)常量

字符串常量用来表示一个String的实例,其指向了一个Utf8编码的字符串常量,其数据结构如11所示。

数据类型

描述

备注

u1

tag

标签=8

u2

string_index

指向Utf8编码的字符串常量

表 11: 字符串常量结构

注意区分字符串常量表项(CONSTANT_String_info)和Utf8编码的字符串常量表项(CONSTANT_Utf8_info)的不同用途:Utf8编码的字符串常量表项用来表示最终的字符串,也就是说,所有的字符串最终都是存储在Utf8编码的字符串常量表项中的;而字符串常量表项,仅用来表示String的实例所代表的字符串的索引,其真正的内容保存在Utf8编码的字符串常量表项中。

如果类中没有使用到String的实例,就不存在字符串常量表项,比如Person.java类。但是,每一个类中一定存在若干个Utf8编码的字符串常量表项用来表达类名、字段名等等。

9 名称和类型(NameAndType)常量

字段和方法通过描述符表达了字段和方法的类型,再加上名称(Name),就构成了CONSTANT_NameAndType_info,如12所示,其中name_index和descripter_index分别指向Utf8编码的字符串常量。

数据类型

描述

备注

u1

tag

标签=12(0x0C)

u2

name_index

字段和属性的名称索引

u2

descriptor_index

字段和属性的描述符索引

表 12: 名称和类型常量结构

Person.class中的名称和类型常量表项如5所示,第一个名称和类型常量表项由#7()和#8(()V)Utf8字符串常量表项组成,表达了构造方法的名称和类型;第二个名称和类型cl表项由#5(age)和#6(I)Utf8字符串常量表项组成,表达了字段age的名称和类型。

3a8a49c9591509f0f15d5aa6c2e090f9.png

图 5: 名称和类型常量表项示例

从javap -v Person.class的输出中,更容易找到这两个名称和类型常量表项,如4所示。

10 字段、方法和接口方法常量

字段、方法以及接口的方法常量表项非常类似,如13、14、15所示。

数据类型

描述

备注

u1

tag

标签=9

u2

class_index

类名称索引

u2

name_and_type_index

名称和类型索引

表 13: 字段常量结构

数据类型

描述

备注

u1

tag

标签=10(0x0A)

u2

class_index

类名称索引

u2

name_and_type_index

名称和类型索引

表 14: 方法常量结构

数据类型

描述

备注

u1

tag

标签=11(0x0B)

u2

class_index

类名称索引

u2

name_and_type_index

名称和类型索引

表 15: 接口方法常量结构

Person.class的字段常量表项如6所示,方法常量表项如7所示。结合javap -v person.class的输出更容易解读这两个常量表项,此处截取片段说明:

#1 = Methodref #4.#16 // java/lang/Object."":()V

#2 = Fieldref #3.#17 // Person.age:I

#3 = Class #18 // Person

#4 = Class #19 // java/lang/Object

...

#16 = NameAndType #7:#8 // "":()V

#17 = NameAndType #5:#6 // age:I

Person.class中没有接口方法常量表项。

0b8bac81f4534912e96457a72bdca77b.png

图 6: Person.class的字段常量表项示例

c132d48b944df4352db586ae6168754e.png

图 7: Person.class中的方法常量表项示例

由于类的方法和接口的方法使用了不同的常量表项来描述,因此方法常量表项中的class_index必须是类,不能是接口;同样的,接口方法常量表项中的class_index必须是接口,不能是类。

字段常量表项中的class_index即可以是类,也可以是接口。

11 方法句柄(MethodHandle_info)常量表项

数据类型

描述

备注

u1

tag

标签=15(0x0F)

u21

reference_kind

u2

reference_index

表 16: 方法句柄常量结构

12 方法类型(MethodType_info)常量表项

这个表项很简单,代表了方法的类型,即方法的描述符,其结构如17所示。但是这个常量和NameAndType_info不是重复了吗?MethodType_info在什么地方会用到呢?有待进一步考察。

数据类型

描述

备注

u1

tag

标签=16(0x10)

u2

descriptor_index

方法描述符索引

表 17: 方法类型常量结构

13 模块(Module_info)常量表项

模块常量用来表示一个模块,其结构如所示,其中name_index指向一个存储了模块名称的Utf8编码的字符串。

数据类型

描述

备注

u1

tag

标签=19(0x13)

u2

name_index

模块名称索引

表 18: 模块常量结构

14 包(Package_info)常量表项

包常量表示一个模块中输出或者打开的包,其结构如所示,其中name_index指向一个存储了包名称的Utf8编码的字符串。

数据类型

描述

备注

u1

tag

标签=20(0x14)

u2

name_index

包名称索引

表 19: 包常量结构

15 CONSTANT_Dynamic_info和CONSTANT_InvokeDynamic_info常量表项

TBD

引用

1Oracle, “JVM specification“.

2汪先生, “你知道Java方法能定义多少个参数吗?” (2018).

d4c39bd54dfbfe627dbd9fe226767b2c.png

0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值