# 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 的字符串拼接方式

  1. StringBuilder 的 apped () 方式:自始至终只创建过一个 StringBuilder 的对象,而使用 String 的字符串拼接方式需要创建多个 StringBuilder 和 String 对象
  2. 使用 String 的字符串拼接方式:内存中由于创建了多个 StringBuilder 和 String 对象,内存占用更大,GC 时间更长

改进的空间:在实际开发中,如果基本确定要添加的字符串不高于某个限定值,建议使用构造器创建 StringBuilder, 即 new StringBuilder(1000) (因为 StringBuilder 内部无参构造默认创建了 char [16] 长度的数组,如果超过了此长度,需要新建数组并将数据复制到新数组)

# String#intern 解析

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);

String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
  • 在 JDK6 下 false false
  • 在 JDK7 下 false true

为什么会造成这种差异呢,主要原因是在 JDK7 及以后的版本,jvm 将 StringTable 的位置从永久代转移到了堆中,这样做有很多好处 :

JVM 不对永久代 GC 做强制性要求,而且永久代 GC 频率极低,而 String 的应用场景特别多,String 放在永久代中会造成空间的浪费,且默认情况下,永久代空间都比较小,很容易 OOM. 而如果把 StringTable 放到堆中,我们调参只需要调整堆空间,且避免了空间浪费

回到正题,为什么 StringTable 转移到堆中,后面的 s3 == s4 就为 true 了呢

我们回顾一下 String 的创建过程

  1. 通过字面量直接创建

    1
    String a1 = "hello";

    通过此种方式创建的 String 变量,对象会直接储存在常量池中

  2. 非字面量创建

    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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];

public static void main(String[] args) throws Exception {
Integer[] DB_DATA = new Integer[10];
Random random = new Random(10 * 10000);
for (int i = 0; i < DB_DATA.length; i++) {
DB_DATA[i] = random.nextInt();
}
long t = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) {
//arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])); 不适用intern
arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern(); //使用intern
}

System.out.println((System.currentTimeMillis() - t) + "ms");
System.gc();
}

统一运行参数 : -Xms2g -Xms2g -Xmn1500M , 运行 intern 的代码时间为 2160ms, 不运行 intern 的代码时间为 826ms

a4ff8aee

使用intern创建的String对象

50f918aa

不适用intern创建的String对象

可以看出,虽然使用 intern 多花费了一秒多的时间,但是一共只有一千多个 String 对象,而未使用则有超过一千万个 String 对象

但是使用 intern 务必谨慎!!!

美团: intern 详解