分层理解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 added
    return 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.mfbz.cn/a/199754.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Ubuntu Linux玩童年小霸王插卡游戏

1.下载安装模拟器 在Windows平台模拟器非常多&#xff0c;而且效果也很优秀&#xff0c;Linux平台的用户常常很羡慕&#xff0c;却因为系统的缘故&#xff0c;无法使用这样的模拟器&#xff0c;但是随着时代的发展&#xff0c;Linux平台也出现了许多优秀的模拟器&#xff0c;现…

选择更灵活的设计工具:SOLIDWORKS 软件网络版与单机版的比较

随着科技的飞速发展&#xff0c;工程设计领域对于高效、灵活的设计工具需求日益增加。SOLIDWORKS 作为一款广受欢迎的三维设计软件&#xff0c;提供了网络版和单机版两种选择。在本文中&#xff0c;我们将深入探讨这两个版本的区别&#xff0c;并为您详细介绍它们的价格差异。 …

基于单片机的烟雾检测报警装置(论文+源码)

1.系统设计 &#xff08;1&#xff09;利用传感器实现环境中温度、烟雾浓度的实时检测&#xff1b; &#xff08;2&#xff09;系统检测的各项数据信息通过液晶模块进行显示&#xff0c;提高设计可视化&#xff1b; &#xff08;3&#xff09;系统可以根据实际情况利用按键模…

20天GMV超过百万美金!桌下迷你跑步机在TikTok Shop美国站热销

上周总GMV达到1.59亿美元&#xff0c;达到历史新高&#xff0c;是美国站自开通以来首次单周出单达到亿级&#xff1b;日均出单1660万美元&#xff0c;单日出单最高达2820万美元&#xff1b; 截至11月19日&#xff0c;GMV Top 5 的商品分类排名依次为&#xff1a;美妆个护、女士…

《深入理解计算机系统》学习笔记 - 第三课 - 位,字节和整型

Lecture 03 Bits,Bytes, and Integer count 位&#xff0c;字节&#xff0c;整型 文章目录 Lecture 03 Bits,Bytes, and Integer count 位&#xff0c;字节&#xff0c;整型运算&#xff1a;加&#xff0c;减&#xff0c;乘&#xff0c;除加法乘法取值范围乘法结果 使用无符号注…

交流负载的功能实现原理

交流负载的功能实现原理主要涉及到电力电子技术、电机控制技术和电力系统保护技术等多个方面。 交流负载的功能实现需要通过电力电子器件进行电能的转换和控制&#xff0c;电力电子器件主要包括开关器件和电力电子变压器等。开关器件主要用于实现电能的通断控制&#xff0c;如晶…

消息队列进阶-1.消息队列的应用场景与选型

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码、Kafka原理&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44…

vue el-table表格中每行上传文件(上传简历)操作

1、HTML中 <el-table :data"formInfo.userListDto" border stripe max-height"400"><el-table-column type"index" label"序号" width"50"> </el-table-column><el-table-column prop"realName&q…

【Udemy】AWS CLF - 01 题库 (英文版 + 中文版)目录

【挑战业余一周拿证】CSDN官方课程目录 【挑战业余一周拿证】AWS 认证云从业者 薅200美金羊毛 一、介绍 文章记录题库&#xff08;包含答案解释中文翻译&#xff09; 共计23章&#xff0c;每天更新 2-10 章习题&#xff0c;需要的客观请点赞收藏 来源Udemy&#xff0c;刷题…

Docker容器常用命令

文章目录 启动类命令帮助类命令镜像命令列出本地主机上的镜像在远程仓库中搜索镜像下载镜像保存镜像加载 tar 包为镜像查看占据的空间删除镜像 虚悬镜像命令自动补全新建启动容器启动交互式容器启动守护式容器 列出正在运行的容器容器其他启停操作启动已经停止的容器重启容器停…

【jupyter notebook中插件 nbextensions 安装失败分析与解决方法】

文章目录 问题描述分析与解决总结 问题描述 一开始在安装 notebook 中的插件 nbextensions 时根本没有注意到版本的适配问题&#xff0c;都是进行默认的安装&#xff0c;结果安装是最新版本的 notebook7.x&#xff0c;恰好 notebook7.x 版本不再适应插件 nbextensions&#xf…

智能优化算法应用:基于头脑风暴算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于头脑风暴算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于头脑风暴算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.头脑风暴算法4.实验参数设定5.算法结果6.参考…

大数据平台/大数据技术与原理-实验报告--部署ZooKeeper集群和实战ZooKeeper

实验名称 部署ZooKeeper集群和实战ZooKeeper 实验性质 &#xff08;必修、选修&#xff09; 必修 实验类型&#xff08;验证、设计、创新、综合&#xff09; 综合 实验课时 2 实验日期 2023.11.04-2023.11.05 实验仪器设备以及实验软硬件要求 专业实验室&#xff08…

leetcode:用栈实现队列(先进先出)

题目描述 题目链接&#xff1a;232. 用栈实现队列 - 力扣&#xff08;LeetCode&#xff09; 题目分析 我们先把之前写的数组栈的实现代码搬过来 用栈实现队列最主要的是实现队列先进先出的特点&#xff0c;而栈的特点是后进先出&#xff0c;那么我们可以用两个栈来实现&…

flask 上传文件

from flask import Flask, request, render_template,redirect, url_for from werkzeug.utils import secure_filename import os from flask import send_from_directory # send_from_directory可以从目录加载文件app Flask(__name__)#UPLOAD_FOLDER media # 注意&#xff…

大数据——一文详解数据仓库概念(数据仓库的分层概念和维度建模详解)

1、ods是什么&#xff1f; ods层最好理解&#xff0c;基本上就是数据从源表拉过来&#xff0c;进行etl&#xff0c;比如MySQL映射到Hive&#xff0c;那么到了Hive里面就是ods层。ods全称是 Operational Data Store&#xff0c;操作数据存储——“面向主题的”&#xff0c;数据…

实战Flask+BootstrapTable最实用服务端分页查询动态表头及数据(ajax方式)

看到这篇文章的朋友们是幸运的,我用了很久才实战出如下结果,且行且珍惜,祝好! 话不多说,有图有源码 1.看图,实现服务端动态表头数据,分页,查询,排序 1.数据准备 CREATE TABLE goods (id int(11) NOT NULL AUTO_INCREMENT,name varchar(255) DEFAULT NULL COMMENT 商品名,no …

运算放大器(五):V-I 转换器

1、高侧电压至电流&#xff08;V-I&#xff09;转换器 下图显示的电路是高侧电压至电流(V-I) 转换器。可将 0 V 至 2V 的输入电压转换为 0mA 至 100mA 的输出电流 其测量转换函数如下图所示&#xff1a; 可利用该电路搭建恒流源电路&#xff0c;如下图仿真电路所示&#xff08…

Linux 调试工具:gdb

调试复习 调试可谓是 “贯穿” 了程序员的一生&#xff0c;调试的重要性&#xff0c;就不再赘述啦&#xff01;如果你还不知道什么是调试&#xff0c;可以看看 Windows 系统的 Visual Studio 是如何调试的&#xff1a;➡️ visual stuudio 使用调试技巧 下载调试软件 gdb yu…

MaskDINO环境搭建与模型测试

1、环境搭建 1、构建虚拟环境安装torch conda create -n mmdetsam python3.8 -y conda activate mmdetsampip install torch1.10.0cu102 torchvision0.11.0cu102 torchaudio0.10.0 -f https://download.pytorch.org/whl/torch_stable.html -i http://mirrors.aliyun.com/pypi…
最新文章