分层理解Java字符串常量池

news2024/11/24 0:07:11

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.coloradmin.cn/o/1262783.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python如何抓取携程酒店的价格,让工作更简单点

有时候老板没事安排点事&#xff0c;为了偷懒&#xff0c;只能使出大招&#xff0c;毕竟自己不是那么老老实实干活的人&#xff0c;整理数据这类累和繁琐的活&#xff0c;我怎么能轻易动&#xff0c;好在gpt可以帮我来实现&#xff0c;有人可能会说&#xff0c;这么点内容你还不…

在线陪诊系统: 医疗科技的崭新前沿

在医学科技的快速发展中&#xff0c;在线陪诊系统正成为医疗服务领域的创新力量。通过结合互联网和先进的远程技术&#xff0c;这一系统为患者和医生提供了更为便捷、高效的医疗体验。本文将深入探讨在线陪诊系统的技术背后的核心代码和实现原理。 技术背后的关键代码 在线陪…

面试题:说一下MyBatis动态代理原理?

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 1.MyBatis简介2.使用步骤2.1、引入依赖2.2、配置文件2.3、接口定义2.4、加载执行 3.原理解析 1.MyBatis简介 MyBatis是一个ORM工具&#xff0c;封装了JDBC的操作&a…

k8s中pod的hostport端口突然无法访问故障处理

故障背景&#xff1a; 租户告知生产环境的sftp突然无法访问了&#xff0c;登录环境查看sftp服务运行都是正常的&#xff0c;访问sftp的hostport端口确实不通。 故障处理过程 既然访问不通那就先给服务做个全面检查&#xff0c;看看哪里出了问题&#xff0c;看下sftp日志&#…

Java学习路线第二篇:Java Web

这篇则分享Java学习路线第二part&#xff1a;Java Web 恭喜你已经成功追到第二章节啦&#xff0c;要被自己的努力感动到了吧&#xff0c;而这节将承担起学完Java web 的使命&#xff0c;本使命为单向契约&#xff0c;你可选择YES或者选择YES。 HTMLCSSJavaScript(JS) 【动…

Linux—进程状态、僵尸进程、孤独进程、优先级

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、进程状态二、僵尸进程、孤儿进程1、Z(zombie)-僵尸进程2、僵尸进程危害3、孤儿进程 三、进…

DHCP协议及实验omnipeek抓包工具分析 IPv4协议

一 抓包命令 adb shell tcpdump -i wlan0 -w /data/tcpdump.pcap 抓包后截图如下 二 DHCP是什么 2.1 DHCP定义 DHCP( Dynamic Host Configuration Protocol, 动态主机配置协议)定义: 存在于应用层(OSI) 前身是BOOTP(Bootstrap Protocol)协议 是一个使用UDP(User …

【沁恒蓝牙mesh】程序烧录 硬件电路设计与原理

本文基于沁恒CH58X 单片机的程序烧录硬件电路设计原理解释 【沁恒蓝牙mesh】目录 1. 程序烧录方式2. USB 烧录过程描述3. 硬件设计原理图3.1 官方开发板原理图3.2 自开发设计原理图 4. 电容在上电瞬间为什么相当于短路&#xff1f; &#x1f4cb; 个人简介 &#x1f496; 作者简…

关于AM5系列微机保护装置在某产业园配电工程中的应用-安科瑞 蒋静

1 摘要 目前&#xff0c;微机保护装置广泛应用于电力系统中&#xff0c;该类装置能够合理监测电力系统的运行状况&#xff0c;并实时记录电力系统出现故障的位置及性质&#xff0c;从而为故障的快速处理提供合理的参考信息。本文介绍的AM5系列微机保护装置&#xff0c;可以针对…

DC电源模块的散热措施

BOSHIDA DC电源模块的散热措施 DC电源模块的散热措施可以分为以下几种&#xff1a; 1. 增加散热器&#xff1a;在DC电源模块的电路板上增加散热片或散热器&#xff0c;通过增加散热面积和散热能力来提高散热效果。 2. 增加风扇&#xff1a;在散热器的基础上增加风扇&#xff…

竞赛选题 题目:基于机器视觉的图像矫正 (以车牌识别为例) - 图像畸变校正

文章目录 0 简介1 思路简介1.1 车牌定位1.2 畸变校正 2 代码实现2.1 车牌定位2.1.1 通过颜色特征选定可疑区域2.1.2 寻找车牌外围轮廓2.1.3 车牌区域定位 2.2 畸变校正2.2.1 畸变后车牌顶点定位2.2.2 校正 7 最后 0 简介 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享…

修改分区序列号的简单方法!

“我使用的是自己组装的电脑&#xff0c;安装了正版Win10操作系统。但奇怪的是&#xff0c;这台电脑看起来完全正常&#xff0c;但是每次启动时的分区序列号却总是不同。我现在要使用的软件需要依赖分区序列号进行注册&#xff0c;所以这个问题不解决我就没法使用软件。系统是正…

提升企业网络安全的得力助手——EventLog Analyzer网络日志管理

在当今数字化时代&#xff0c;企业的网络安全问题变得尤为重要。为了更好地应对日益增多的威胁和安全漏洞&#xff0c;企业需要一种高效的网络日志管理工具&#xff0c;EventLog Analyzer便是其中一款卓越的解决方案。 EventLog Analyzer EventLog Analyzer是一款综合性的网络…

户外园林气象环境RTU采集主机监测的具体使用

户外园林是人们休闲、娱乐和放松心情的场所&#xff0c;良好的气象环境对于提供舒适的户外体验至关重要。为了有效监测和管理园林的气象环境&#xff0c;户外园林气象环境RTU&#xff08;Remote Terminal Unit&#xff09;采集主机应运而生。本文将详细介绍户外园林气象环境RTU…

ELK高级搜索,深度详解ElasticStack技术栈-上篇

前言 1、黑马视频地址&#xff1a;java中级教程-ELK高级搜索&#xff0c;深度详解ElasticStack技术栈 2、本内容仅用于个人学习笔记&#xff0c;如有侵扰&#xff0c;联系删除 1. 课程简介 1.1 课程内容 ELK是包含但不限于Elasticsearch&#xff08;简称es&#xff09;、Lo…

【领域驱动设计 学习目标及大纲】从CRUD到架构设计

从2018年至今&#xff0c;已工作了5年有余&#xff0c;回望这5年的工作历程&#xff0c;虽然一直在学习、一直在积累&#xff0c;但其实都在术的层面上停留&#xff0c;也就是具体的技术点。这5年多的时间里其实也不是没有窥道的想法&#xff1a; 一次是2018年刚工作的时候&am…

软件提示找不到“vcruntime140.dll丢失的五个解决方法”(有效方法)

“vcruntime140.dll丢失的五个解决方法”。在我们的日常生活和工作中&#xff0c;有时候会遇到一些电脑问题&#xff0c;而vcruntime140.dll丢失就是其中之一。那么&#xff0c;什么是vcruntime140.dll文件呢&#xff1f;它为什么会丢失&#xff1f;又该如何解决这个问题呢&…

【产品设计】SaaS平台产品架构设计

产品架构是基于业务架构的&#xff0c;那么做产品架构前&#xff0c;需要对业务架构有哪些清晰的了解呢&#xff1f; 当我们去搜索“架构”&#xff0c;可以得到很多的架构图片&#xff0c;比如组织架构、业务架构、数据架构、技术架构、安全架构、产品架构、部署架构等。 什么…

计算4人队形的最可能分布

2 2 2 1 2 2 2 2 2 1 2 2 2 2 2 1 2 2 3 3 3 x 3 3 2 2 2 1 2 2 2 2 2 1 2 2 在6*6的平面上2个点随机分布&#xff0c;有3种分布方式&#xff0c;2a1&#xff0c;2a2&#xff0c;2a3&#xff0c;占比为1&#xff1a;5&#xff1a;1. 3 3 …

附录11-math.h的常见方法

stdlib.h是做数学计算的头文件 目录 1 数学知识 1.1 弧度值/π 角度值/180 1.2 双曲函数 2 math.h 2.1 反余弦值 acos() 2.2 反正弦值 asin() 2.3 反正切值 atan() 2.4 两个数的反正切值 atan2() 2.5 向上取整 ceil() 2.6 余弦值 cos() 2.7 双曲余弦 c…