Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2.2.6运行时常量池与字符串常量池应该不是同一个池 #112

Open
dingjs opened this issue May 4, 2021 · 12 comments
Open

2.2.6运行时常量池与字符串常量池应该不是同一个池 #112

dingjs opened this issue May 4, 2021 · 12 comments

Comments

@dingjs
Copy link

dingjs commented May 4, 2021

2.2.6章节中描述

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量 一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常 量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的 intern()方法。

而在jvms8中对运行时常量池描述为

A run-time constant pool is a per-class or per-interface run-time representation
of the constant_pool table in a class file (§4.4).

Stringintern方法对应的是字符串常量池,一个定长的hashtable,在HotSpot jvm中,以前是在方法区中,1.7移到堆中,
运行时常量池如果用javap查看class文件,可以看到Constant pool,这个应该才是jvms中提到的运行时常量池,您在书中也有提到。

public class ch.qos.logback.access.joran.action.ConfigurationAction extends ch.qos.logback.core.joran.action.Action
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
    #1 = Class              #2            // ch/qos/logback/access/joran/action/ConfigurationAction
    #2 = Utf8               ch/qos/logback/access/joran/action/ConfigurationAction
    #3 = Class              #4            // ch/qos/logback/core/joran/action/Action
    #4 = Utf8               ch/qos/logback/core/joran/action/Action
    #5 = Utf8               INTERNAL_DEBUG_ATTR
    #6 = Utf8               Ljava/lang/String;
    #7 = Utf8               ConstantValue
    #8 = String             #9            // debug

综合来看,字符串常量池与jvm的运行时常量池并无关联。
个人见解,期待沟通!

@fenixsoft
Copy link
Owner

string iteral pool是jvm runtime constant pool的一个逻辑部分,在《JVMS》对runtime constant pool的定义中明确声明了这一点:

sshot-5

这段(以及后面未贴出的一页)正好解释了String.intern()方法与runtime constant pool的交互。

Issues中提到的:

可以看到Constant pool,这个应该才是jvms中提到的运行时常量池,您在书中也有提到

似乎混淆了constant pool和runtime constant pool的概念,前者是定义在Class文件格式之中,存储的Class文件中直接使用到的,编译期已经静态可知的符号,后者是定义在虚拟机类加载(Loading、Linking、Initalizing)行为之中,存储的不仅包括constant pool中的静态符号,还包括了程序运行期动态产生的符号,所以带有“runtime”的定语。它们两者是具有不同作用的独立概念。

@dingjs
Copy link
Author

dingjs commented May 4, 2021

周老师,你好,感谢回复。
Run-time Contant Pool的官方定义中明确表述为:

A run-time constant pool is a per-class or per-interface run-time representation
of the constant_pool table in a class file (§4.4).

翻译为中文为

运行时常量池是class file中每个类或接口的常量池表(constant_pool table)的运行时表示方式。

至于您截图的内容,只是为了表示string literal表述的内容,由于对string literal表述过多,容易喧宾夺主,产生字符串常量池是运行时常量池一部分的误导。

根据2018年的jvms11表述
image

翻译为中文为

一个字符串常量是String类的实例对象引用,派生于 CONSTANT_String_info结构。派生一个字符串常量,jvm会检查 CONSTANT_String_info中给出的code points序列:
- 如果之前调用过有相同 code points序列的string对象的inern方法,那么这个字符串常量引用的就是之前intern后的实例对象
- 不然,就会创建一个新的字符串对象,并且在这个新对象上执行intern方法。

翻译的比较拙劣,请见谅。但基于jvms的表述,我还是对以下说法有不同看法。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量 一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的 intern()方法。

而且现在字符串常量池已经不在方法区了。

@fenixsoft
Copy link
Owner

有不同意见是好事,欢迎讨论。

而且现在字符串常量池已经不在方法区了。

这个结论并不正确,我明白你想说的是HotSpot在JDK8之后的变动,是指:“JDK 8后,HotSpot虚拟机已经不再使用以前PermGen来实现方法区了,类型信息挪到Metaspace,类变量、字面量等信息挪到了Heap中了”。

但后面这个事实并不能推导出前面的结论,运行时常量池、方法区、字符串常量池这些都是不随虚拟机实现而改变的逻辑概念,是公共且抽象的,Metaspace、Heap是与具体某种虚拟机实现相关的物理概念,是私有且具体的。

虚拟机规范提倡的理念是“公有设计,私有实现”,我们可以说“目前版本的HotSpot没有将字符串常量池放在元空间中",但是不能说“现在的JVM不再将字符串常量池放在方法区中”。无论是哪一种Java虚拟机,无论它是使用PermGen(8之前的HS),还是JavaHeap(8之后的HS),抑或是CHeap(J9)来存放字面量,都不影响字符串常量仍然是运行时常量的一个子集,因为它们都是逻辑上的概念,所有Java虚拟机一致的外观。

其实“字符串常量池”这个说法,也是因为HotSpot本身实现了这个概念,大家已经形成习惯。其实在JVMS中只陈述了字面量应该不重复地存储在运行时常量池之中,并没有定义过字符串常量“池”的概念。但有一点是明确的,无论我回帖贴截图中《JVMS 15》的表述,还是你翻译的《JVMS 11》的表述,都规定了:

  • 字面量(String Literal)以CONSTANT_String_info结构存储在运行时常量池中。
  • 如果一个全新的String首次执行intern()方法,会产生一个新的字面量。

最后,关于这句话:“并非预置入Class文件中常量池的内容才能进入方法区运行时常量池”,具体是与翻译中的哪一部分相抵触?

@dingjs
Copy link
Author

dingjs commented May 4, 2021

周老师,字符串常量池已经不在方法区了,我这个表述确实不恰当。

jvms和jls都只规定了

  1. 同样的字符串字面量指向的是同一个对象
  2. 调用string.intern与对应的字符串字面量指向的也是同一个对象

通篇并没有说字符串常量池,字符串常量池只是jvm的具体实现满足jvm对于字符串相关规定的解决方案。您在回贴中也有提到。您回贴中的表述我都认可,除了字符串常量池是运行时常量池的子集这个表述。

我反复看了几遍jvms11-5.1章节关于运行时常量池的描述,原文重点内容引用如下

  1. There are two kinds of entry in the run-time constant pool: symbolic references, which may later be resolved (§5.4.3), and static constants, which require no further processing.
  2. The static constants in the run-time constant pool are also derived from entries in the constant_pool table in accordance with the structure of each entry
    • A string constant is a reference to an instance of class String, and is derived from a CONSTANT_String_info structure (§4.4.3). To derive a string constant, the Java Virtual Machine examines the sequence of code points given by the CONSTANT_String_info structure:
      • If the method String.intern has previously been invoked on an instance of class String containing a sequence of Unicode code points identical to that given by the CONSTANT_String_info structure, then the string constant is a reference to that same instance of class String.
      • Otherwise, a new instance of class String is created containing the sequence of Unicode code points given by the CONSTANT_String_info structure. The string constant is a reference to the new instance. Finally, the method String.intern is invoked on the new instance.

此描述与您最后翻译的如果一个全新的String首次执行intern()方法,会产生一个新的字面量并不相符,文档并无此描述。

刚刚我引用的原文,中文表述如下

  1. 运行时常量池中有两种类型的entry,一个是symbolic references,另一个是static constants。symbolic references主要是CONSTANT_Class_info,CONSTANT_Fieldref_info等。
  2. 运行时常量池中的静态常量也派生于constant_pool表中的entry
    • 主要是字符串常量(字面量)和数字常量(CONSTANT_Integer_info 等),数字常量可看原文jvms-4.html#jvms-4.4.4。而jvm应该如何解析字符串常量呢
      • 如果之前有调用过相同string对象的intern方法,那么这个字符串常量(string constants)应该解析为那个相同的string对象的引用
      • 否则 ,jvm应该创建一个新的string实例对象,这个string常量就是这个新对象的引用。最后,会在这个新对象上执行intern方法。

感谢周老师不吝赐教,此问题已困扰我多年。

@fenixsoft
Copy link
Owner

此问题已困扰我多年

没那么夸张吧。“此问题”我看描述不知道具体是指哪个?先把“字符串常量池是否属于运行时常量池”和“String.intern方法的行为”分开来讨论。你明确后描述疑问后可以再进一步展开。

对于前者,这个结论是否能认可?

字面量(String Literal)以CONSTANT_String_info结构存储在运行时常量池中。

如果能认可,那字符串常量池就是虚拟机中存储字面量的集合,既然字面量是存储在运行时常量池的结构之一,那字符串常量池自然是它的子集。
如果不能认可,你的疑问点是什么?

对于后者,前面中文的说法有歧义,所指的就是你贴出的这行原文:

a new instance of class String is created containing the sequence of Unicode code points given by the CONSTANT_String_info structure

这个新创建的String实例就是字面量,关于String Literal的定义在JLS中有明确定义,它就是一个唯一的String实例。

@dingjs
Copy link
Author

dingjs commented May 4, 2021

字面量(String Literal)以CONSTANT_String_info结构存储在运行时常量池中。

周老师,这句话我认可。

但 字符串常量池就是虚拟机中存储字面量的集合 这句话不认可。

String str = "abc";
"abc"是字面量,这个字面量是啥,我在上一回复中有说明。

String str = String.valueOf(6666666).intern()
这里没有字面量,会生成一个string对象,放在字符串常量池中。

我感觉咱俩的主要分歧在于字符串常量池的定义上了。
我的理解如下:
在jvms中,没有定义字符串常量池,只是具体jvm实现为了满足规范要求的一种实现。字符串常量池是jvm具体实现(如HotSpot)中用于存放字符串实例对象的地方,是一个定长的hashtable,可用StringTableSize来配置其大小。

String str = "abc";
这个“abc”的字面量以以CONSTANT_String_info结构存储在运行时常量池中,jvm把这个字面量解析为一个string对象的引用。恕我再贴一下原文

  1. There are two kinds of entry in the run-time constant pool: symbolic references, which may later be resolved (§5.4.3), and static constants, which require no further processing.
  2. The static constants in the run-time constant pool are also derived from entries in the constant_pool table in accordance with the structure of each entry
    • A string constant is a reference to an instance of class String, and is derived from a CONSTANT_String_info structure (§4.4.3). To derive a string constant, the Java Virtual Machine examines the sequence of code points given by the CONSTANT_String_info structure:
      • If the method String.intern has previously been invoked on an instance of class String containing a sequence of Unicode code points identical to that given by the CONSTANT_String_info structure, then the string constant is a reference to that same instance of class String.
      • Otherwise, a new instance of class String is created containing the sequence of Unicode code points given by the CONSTANT_String_info structure. The string constant is a reference to the new instance. Finally, the method String.intern is invoked on the new instance.

@dingjs
Copy link
Author

dingjs commented May 4, 2021

JLS对于String Literal明确定义如下:
A string literal is a reference to an instance of class String (§4.3.1, §4.3.3).
Moreover, a string literal always refers to the same instance of class String.

一个字面量是一个String对象实例的引用,而且相同的字面量指向的是同一个实例。
- 如果之前有调用过相同string对象的intern方法,那么这个字符串常量(string constants)应该解析为那个相同的string对象的引用
- 否则 ,jvm应该创建一个新的string实例对象,这个string常量就是这个新对象的引用。最后,会在这个新对象上执行intern方法。

至于具体jvm怎么实现,jvms不管,满足上面两个要求即可。

根据我对jvms原文描述的理解,一言以蔽之,字符串常量池与运行时常量池无关,String.intern与运行时常量池无关。

@fenixsoft
Copy link
Owner

字面量(String Literal)以CONSTANT_String_info结构存储在运行时常量池中。

周老师,这句话我认可。

但 字符串常量池就是虚拟机中存储字面量的集合 这句话不认可。

String str = "abc";
"abc"是字面量,这个字面量是啥,我在上一回复中有说明。

String str = String.valueOf(6666666).intern()
这里没有字面量,会生成一个string对象,放在字符串常量池中。

我感觉咱俩的主要分歧在于字符串常量池的定义上了。
我的理解如下:
在jvms中,没有定义字符串常量池,只是具体jvm实现为了满足规范要求的一种实现。字符串常量池是jvm具体实现(如HotSpot)中用于存放字符串实例对象的地方,是一个定长的hashtable,可用StringTableSize来配置其大小。

String str = "abc";
这个“abc”的字面量以以CONSTANT_String_info结构存储在运行时常量池中,jvm把这个字面量解析为一个string对象的引用。恕我再贴一下原文

  1. There are two kinds of entry in the run-time constant pool: symbolic references, which may later be resolved (§5.4.3), and static constants, which require no further processing.

  2. The static constants in the run-time constant pool are also derived from entries in the constant_pool table in accordance with the structure of each entry

    • A string constant is a reference to an instance of class String, and is derived from a CONSTANT_String_info structure (§4.4.3). To derive a string constant, the Java Virtual Machine examines the sequence of code points given by the CONSTANT_String_info structure:

      • If the method String.intern has previously been invoked on an instance of class String containing a sequence of Unicode code points identical to that given by the CONSTANT_String_info structure, then the string constant is a reference to that same instance of class String.
      • Otherwise, a new instance of class String is created containing the sequence of Unicode code points given by the CONSTANT_String_info structure. The string constant is a reference to the new instance. Finally, the method String.intern is invoked on the new instance.

上面这段描述(以及后面一贴在“一言以蔽之”之前的部分),并没有什么问题。然而我疑惑的是,你不认可的“字符串常量池就是虚拟机中存储字面量的集合”这个与描述中的内容的矛盾点具体是什么?

这句话与你给出的定义“ 字符串常量池是jvm具体实现(如HotSpot)中用于存放字符串实例对象的地方”,从文字结构来看,差别是宾语“存储字面量的集合”与“存放字符串实例对象的地方”。
然而后面你用引用JLS也明确了"A string literal is a reference to an instance of class String",这说明你是清楚string literal定义的,字面量就是一个字符串实例的引用,且这个实例是唯一的,那这里两个定义的分歧是在哪里?

退一步说,字符串常量池不是JVMS中的概念,针对虚拟机的抽象讨论中,可以不需要它,那回到最初的问题:

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量 一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的 intern()方法。

结合你的例子:

String str = String.valueOf(6666666).intern()
这里没有字面量,会生成一个string对象,放在字符串常量池中。

这里准确地说是没有静态常量(static constants)的字面量,在Class文件的常量池中只会存储数值型的6666666,不会存储字符串类型的“6666666”。但是运行期间是肯定会生成一个字符串类型的“6666666”。

所以先明确一下,你的观点是当运行到String.valueOf(6666666).intern()这样的语句时,虚拟机:

  1. 字面量 != 字符串实例
  2. 不会产生“6666666”的字符串实例
  3. 会产生一个“6666666”的字符串对象实例,但没有字面量
  4. 会产生一个“6666666”的字符串对象实例,但还有一个与它互相独立的字面量
  5. 会产生一个“6666666”的字符串对象实例,它就是字面量,但并不存储在运行时常量池之中

以上的哪一种?或者是在以上没有列举到的某一种?

@dingjs
Copy link
Author

dingjs commented May 5, 2021

周老师,constant pool与runtime constant pool之间的关系如下:

A class file keeps all its symbolic references in one place, the constant pool. Each class file has a constant pool, and each class or interface loaded by the Java virtual machine has an internal version of its constant pool called the runtime constant pool .

这和jvms官方定义一致

A run-time constant pool is a per-class or per-interface run-time representation of the constant_pool table in a class file

runtime constant pool对应的Hotspot实现类应该是constantPool.cpp.

字符串常量池对应的Hotspot实现类应该是symbolTable.cpp.

字面量!= 字符串实例。字符串实例是java对象,jvm在解析constant pool为runtime constant pool时,会将字面量解析为对应字符串实例的引用

当运行String.valueOf(6666666).intern()时,会产生一个“6666666”的字符串对象实例,但没有字面量,在字符串常量池中,但不在运行时常量池中。

        String str = String.valueOf(6666666);
        str.intern();
        String str2 = "6666666";
        System.out.print(str == str2);

周老师,上面这段代码应该输出什么?

@fenixsoft
Copy link
Owner

周老师,constant pool与runtime constant pool之间的关系如下:

A class file keeps all its symbolic references in one place, the constant pool. Each class file has a constant pool, and each class or interface loaded by the Java virtual machine has an internal version of its constant pool called the runtime constant pool .

这和jvms官方定义一致

A run-time constant pool is a per-class or per-interface run-time representation of the constant_pool table in a class file

这部分问题依然是与之前一样,我们找资料摆论据,目的是为了证明自己的论点,以上贴出的资料时,你应该说明清楚的论点是什么?是以下:

  1. 认为我前面对constant pool与runtime constant pool的解释(似乎混淆了constant pool和runtime constant pool的概念,前者是定义在Class文件格式之中,存储的Class文件中直接使用到的,编译期已经静态可知的符号,后者是定义在虚拟机类加载(Loading、Linking、Initalizing)行为之中,存储的不仅包括constant pool中的静态符号,还包括了程序运行期动态产生的符号,所以带有“runtime”的定语。它们两者是具有不同作用的独立概念。)有与定义矛盾之处?
  2. 认为constant pool与runtime constant pool中存储的内容数量是相等的,应该一一对应,runtime constant pool不会在运行期产生新的内容?

或者是以上没有列举到的哪一种?

runtime constant pool对应的Hotspot实现类应该是constantPool.cpp.

字符串常量池对应的Hotspot实现类应该是symbolTable.cpp.

HotSpot中常量池的实现是src/hotspot/share/classfile/stringTable.cpp,symbolTable.cpp顾名思义是符号表,这里符号不是字符串的意思,是编译原理中的基础概念,程序中符号不仅仅包括字符串,其他的譬如关键字、变量名称、数值、运算符等等都是符号,注解甚至是注释如果有必要的话,都可以作为符号保留下来。
用HotSpot的代码来解释虚拟机规范的定义,在逻辑上其实是有瑕疵的,因为发现鸵鸟不会飞,不能就说鸟并不会飞。不过考虑到HotSpot的绝对垄断地位,且这里既然贴代码了,稍后我们可以从代码入手来解决字符串实例与字面量的关系。

字面量!= 字符串实例。字符串实例是java对象,jvm在解析constant pool为runtime constant pool时,会将字面量解析为对应字符串实例的引用

当运行String.valueOf(6666666).intern()时,会产生一个“6666666”的字符串对象实例,但没有字面量,在字符串常量池中,但不在运行时常量池中。

先从概念上看。

String str = String.valueOf(6666666).intern()这行代码如果按你描述的“会产生一个“6666666”的字符串对象实例,但没有字面量”,那str是这个字符串对象实例的引用是否能认可?如果str是这个字符串对象实例的引用,你如何看待此前你从JLS中摘除的定义A string literal is a reference to an instance of class String。还是我之前问的问题,你认为它们不是同一个东西,那你觉得的差别在哪里?

如果从概念入手,实在绕不出来的话,那退一步,容忍一些逻辑上的瑕疵,从具体代码入手来看看。

String.intern()方法的JNI入口在src/java.base/share/native/libjava/String.c中,如下:

sshot-7

经过若干次调用,最终的代码逻辑会调用前面提及的stringTable.cpp,方法如下:

s8

对于直接存在于Class文件常量池中的静态常量,是通过ldc这个bytecode指令进入的,最终也会调用到同一个stringTable::intern()方法,堆栈如下:
sshot-8

可见,无论是直接在Java代码中存在的"6666666"静态常量,还是通过String.valueOf(6666666).intern()动态加进去的运行时常量,在HotSpot中最后都是在同一个地方存放的,在虚拟机视角一侧看来,没有什么区别。

基于这个基础,再来回看你的观点:

会产生一个“6666666”的字符串对象实例,但没有字面量,在字符串常量池中,但不在运行时常量池中。

现在通过代码调试,可以证实stringTable.cpp中存储了确是是这个字符串的unicode表示,假设它真的独立于运行时常量池的话,你认为当Java代码中出现"6666666"静态常量时,譬如String s = “6666666”,那当它被ldc指令加载后,字符串常量池中存储了unicode表示,那运行时常量池中会存储的应该是什么内容?如果依然还是这个字符串的unicode表示,它分别在两个地方存储两份,是否合适必要?

@dingjs
Copy link
Author

dingjs commented May 5, 2021

周老师,与之交流,收益颇多。

  1. symbolTable.cpp是因我看的非官方源码,而是jetBrains的,这个版本中StringTable在symbolTable.cpp中。但确实不不该引用非官方源码。
  2. 至于拿HotSpot的代码来解释虚拟机规范的定义,确实逻辑上存在瑕疵,但也是不得已而为之,因为字符串常量池并非jvms中的定义,String.intern操作的是stringTable,而非运行时常量池contantPool
  3. String s = “6666666”String s= String.valueOf(666666).intern()都会往字符串常量池里存放(注:此表述又隐含了具体jvm实现)。前文多次引用的JVMS中对于static constant的解析时也提到,JVM在解析constant pool表生成runtime constant pool时,如果相同字符串之前已调用intern,则返回该对象实例的引用,否则创建一个新对象,最后在新对象上调用intern.
  4. jvm在parse classfile,生成运行时常量池时,按照规范是会调用stirng.intern.您最后的问题,我的回答是运行时常量池中存的是字符串实例的引用
    image
  5. jvm在解析classfile生成运行时常量池时用到了String.intern,无法推导出string.intern操作的就是运行时常量池。这两逻辑不对等。字符串常量池并不是运行时常量池的一部分。

@zark721
Copy link

zark721 commented Sep 14, 2024

string iteral pool是jvm runtime constant pool的一个逻辑部分,在《JVMS》对runtime constant pool的定义中明确声明了这一点:

sshot-5

这段(以及后面未贴出的一页)正好解释了String.intern()方法与runtime constant pool的交互。

Issues中提到的:

可以看到Constant pool,这个应该才是jvms中提到的运行时常量池,您在书中也有提到

似乎混淆了constant pool和runtime constant pool的概念,前者是定义在Class文件格式之中,存储的Class文件中直接使用到的,编译期已经静态可知的符号,后者是定义在虚拟机类加载(Loading、Linking、Initalizing)行为之中,存储的不仅包括constant pool中的静态符号,还包括了程序运行期动态产生的符号,所以带有“runtime”的定语。它们两者是具有不同作用的独立概念。

周老师你好,我也认为单纯从《JVMS》这段描述,也无法说明"string iteral pool是jvm runtime constant pool的一个逻辑部分"。

我个人在看运行时常量池和字符串常量池时,也会感觉字符串常量池应该是一块独立的空间,和运行时常量池没有关系。运行时常量池单纯对应了.class文件中的常量池,只不过运行时常量池的内容会被解析成别的内容,之后运行时常量池的"项目数"不会增长。我们程序员自己去调用String.intern()也不应该动到运行时常量池,只是会用到字符串常量池。
@dingjs 说"此问题已困扰我多年",我觉得是真的。我自己没有能力看源码,只能去查,查了一天,都没查明白"运行时常量池和字符串常量池是否有包含关系,还是独立的?"和“String.intern()真的会动到运行时常量池吗?”这两个问题,因为都没有确凿的证明。我更倾向于 @dingjs 的看法吧,纯粹个人看法。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants