[java]JVM之运行时常量池里到底有什么

1. 概念

首先我们来复习一下java内存模型,java运行时数据区大概分为五块,分别是

  • 方法区
  • 虚拟机栈
  • 本地方法栈
  • 程序计数器

而运行时常量池是方法区的一部分,文字解释:

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(ConstantPool Table), 用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

从这段描述中我们可以得出结论,运行时常量池里面存放的是从Class文件中的常量池表中加载到的数据,为了搞清楚运行时常量池里有什么,我们需要搞清楚对应常量池表里面有什么

2. 常量池表

2.1 Class文件的数据类型

先说一下Class的文件格式:Class文件的文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型,“无符号数”和“表”。

  • 无符号数属于基本的数据类型,以u1/u2/u4/u8来分别代表1个字节、2个字节、4个字节、8个字节的的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按utf-8编码构成字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的符合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表。

2.2 常量池

常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据。

常量池中主要存放两大数据:字面量和符号引用。字面量比较接近Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。

2.2.1 符号引用

由于Java代码在进行Javac编译的时候,并不像C/C++那样有“连接”这一步骤,而是在虚拟机加载Clsss文件的时候进行动态连接,因此,在我们将Java代码编译成Class文件后,Class文件并不会保存方法、字段等在内存中的布局。为了解决这个问题,Class文件会在常量池内保存方法、字段等的符号引用。所谓符号引用,我们可以简单的理解为真正内存布局的占位符,在类加载过程的解析阶段,符号引用会被替换为真正的直接引用。

2.2.2 常量池的结构

常量池中每一项常量都是一个表,这些表都有一个共同的特点,即表结构的起始第一位为一个u1类型的标志位,代表着当前常量属于哪一种常量类型

常量池中的项目类型

项目 类型 描述
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_InvkoeDynamic_info 18 表示一个动态方法调用点
CONSTANT_Module_info 19 表示一个模块
CONSTANT_Package_info 20 表示一个模块中开放或者导出的包

具体表信息和表结构如下:

2.2.2.1 CONSTANT_Utf8_info

类型 标志 描述
tag u1 值为1
length u2 UTF-8编码的字符串占用的字节数
bytes u1 长度为length的UTF-8编码的字符串,总共length个

2.2.2.2 CONSTANT_Integer_info

项目 类型 描述
tag u1 值为3
bytes u4 按照高位在前存储的int值

2.2.2.3 CONSTANT_Float_info

项目 类型 描述
tag u1 值为4
bytes u4 按照高位在前存储的float值

2.2.2.4 CONSTANT_Long_info

项目 类型 描述
tag u1 值为5
bytes u8 按照高位在前存储的long值

2.2.2.5 CONSTANT_Double_info

项目 类型 描述
tag u1 值为6
bytes u8 按照高位在前存储的double值

2.2.2.6 CONSTANT_Class_info

项目 类型 描述
tag u1 值为7
index u2 指向全限定名常量项的索引

2.2.2.7 CONSTANT_String_info

项目 类型 描述
tag u1 值为8
index u2 指向字符串字面量的索引

2.2.2.8 CONSTANT_Fieldref_info

项目 类型 描述
tag u1 值为9
index u2 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项
index u2 指向字段描述符CONSTANT_NameAndType的索引项

2.2.2.9 CONSTANT_Methodref_info

项目 类型 描述
tag u1 值为10
index u2 指向声明方法的类或者接口描述符CONSTANT_Class的索引项
index u2 指向名称及类型描述符CONSTANT_NameAndType的索引项

2.2.2.10 CONSTANT_InterfaceMethodref_info

项目 类型 描述
tag u1 值为11
index u2 指向声明方法的接口描述符CONSTANT_Class的索引项
index u2 指向名称及类型描述符CONSTANT_NameAndType的索引项

2.2.2.11 CONSTANT_NameAndType_info

项目 类型 描述
tag u1 值为12
index u2 指向该字段或方法名称常量项的索引
index u2 指向该字段或方法描述符常量项的索引

2.2.2.12 CONSTANT_MethodHandle_info

项目 类型 描述
tag u1 值为15
reference_kind u1 值必须在1至9之间[1-9]它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为
reference_index u2 值必须是对敞亮吃的有效索引

2.2.2.13 CONSTANT_MethodType_info

项目 类型 描述
tag u1 值为16
descriptor_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符

2.2.2.14 CONSTANT_Dynamic_info

项目 类型 描述
tag u1 值为17
bootstrap_method_attr_index u2 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引
name_and_type_index u2 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符

2.2.2.15 CONSTANT_InvkoeDynamic_info

项目 类型 描述
tag u1 值为18
bootstrap_method_attr_index u2 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引
name_and_type_index u2 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符

2.2.2.16 CONSTANT_Module_info

项目 类型 描述
tag u1 值为19
name_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示模块名称

2.2.2.17 CONSTANT_Package_info

项目 类型 描述
tag u1 值为19
name_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示包名称

3. 查看常量池表

如果我们使用文本编辑器打开某个Class文件,那么你见到的场景大概是这样的:


image.png

除了魔数CAFE BABE 其他的信息阅读起来可能会有很大的困难。好在Oracle为我们提供了一个专门用于分析Class文件字节码的工具:javap。javap的使用方式:

javap -verbose xxxx.class

简单写一个java类Test, 代码如下:

public class Test {
    String test = "dafa";
    String test1 = "soft";
    String test3 = "dafasoft";

    public Test() {
    }

    void Test() {
        this.testFun();
    }

    public void testFun() {
    }

    public void testFun1() {
    }

    public void testFun2() {
    }

    public static void main(String[] args) {
    }
}

我们编译后使用javap命令,观察一下它的常量池表是什么样的。它的常量池表截图如下:


image.png

javap工具为我们自动加了注释,但实际上如果我们把上面的表结构看完,不加注释也是看明白的
举两个例子:

3.1. 示例一:String test3 = "dafasoft";

这是我们在java类中定义的字符串字面常量,如何在常量池表中寻找呢?首先,它是一个Field,我们首先寻找Fieldref,上图的表中共有三个Fieldref,我们逐一查找即可。

根据 第2.2.2.8章节的表结构我们知道,Filedref后面的两个值是两个index, 分别指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项指向字段描述符CONSTANT_NameAndType的索引项,比如 #7处的Fieldref对应的值为#9.#40, #9 对应的值为#42, #42对应的值字符串“com/dafasoft/test/Test”; #40对应的值为#14:#12, #14对应的值为字符串test3, #12对应的值为字符串“Ljava/lang/String;”, 这些信息频道一起,我们就可以得出#7处的Fieldref是指向com/dafasoft/test/Test.test3:Ljava/lang/String;处的一个字符引用。

到现在为止,我们已经知道这个类里有一个引用指向com/dafasoft/test/Test.class 的变量test3 ,其类型为String类型,那么它是怎么和它的值"dafasoft"产生关联的呢?这和对象的初始化有关,具体的字节码在<init>方法中:


image.png

注意看Code的第17 和19行。查询字节码指令可知,ldc指令的含义是'将int、float、或String型常量值从常量池中推送至栈顶' putfied指令的含义是'为指定类的实例field赋值',查询常量池表可知,#6对应的值为字符串"dafasoft"。在执行过这两条指令后,我们才完成了对String变量test3的赋值。

3.2 示例二:this.testFun();

这条语句在构造方法Test() 中,要调用方法,需要知道这个方法在方法区的引用。方法的引用在常量池表里的形式为CONSTANT_Methodref_info, 查看本例的常量池表,它的索引为#8,具体的分析形式我们就不展开了,跟示例一的解析方式是一样的,解析完成后我们得知,它是一个指向com/dafasoft/test/Test.testFun:()V的一个符号引用,翻译成java语言就是:com.dafasoft.test包里Test类的void testFun()方法。
调用方式在Test 方法的字节码中:

image.png

注意看Code的第一行,这行指令的意思是,调用指向字符引用 #8的方法,而字符引用 #8 对应的方法就是com/dafasoft/test/Test.testFun:()V

由此我们也可以得出一个结论:当Java类中产生方法调用时才会在常量池中添加该方法的引用。那么本类中定义的方法去哪了?比如,Test类有三个方法,只有testFun出现了调用,testFun1和testFun2的引用就没有出现在常量池表中。那么这三个方法的实现在哪里呢?答案是 字节码的方法表中,当然这是另外的话题了。

常量池表中有17种类型,这17种类型的引用都可以用这种方式推导出来,我们就不一一介绍了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,454评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,553评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,921评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,648评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,770评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,950评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,090评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,817评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,275评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,592评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,724评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,409评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,052评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,815评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,043评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,503评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,627评论 2 350

推荐阅读更多精彩内容