# String 底层 Hashtable 结构的说明
# String 的基本特性
- 字符串常量池中不会储存相同内容的字符串
- String 的 String Pool 是一个固定大小的 Hashtable, 默认值大小长度为 1009. 如果放进 String Pool 的 String 非常多,就会造成 Hash 冲突严重,从而导致链表会很长,而链表长了后会直接造成的影响就是当调用 String.intern 时性能会大幅下降
- 使用 - XX:StringTableSize 可设置 StringTable 长度
- 在 jdk6 中 StringTable 是固定的,就是 1009 长度,所以如果常量池中的字符串过多就会导致效率下降的很快. StringTableSize 设置没有要求
- 在 jdk7 中,StringTable 的长度默认值 60013, 1009 是可设置的最小值
# String 的内存分配
在 Java 语言中有 8 种基本数据类型和一种比较特殊的类型 String. 这些类型为了使它们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念
常量池就类似于一个 Java 系统级别提供的缓存. 8 种基本类型的常量池都是系统协调的. String 类型的常量池比较特殊。它的主要使用方法有两种
直接使用双引号声明出来的 String 对象会直接储存在常量池中
例如 : String info = "hello world";
如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern () 方法
# 字符串拼接操作
- 常量与常量的凭借结果在常量池,原理是编译器优化
- 常量池中不会存在相同内容的常量
- 只要其中有一个是变量,结果就在堆中。变量拼接的原理是 StringBuilder
- 如果拼接的结果调用 intern () 方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
# 体会执行效率
通过 StringBuilder 的 apped () 方式添加字符串的效率要远高于使用 String 的字符串拼接方式
- StringBuilder 的 apped () 方式:自始至终只创建过一个 StringBuilder 的对象,而使用 String 的字符串拼接方式需要创建多个 StringBuilder 和 String 对象
- 使用 String 的字符串拼接方式:内存中由于创建了多个 StringBuilder 和 String 对象,内存占用更大,GC 时间更长
改进的空间:在实际开发中,如果基本确定要添加的字符串不高于某个限定值,建议使用构造器创建 StringBuilder, 即 new StringBuilder(1000) (因为 StringBuilder 内部无参构造默认创建了 char [16] 长度的数组,如果超过了此长度,需要新建数组并将数据复制到新数组)
# String#intern 解析
1 | public static void main(String[] args) { |
- 在 JDK6 下
false false - 在 JDK7 下
false true
为什么会造成这种差异呢,主要原因是在 JDK7 及以后的版本,jvm 将 StringTable 的位置从永久代转移到了堆中,这样做有很多好处 :
JVM 不对永久代 GC 做强制性要求,而且永久代 GC 频率极低,而 String 的应用场景特别多,String 放在永久代中会造成空间的浪费,且默认情况下,永久代空间都比较小,很容易 OOM. 而如果把 StringTable 放到堆中,我们调参只需要调整堆空间,且避免了空间浪费
回到正题,为什么 StringTable 转移到堆中,后面的 s3 == s4 就为 true 了呢
我们回顾一下 String 的创建过程
通过字面量直接创建
1
String a1 = "hello";
通过此种方式创建的 String 变量,对象会直接储存在常量池中
非字面量创建
1
2
3① String a2 = new String("hello");
② String a3 = a2 + " world";
③ String a4 = a2 + a3;① 首先查询字符串常量池中是否存在 "hello" 这个字符串,如果有的话,则 String 内部的 value 则指向该字符串,若没有的话,则在常量池创建该字符串,然后内部的 value 属性指向该字符串,一共新建了两个对象
![image-20221013213107240]()
② 首先将第一个变量的值取出来,创建一个 StringBuilder 对象,并调用一次 append 方法将 a2 写进去。接着,在常量池中把 "world" 字符串取出来,然后再调用一次 append 方法将 "world" 写进去,最后再调用 StringBuilder 的 toString 方法,返回一个 String 对象并赋值给 a3, 一共新建了三个对象
③ 因为没有出现过字面量,所以直接在堆中创建 StringBuilder, 并运行两次 append 方法,最后 toString 一个 String 变量出来,一共新建了两个对象
# 正确使用 intern 的例子
1 | static final int MAX = 1000 * 10000; |
统一运行参数 : -Xms2g -Xms2g -Xmn1500M , 运行 intern 的代码时间为 2160ms, 不运行 intern 的代码时间为 826ms


可以看出,虽然使用 intern 多花费了一秒多的时间,但是一共只有一千多个 String 对象,而未使用则有超过一千万个 String 对象
但是使用 intern 务必谨慎!!!
美团: intern 详解

