分层理解Java字符串常量池

Java是一门计算机编程语言,但我们脑海中所理解的Java不仅仅是一门语言。它还包括Java虚拟机(JVM)的一系列规定,及具体Java产品(如Hotspot)的实现原理。

不管我们日常在Java中用到的任何一种语法,都会由语言规范对其进行语义和用法上的规定,再由虚拟机规范进行实现方案上的约束和建议,最后由具体的产品进行编码实现。其中,语言规范和虚拟机规范是 Oracle 制定好的(https://docs.oracle.com/javase/specs/index.html),不同的Java产品(如Hotspot、JRockit、J9)等,对虚拟机规范的实现方式不尽相同。

在这里插入图片描述

我们将在此思想基础上,探究Java中字符串常量池的概念,及字符串的对象创建、引用、“驻留”(intern)等一系列操作,及由此引申的Java加载、链接、初始化步骤。

Java语言层面

在Java语言标准中,没有提及字符串常量池,但是有对于“字符串字面常量”的定义:

3.10.5. 字符串字面量
字符串字面常量是用由双引号括起来的0个或多个字符构成的,字符串字面常量的类型总是String,是对String类的实例的引用。

一个字符串字面常量总是引用String类的同一个实例。这是因为其被通过使用String.intern() 方法而“驻留”了(直译应为“限定”,但是“驻留”更能体现字符串常量池的存在),这样做是为了让它们可以共享唯一的实例。

从这段表述中可以得出几点结论:

  • 字符串字面量的指向不会发生变化,被指向的实体是“先到先得”的;
  • 不同的 String 对象可以通过 String.intern() 方法得到同一个字符串字面量,即同一个 String 对象的引用;

尽管标准中没有提到字符串常量池,但姑且根据常识和上面的描述推测出一个简单模型:

image

按照这个模型,可以推出几个简单的判断

String a = "xyz";
String b = new String("xyz");
System.out.println(a==b); //false
System.out.println(a==a.intern()); //true
System.out.println(a==b.intern()); //true
System.out.println(b==b.intern()); //false

为什么a = "xyz"b = new String("xyz")对应的是两个对象呢,Java语言标准中有对创建类实例的标准描述 :

12.5. 创建新的类实例
新的类实例在类实例创建表达式的计算导致类被实例化时显式地创建。
新的类实例可以在下列情况下隐式地创建:

  • 加载包含String字面常量的类或接口时,会创建新的String对象,用来表示该字面常量。(如果同一个String对象之前已经被驻留了,那么这里就不会再创建新的String对象了);
  • 执行不是常量表达式的字符串连接操作符时,有时会创建新的String对象以表示执行结果。

从这段表述中可以得出几点结论:

  • String a = new String()这种显式创建的写法,必定会在堆中创建一个新的对象
  • String a = "xyz"这种写法一般情况下会在类加载过程中隐式在堆中创建一个新的对象并将字面量“xyz”驻留,但是若"xyz"字面量已经被驻留的话,则不创建新对象
  • String a = new String("xy") + new String("z"),这种通过"+"连接的写法,运行时会创建一个新的String对象表示结果,但是并不会将"xyz"驻留(这里要注意了)
  • 但如果是一个常量表达式 (§15.29) String a = "xy" + "z"则不同,它与String a = "xyz"一样,对应的"xyz"字面量已在类加载过程中被驻留(interned)了(可以理解为编译期间已经被优化为了"xyz"),且运行时不会创建新对象

这里再重点强调一遍,对于String a = "xyz",在类加载过程中,就已经生成了一个代表"xyz"的对象,在运行这行代码时仅仅是从字符串常量池中获取了该对象的引用并返回,但是对于String a = new String("xyz"),虽然在类加载过程中就已经生成了一个代表"xyz"的对象,但是由于是显式的使用了new关键字,所以仍会创建一个新的对象

那么根据这几点表述,继续完善模型:

image

按照这个模型,可以推出几个简单的判断:

String a = "xyz";
String b = new String("xyz");
String c = "xy"+"z";
String d = "xyz";
String e = new String("xy")+"z";
System.out.println(a==b); //false
System.out.println(a==c); //true
System.out.println(a==d); //true
System.out.println(a==e); //false

整理已经介绍过的标准,可以给出一个推论,当且仅当出现以下三种情况下,字面量才有可能会驻留(interned):

  • 代码中出现被引号包含的字面量,如:String a = “xyz”,字面量"xyz"在类加载过程被驻留
  • 代码中出现String类型的常量表达式,如:String a = “xy” + “z”,字面量"xyz"在类加载过程被驻留
  • 调用了intern()方法,如 a.intern(),若a对应的字面量没有被驻留过,则驻留该字面量,否则返回之前驻留的字面量(即对象引用)

可以结合以下例子理解:

// 字面量abc既未被引号包围,也不是一个常量表达式,仅创建对象
String f = new String("ab") + new String("c"); 
// 将字面量abc驻留(即f对象引用)
f.intern();
// 字面量abc被引号包围,类加载过程中,发现字面量abc已被驻留,则直接返回f对象引用
String g = "abc";
// true
System.out.println(f == g); 

这里要注意一点:f.intern()f = f.intern() 是不一样的,f.intern()并不更改f本身的值。

到这里,可以看出,java语言层面中尽管没有提到字符串常量池这个字眼,但是对驻留(interned)这个概念的解释已经非常通透了,根据以上根据标准的推论,所有字符串的 == 问题都能够得到答案,接下来介绍语言层面之下的细节。

JVM层面

在JAVA语言标准中,定义字面量是被引号包围的东西,实际上是一个对象引用,那么JVM标准层面是如何描述字面量这个概念的呢:

5.1. 运行时常量池

字符串常量是指向String类实例的引用,它来自于类或接口二进制表示中的 CONSTANT_String_info 结构。其给出了由Unicode码点序列所组成的字符串常量。
Java语言规定,相同的字符串常量必须指向同一个String类实例。此外,如果在任一字符串上调用String.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。

为了得到字符串常量,Java虚拟机需要检查 CONSTANT_String_info 结构中的码点序列:

  • 如果某String实例所包含的Unicode码点序列与 CONSTANT_String_info 结构所给出的序列相同,而之前又曾在该实例上面调用过 String.intern 方法,那么此次字符串常量获取的结果将是一个指向相同String实例的引用;
  • 否则,会创建一个新的String实例,其中包含由 CONSTANT_String_info 结构所给出的Unicode码点序列;字符串常量获取的结果是指向那个新String实例的引用,最后,新String实例的intern方法被Java虚拟机自动调用。
4.4.3. CONSTANT_String_info 结构

CONSTANT_String_info structure 用于表示String类型的常量对象,其结构如下:

CONSTANT_String_info {
​ u1 tag;
​ u2 string_index;
}

CONSTANT_String_info 结构各项说明如下:

  • tag:值为 CONSTANT_String (8);
  • string_index:必须是对常量池标的有效索引,常量池表在该索引出的成员必须是 CONSTANT_Utf8_info 结构,此结构表示Unicode码点序列,此序列最终会被初始化为一个String对象。
4.4.7. CONSTANT_Utf8_info 结构

CONSTANT_Utf8_info 结构用来表示字符串常量的值:

CONSTANT_Utf8_info {
​ u1 tag;
​ u2 length;
​ u1 bytes[length];
}

这段描述中,string constant就是java语言标准中的字面量(string literals),对其特性又描述了一遍,基本与java标准中描述的一致,但是提供了更多的细节:

  • 字面量是通过运行时常量池中的CONSTANT_String_info结构来获取的
  • CONSTANT_String_info结构只持有了一个CONSTANT_Utf8_info结构在运行时常量池中的index
  • CONSTANT_Utf8_info持有了一个Unicode字节序列,JVM可以通过这个序列来做唯一性检测

通过上面的几点描述,字符串常量池的轮廓已经差不多了,规范中说要通过CONSTANT_String_info来获取字面量,那怎么获取呢,很容易推断出JVM中应该存在一个类似HashMap的结构,key可能是根据CONSTANT_String_info获取到的CONSTANT_Utf8_info中的Unicode字节序列(bytes[length])(这里只是一个猜测,实际如何得看HotSpot层面的实现了),value就是字面量(对象引用)

至于CONSTANT_Utf8_info究竟是个什么东西,就得看具体JVM是如何实现的了,对于Hotspot而言,所谓的Unicode字节序列可能就是个C++的bytes数组。

Java产品实现层面

Java的实现,我们日常接触最多的就是Hotspot。我们以Hotspot为例了解。

回顾一下JAVA语言层面与JVM标准层面的描述:JVM可以通过类运行时常量池中的CONSTANT_String_info来找到对应的字面量(即对象引用),于是我们假设存在一个类似HashMap结构的字符串常量池来辅助完成这件事情,那么真的有这么个结构吗,可以从HotSpot代码中找到答案:

HotSpot VM里实现字符串常量池功能的是StringTable类,在hotspot/src/share/vm/classfile/symbolTable.[hpp|cpp]中,看它的定义:

class StringTable : public Hashtable<oop, mtSymbol>

在C++层面的确是个Hashtable类型,key是oop类型(oop类型就是Java层面的对象引用),value是mySymbol类型,乍眼一看不知道是什么东西,那么可以从向这个Stringtable里插入的代码入手:

oop StringTable::basic_add(int index_arg, Handle string, jchar* name,int len, unsigned int hashValue_arg, TRAPS) {// ...// Check if the symbol table has been rehashed, if so, need to recalculate// the hash value and index before second lookup.unsigned int hashValue;int index;if (use_alternate_hashcode()) {hashValue = hash_string(name, len);index = hash_to_index(hashValue);} else {hashValue = hashValue_arg;index = index_arg;}// Since look-up was done lock-free, we need to check if another// thread beat us in the race to insert the symbol.oop test = lookup(index, name, len, hashValue); // calls lookup(u1*, int)if (test != NULL) {// Entry already addedreturn test;}HashtableEntry<oop, mtSymbol>* entry = new_entry(hashValue, string());add_entry(index, entry);return string();
}

首先根据jchar* name计算出hash值,转化赋值给index,然后调用了lookup方法去判断StringTable中是否已经存在对应的字面量,结合JVM规范来看,jchar* name这个入参应该就是运行时常量池中CONSTANT_Utf8_info类型变量持有的Unicode字节序列(bytes[length])

再看lookup函数:

oop StringTable::lookup(int index, jchar* name,int len, unsigned int hash) {int count = 0;for (HashtableEntry<oop, mtSymbol>* l = bucket(index); l != NULL; l = l->next()) {count++;if (l->hash() == hash) {if (java_lang_String::equals(l->literal(), name, len)) {return l->literal();}}}// If the bucket size is too deep check if this hash code is insufficient.if (count >= BasicHashtable<mtSymbol>::rehash_count && !needs_rehashing()) {_needs_rehashing = check_rehash_table(count);}return NULL;
}

逻辑非常简单,根据index找到了hashtable对应的拉链节点的位置,然后逐个对节点的key进行判断,对比jchar* name是否与对象表示的字符串相同(java_lang_String::equals),若一致,则直接把key(对象引用)返回。

所以,StringTable的结构就可以当成是个简单的hashtable(拉链式),hashcode是根据对应的Unicode字节序列计算而来,节点中存放的就是对象引用(字面量)。

综上,我们从不同层次介绍了字符串常量池的概念和实现。实际上我们主要需要聚焦Java语言层面和JVM层面,实现层面可以给我们提供更多的实践思路。关于引出的Java加载、链接、初始化的过程,以后再继续探究。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/225867.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度学习之图像分类(十四)CAT: Cross Attention in Vision Transformer详解

IPSA和CPSA的处理流程、维度变换细节 FLOPs的计算方法、以及flops和划分的patch数目以及patch的维度计算关系 IPSA如何进行local attention、CPSA如何进行globe attention CAT的代码详细注释---需要学习完Transformer TNT、swin transformer、crossViT CAT: Cross Atten…

P9240 [蓝桥杯 2023 省 B] 冶炼金属(比值问题)

数学分析&#xff1a; 1. max(最大比值) A/B 余数p&#xff08;p<B&#xff09; > Amax*Bp 反证&#xff1a;若max不为最大,则设maxn为最大比值 (maxn)*Bmax*Bn*Bp1 > A (n*Bp1 > p ,矛盾) 故max为最大比值 2.min(最小比值…

2021年09月 Scratch图形化(四级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共10题,每题3分,共30分) 第1题 下面哪个选项程序可以交换下图列表中第2项和第3项的位置? A: B: C: D:

【STM32单片机】基于语音识别的智能分类垃圾桶,ld3320语音识别模块如何使用,mp3播放模块如何使用

文章目录 需求语音识别模块MY1690 播放模块舵机源码 需求 对于“可回收物”“有害垃圾”“厨余垃圾”“其它垃圾”&#xff0c;不能分清扔到哪个垃圾桶怎么办&#xff1f; 基于语音识别的智能分类垃圾桶&#xff0c;识别到关键词就打开对应的垃圾桶&#xff0c;完全没有分不清…

一图解释 为什么选择实时云渲染而不是像素流?

首先我们要明白两个概念&#xff0c;实时云渲染和像素流到底是什么&#xff1f; 像素流送&#xff0c;指在用户看不到的计算机上远程运行UE开发的应用。虚幻引擎使用该计算机可用的资源&#xff08;CPU、GPU、内存等&#xff09;来运行游戏逻辑并渲染每一帧。它不断地将此渲染…

2023年全国硕士研究生入学统一考试管理类专业学位联考数学试题——解析版

文章目录 一、问题求解&#xff1a;真题&#xff08;2023-01&#xff09;真题&#xff08;2023-02&#xff09;真题&#xff08;2023-03&#xff09;真题&#xff08;2023-04&#xff09;真题&#xff08;2023-05&#xff09;真题&#xff08;2023-06&#xff09;真题&#xff…

安防视频监控/视频融合/云存储EasyCVR页面数据显示不全该如何解决?

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安…

语文老师怎么和家长沟通

作为一位语文老师&#xff0c;深知教育不单单是传授知识&#xff0c;更是引导学生发展潜能&#xff0c;培养品格。而在这个过程中&#xff0c;与家长建立良好的沟通关系是至关重要的。 建立信任关系 与家长沟通的第一步是建立信任关系。作为老师&#xff0c;需要展现出专业、热…

Vim多行编辑

Vim多行编辑 Ctrlq进入多行编辑模式&#xff0c;然后上下选择要编辑的行 按下I或者Shifti&#xff0c;进入编辑模式 编辑的时候多行不会同时变化&#xff0c;不要担心&#xff0c;确实是多行编辑 编辑完成&#xff0c;想要结束多行编辑&#xff0c;按下Esc&#xff0c;此时…

一文例说嵌入式 C 程序的内聚和耦合

1 - 原理篇 低耦合&#xff0c;是指模块之间尽可能的使其独立存在&#xff0c;模块之间不产生联系不可能&#xff0c;但模块与模块之间的接口应该尽量少而简单。这样&#xff0c;高内聚从整个程序中每一个模块的内部特征角度&#xff0c;低耦合从程序中各个模块之间的关联关系…

社区新零售:重塑零售业的全新模式

社区新零售&#xff1a;重塑零售业的全新模式 近年来&#xff0c;新零售业成为了研究的焦点&#xff0c;它是一种以互联网为基础的零售形式。新零售通过运用先进技术手段&#xff0c;如大数据和人工智能&#xff0c;对商品的生产、流通和销售过程进行升级改造&#xff0c;重新构…

mysql处理40w数据脚本执行慢问题

需求背景&#xff1a; 2张表 SS_ZYXX 1w数据&#xff0c;WD_GZPZ 50w数据 SS_ZYXX.id WD_GZPZ.zyxx_id 找到SS_ZYXX表有数据&#xff0c;关联表WD_GZPZ没有数据的SS_ZYXX表的id 处理方案 方案一&#xff1a; 联合查询&#xff1a; 下面sql&#xff0c;在mysql执行时间3…