什么是字符串?
字符串是由引号所括起来的一系列字符序列。例如"String","Hello"就为一个字符串
String 的不可变性
"String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何操作都会生成新的对象“。
- 固定不变 - 从String 对象的源码中可以看出,String 类声明为 final,且它的属性和方法都被 final 所修饰
- 任何操作都会生成新对象 - String:: subString(),String::concat() 等方法都会生成一个新的String对象,不会在原对象上进行操作
从下面String源码部分中很容易得到上面的结论:
/** String 类源码 */ |
接下来使用一段代码来揭示这个过程:
public class ImmutableStrings |
在这段代码中,没有改变任何对象。首先在第一个代码中,会在堆内存中创建一个新的String 对象,并把它的引用赋值给 start,接着在第二个调用String:: concat()方法对字符串进行拼接,此时会创建一个新的String 对象,该对象是"Hello" 和 “World” 的串联。就如String:: concat() 源码所示,第三个/四个代码的输出结果分别为:“Hello World!”, “World”。并且操作符 " + "完成了和String:: concat() 类似的事 - > 操作符 “+” 算是一个语法糖,查看编译之后的字节码可以知道最终会调用StringBuilder:: append() 来完成字符串的拼接。
/** concat() 源码 */ |
不可变性设计的初衷
- 字符串常量池的需要。String对象的不可变性为字符串常量池的实现提供了基础,使得常量池便于管理和优化。
- 多线程安全。同一个字符串对象可以被多个线程共享。
- 安全性考虑。字符串应用场景众多,设计成不可变性可以有效防止字符串被有意篡改。
- 由于String对象的不可变性,可以对其HashCode进行缓存,可以作为HashMap,HashTable等集合的key 值。
字符串常量池
很多文章都提及到字符串常量池是String对象的集合,这种说法很接近了,但是更准确来说,它是 String 对象引用的集合 (网上关于这个众说纷纭,我更加倾向于存储的是引用的集合~ 若有错误了请指出! 谢谢~ ps: 又看了一遍书,发现在JDK 6以前,永久代中的字符串常量池是存放String 对象实例的,但是JDK 7之后,字符串常量池移到了堆中,String 对象实例也是在堆中的,那字符串常量池只需要保存String 对象的引用就行啦~ 详见《深入理解Java虚拟机》 P63)。 虽说String 是不变的,但是它还是和Java中的其他对象一样,是分配在堆中的,所以说 String 对象存在于堆中,字符串常量池存放了它们的引用。因为 String 对象是不可变的,所以多个引用 “共享” 同一个String 对象是安全的,这种安全性就是 字符串常量池所带来的。
字面量的形式创建字符串
public class ImmutableStrings{ |
执行完上面的第一句代码之后,会在堆上创建一个String 对象,并把String 对象的引用存放到字符串常量池中,并把引用返回给 one,那当第二句代码执行时,字符串常量池已经有对应内容的引用了,直接返回对象引用给 two。one.equals(two) / one == two 都为true。 图形化如下所示:
new 创建字符串
public class ImmutableStrings |
在使用 new关键字时的情况会有稍微不同,关于这两个字符串的引用任然会存放字符串常量池中,但是关键字 new使得虚拟机在运行时会创建一个新的String对象,而不是使用字符串常量池中已经存在的引用,此时 two 指向 堆中这个新创建的对象,而one 是常量池中的引用。 one.equals(two) 为 true,而 one == two 都为false。
如果想要one,two都引用同一个对象,则可以使用 String:: intern()方法 - 当调用intern()方法时,如果字符串常量池中已经有了这个字符串,那么直接返回字符串常量池中它的引用,如果没有,那就将它的引用保存一份到字符串常量池中,然后直接返回这个引用。这个方法是有返回值的,是返回引用。
String one = "someString"; |
垃圾收集
当一个对象没有引用指向时,垃圾收集器便会对它进行收集操作。看下面的一个事例:
public class ImmutableStrings |
当 one = two = null时,只有一个对象会被回收,String 对象总是有来自字符串常量池的引用,所以不会被回收
String 对象的创建和字符串常量池的放入
上面嘀咕了那么久,那到底什么时候会创建String 对象?什么时候引用放入到字符串常量池中呢?先需要提出三个常量池的概念:
- 静态常量池:常量池表(Constant Pool table,存放在Class文件中),也可称作为静态常量池,里面存放编译器生成的各种字面量和符号引用。其中有两个重要的常量类型为CONSTANT_String_info和CONSTANT_Utf8_info类型(具体描述可以看看《深入理解Java虚拟机》的p 219 啦~)
- 运行时常量池:运行时常量池属于方法区的一部分,常量池表中的内容会在类加载时存放在方法区的运行时常量池,运行时常量池相比于Class文件常量池一个重要特征是 动态性,运行期间也可以将新的常量放入到 运行时常量池中
- 字符串常量池:在HotSpot 虚拟机中,使用StringTable来存储 String 对象的引用,即来实现字符串常量池,StringTable 本质上是HashSet
,所以里面的内容是不可以重复的。一般来说,说一个字符串存储到了字符串常量池也就是说在StringTable中保存了对这个String 对象的引用
执行过程
有了上面的概念之后,便可来描述下述过程了
首先给出结论,“在类的解析阶段,虚拟机便会在创建String 对象,并把String对象的引用存储到字符串常量池中”。
- 当*.java 文件 编译为*.class 文件时,字符串会像其他常量一样存储到class 文件中的常量池表中,对应于CONSTANT_String_info和CONSTANT_Utf8_info类型;
- 类加载时,会把静态常量池中的内容存放到方法区中的运行时常量池中,其中CONSTANT_Utf8_info类型在类加载的时候就会全部被创建出来,即说明了加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,但是此时StringTable(字符串常量池)并没有相应的引用,在堆中也没有相应的对象产生;
- 遇到ldc字节码指令(该指令将int、float或String型常量值从常量池中推送至栈顶)之前会触发解析阶段,进入到解析阶段,若在解析的过程中发现StringTable已经有与CONSTANT_String_info一样的引用,则返回该引用,若没有,则在堆中创建一个对应内容的String对象,并在StringTable中保存创建的对象的引用,然后返回;
具体示例
下面给出几个具体实例,来说下这个过程:
- 字面量的形式创建字符串
public class test{ |
通过javap 反编译后的字节码代码如下所示
#2 = String #14 |
当编译成字节码文件后,字面量"HB" 会存储到常量类型 CONSTANT_Utf8_info中,类加载时,其也会随之加载到方法区中的运行时常量池中,接下来可以用此来在StringTable查询是否有匹配的String 对象引用(当然只是简化的说法,具体CONSTANT_Utf8_info还指向一个Symbol对象~);遇到第一个ldc字节码指令之前,解析过程中发现StringTable(字符串常量池)还没有与CONSTANT_String_info一样的引用,则在堆中创建一个对应内容的String对象,并在StringTable中保存创建的对象的引用,然后返回;astore_1指令把返回的引用存到本地变量name; 遇到二个ldc字节码指令之前,解析过程中发现StringTable(字符串常量池)已经有与CONSTANT_String_info一样的引用,则直接返回即可,并通过astore_2 指令将其返回的引用保存到本地变量 name2中
- new 创建字符串
public class test2{ |
通过javap 反编译后的字节码代码如下所示
public static void main(java.lang.String[]); |
使用了关键字new后,会有稍微不同,new 指令会在堆中创建一个新的String 对象,并将其引用值压入栈顶,通过dup指令 复制栈顶的新对象的引用值并把复制值压入栈顶,本地变量name 所保存的值就为该引用值;接下来在遇到第一个ldc字节码指令之前,解析过程中发现StringTable(字符串常量池)还没有与CONSTANT_String_info一样的引用,则在堆中创建一个对应内容的String对象,并在StringTable中保存创建的对象的引用, 所以在运行时,会创建两个String对象哦~接下来的过程和前面的差不多,就不一一叙述啦!
- 其他重要值得关注的示例
String s1 = new String("hb"); |
总结
- String 对象存在于堆中,字符串常量池存放了它们的引用
- 字符串常量池存储String对象的引用,且是全局共享的,相同的字符串都将指向同一个字符串对象
- 运行时创建的字符串(new)关键字 和 “” (字面量形式) 创建的字符串存在不同
- 检查字符串是否相同的最好方法是 equal()
- 可以通过String:: intern() 方法从常量池中得到String对象的引用,或 将String 对象的引用存入到 字符串常量池中
- 上述所有的实验都是在JDK 8 HotSpot虚拟机下进行的,在JDK 7 中HotSpot,字符串常量池移到了堆中哦~,所以不同JDK版本,不同虚拟机下可能存在差异
参考资料
[1] https://javaranch.com/journal/200409/ScjpTipLine-StringsLiterally.html
[2] https://www.iteye.com/blog/rednaxelafx-774673#comments
[3] https://www.zhihu.com/question/55994121/answer/408891707
[4] https://www.cnblogs.com/Kidezyq/p/8040338.html