JVM-String-Pool
最近在学习JVM的过程中,遇到了一个十分容易混淆的概念,字符串池。
有关字符串池的代码,运行结果始终没有和预期对应上。查询了大量资料无果后,在stackoverflow上发布了提问,提问链接。提问后的半天内(在此感叹下国际友人的钻研精神),有两位大佬和我就此问题进行了探讨。在交流中对这个概念的理解逐渐清晰。通过将这个事情记录下来,来理清自己的思路,并分享给更多人。
由于具体情况可能在不同JVM上表现不同,本次实验和讨论基于JDK8,HotSpot JVM。
问题
1 | |
有上述两段代码,唯一的区别就是是否调用了s1.intern(),而产生了不同的输出结果。
intern是一个native方法,在Java8源代码中的注释如下:
When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.
这段代码的字节码如下:
1 | |
错误的理解
在源代码中,出现了
"12"、"21"、"1221"字面量。因此编译后,字节码常量池中可以观察到这三个字面量对应的CONSTANT_String_info,这些CONSTANT_String_info在这些指令中被使用:1
2
311 ldc #5 <12>
23 ldc #8 <21>
40 ldc #11 <1221>当程序运行,类加载器加载这个类时,字节码文件中常量池被载入内存。在内存中称为运行时常量池。字符串池是运行时常量池的一部分,因此在字符串池中包含
"12"、"21"、"1221"这些字符串。new String("12")在堆中申请了一块空间,容纳新String实例,并从字符串池中取出"12",并用此对新实例进行初始化。对应的字节码如下,new String("21")也是同理。1
2
37 new #4 <java/lang/String>
11 ldc #5 <12>
13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>+进行字符串拼接,本质被编译器转化为了StringBuilder实例和进行append方法,并最后调用toString方法,对应的字节码:1
2
3
40 new #2 <java/lang/StringBuilder>
4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
31 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>在
toString方法中,调用了new String,因此最后返回的s1是"1221"的String实例,位于堆上。根据文档,
s1.intern()方法,查看了字符串池,发现确实存在"1221",返回了字符串池中的地址引用。不管如何,代码中没有利用返回值,只是将他pop掉了,因此不会对已有的变量进行任何改动。String s2 = "1221"查看了字符串池,发现存在,因此将其地址引用赋值给s2。对应字节码为:1
240 ldc #11 <1221>
42 astore_2==运算符对于引用类型来说,比较的是俩者的地址。s1的地址指向堆中,s2的地址指向字符串池中。俩者怎么可能会相等?
疑惑
s1和s2到底指向哪里?- 为什么调用
intern方法会影响代码行为,甚至没有去利用其返回值?
正解
错误出现在理解2和6中,字符串池并没有随着类加载,被载入运行时常量池。s1.intern()是首次将"1221"加入到字符串池中,而后运行的String s2 = "1221",会根据字符串池中是否存在"1221"改变行为。
为了更好解释这个问题,首先对涉及的关键概念进行定义。
关键概念
- 常量池:字节码中的一种存储结构,用来存储源代码中用到的常数、字符串,类、字段、方法、接口、参数类型等。位于硬盘上的字节码文件中。具体见官方文档。
- 运行时常量池:程序运行时,内存中的常量池。在类加载时,会将常量池数据加载到
JVM方法区,形成运行时常量池。具体见官方文档。 CONSTANT_String_info:常量池中的一种数据结构,储存了源代码中字符串字面量对应的Unicode序列,具体见官方文档。- 字符串池:JDK8堆中的一块内存区域,用于存取用到过的
String实例。源码中String.intern()函数的官方文档。
A pool of strings, initially empty, is maintained privately by the class String.
ldc #5,将序号为5的常量从运行时常量池中推到操作数栈上,具体见官方文档。在使用到通过字面量表示的字符串,都会先检查字符串池中是否存在对应的string实例。如果存在,则入栈其引用地址;如果不存在,则在字符串池内创建string实例并入栈其地址。
错误原因
错误来自于误解了字符串池和常量池(此后不加区分地使用常量池和运行时常量池)间的关系。
虽然通常称其为字符串常量池,但其和常量池并不存在属于关系。因此并不会随着类的加载而载入。在JDK6时,字符串池和常量池都位于永久代,好似之间存在一些关系。但在JDK8中,字符串池被移到堆中。与其说是常量池的一部分,更不如说是String类的一部分。可以理解为String类的一个私有的成员变量,虽然在String源码中无法观察到。字符串池中的String实例在被创建后,无法改变实例中的字节数组字段。如果对已有的String实例进行改变的操作,会生成一个新的String实例,表现出常量的特性,所以通常称其为字符串常量池。但为了避免混淆字符串池和常量池,本文尽量使用字符串池,而不是字符串常量池。
与其容易混淆的另一个概念是常量池中的CONSTANT_String_info。里面以Unicode序列储存着字符串字面量,并且会随着类加载,载入到运行时常量池中。但其和字符串池有着本质区别:CONSTANT_String_info只储存了Unicode序列,而字符串池储存了String实例。String实例不仅包含了Unicode序列,还包含了其他成员属性,如hash。并且String类绑定了许多方法,这些方法无法在CONSTANT_String_info上执行。可以通过以CONSTANT_String_info中的Unicode序列为参数,执行String初始化函数,生成对应的String实例。
具体行为
根据文档,字符串池初始为空,并会在字符串第一次被使用时被加入到字符串池中。以后再用到相同的字符串,会复用字符串池已经存在的对象。所以详细描述的ldc #5执行过程应该是这样的:
1.取出运行时常量池中的索引为5的Unicode序列
2.根据此序列,在字符串常量池中寻找是否存在对应的String实例
3.如果存在,则将其引用入栈
4.如果不存在,则在字符串中创建一个新的String实例,以Unicode序列为参数,执行初始化,并将新创建的String实例的引用入栈。
而new关键字,则会在堆(字符串池外)中创建一个新的String实例。如果通过字面量形式赋值,如new String("12"),则会现在字符串池中创建实例,再以字符串池中的实例为参数,调用参数类型为String的初始化函数。执行完后,内存中存在两个一模一样的String实例,一个在堆中,一个在字符串池中。这点可以从字节码指令中看出:
1 | |
向字符串池中加入新的String实例通常有两个方法。ldc一个字符串池中不存在的常量,或者调用一个字符串池中不存在的string实例的intern方法。第二中方法相对少见,因为这需要在堆上,不通过字面量的形式创建一个string实例。这通常需要通过字符串操作来做到。
对于一个字符串池中不存在的string实例,对其调用intern方法会将其引用添加到字符串池中,而不是重新在字符串池中创建一个新的相同的实例。此为JDK8 Hotspot的行为,这个行为可能会因JDK版本和JVM具体实现而异。例如在JDK6就会重新在字符串池中创建一个新的相同的实例。
因此回答上文提到的两个问题:
1.s1指向堆中的"1221" String实例,s2根据字符串池中储存的引用,最终也指向了堆中的"1221" String实例。所以在JDK8中,内存中存在两处"1221"序列,一处在常量池中,一处在堆上。字符串池由于存储的是引用,因此没有"1221"序列。
2.因为s1没有通过字面量,而是字符串拼接的形式创建了"1221" String实例,在这种情况下,字符串池中不存在"1221",调用intern方法会影响字符串池,从而影响到ldc指令获取到的地址。
总结
避免混淆字符串池、常量池、CONSTANT_String_info的概念和之间的关系。
字符串池是一个位于堆上的,用于管理String实例的数据结构,可以被视为String类的一个私有成员变量。
常量池是字节码文件中的存储结构。运行时常量池是在类加载过程中,将常量池加载到方法区/元空间形成的内存区域。
CONSTANT_String_info是常量池/运行时常量池中的存储结构,储存了源代码中用到的字符串字面量的Unicode序列。是常量池的一部分,随类加载而加载。