JDK7 中的String的内存结构

缘起

【1】中我们详述了String的不变性问题. 再次强调,String的不变性和String的final修饰符以及内部的value属性的final修饰符无关,只与String的所有方法”小心翼翼不发布value”有关.

现在,我们关心的是另一个问题——String在内存中的结构是长什么样子的? 本文结论对于JDK8中亦成立. 本文主要借鉴的是【3】

分析

先来看看一道经常出现的java面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package reverse;

public class Reverse {
public static void main(String[] args)
{
String c1=new String("abc");
String c2=new String("abc");
String c3=c1;
System.out.println("c1==c2:"+ (c1==c2)); // c1==c2:false
System.out.println("c1.equals(c2):"+c1.equals(c2)); // c1.equals(c2):true
System.out.println("c3==c1:"+(c3==c1)); // c3==c1:true
System.out.println("c1.equals(c3):"+c1.equals(c3)); // c1.equals(c3):true
c1="han";
System.out.println(c1+" "+c3); // han abc
System.out.println(""+(c3==c1)); // false
}
}

​ 示例①

回答上面的问题应该都不难,但是网上很多讲String内存结构的图片并没有画完整——例如下面的

声明一个String对象——String s = “abcd”;

将一个String变量赋值给另一个String变量——String s2 = s;

之所以说没画完整,是因为没有将字符串常量池画进去. 下面我们以一道题为入口点来谈谈JDK6和JDK7下的String的内存结构

来看两段代码

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
String s = new String("1"); // 相信很多 JAVA 程序员都做做类似 String s = new String("abc")这个语句创建了几个对象的题目。 这种题目主要就是为了考察程序员对字符串对象的常量池掌握与否。上述的语句中是创建了2个对象,第一个对象是"abc"字符串存储在常量池中,第二个对象在JAVA Heap中的 String 对象。
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);
}

​ 示例②

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
String s = new String("1");
String s2 = "1";
s.intern(); // 本行与上一行代码顺序交换,这是示例三和示例二唯一的区别
System.out.println(s == s2);

String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern(); // 本行与上一行代码顺序交换,这是示例三和示例二唯二的区别
System.out.println(s3 == s4);
}

​ 示例③

先说结果

JDK6下,所有打印都是false. JDK7下,示例②打印的是”false true”, 示例③打印的是”false false”.

在解释之前,我们需要知道以下事实

1
2
3
4
5
6
7
在 JAVA 语言中有8中基本类型和一种比较特殊的类型——String,。这些类型为了使他们在运行过程中速度更快,
更节省内存,都提供了一种常量池的概念。常量池就类似一个JAVA系统级别提供的缓存。
jdk6中的常量池(里面就有字符串常量)是放在 Perm 区(有些文献称之为永久区或者方法区或者叫非堆)中的,Perm 区和正常的 JAVA Heap 区域是完全分开
的,Perm 区是一个类的静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,
一旦常量池中大量使用 intern 是会直接产生java.lang.OutOfMemoryError: PermGen space错误的。(永久区也会被GC的) 所以在 jdk7 的版本中,字符串常量池已经从 Perm区(非堆的一部分)移动到正常的 Java Heap 区域
了(但是JDK7中永久区依旧还是存在,只是字符串常量池不再在其中了)。为什么要移动,Perm 区域太小是一个主要原因,而jdk8已经直接取消了 Perm 区域,
而新建立了一个元数据区(MetaSpace,这是一块堆外的直接内存,不指定大小的话, 虚拟机会耗尽系统全部内存)。应该是 jdk 开发者认为 Perm 区域已经不适合现在 JAVA 的发展了。

再来解释一下String的intern方法.

1
public native String intern();

这是一个本地方法. 在 jdk7后,oracle 接管了 JAVA 的源码后就不对外开放了,根据 jdk 的主要开发人员声明 openJdk7 和 jdk7 使用的是同一分主代码,只是分支代码会有些许的变动。所以可以直接跟踪 openJdk7 的源码来探究 intern 的实现。它的大体实现结构就是:JAVA 使用 jni 调用c++实现的StringTable的intern方法,该方法跟Java中的HashMap的实现是差不多的, 只是不能自动扩容。默认大小是1009。要注意的是,String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找,蜕化O(n)复杂度了)。在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致intern方法的效率下降很快。在jdk7中,StringTable的长度可以通过一个jvm参数指定:

1
-XX:StringTableSize=99991

其API注释为(以下注释是JDK8中的)

1
2
3
4
5
6
7
8
9
10
11
Returns a canonical representation for the string object.
A pool of strings, initially empty, is maintained privately by the class String.

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.

It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.

All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java™ Language Specification.

Returns:
a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.

翻译过来就是

1
2
3
4
有一个叫字符串常量池(下简称P)的东西(伊始是空的).当一个String对象(下简称S)的intern方法被调用,如果P中存在与S相等的字符串的话(相等用equals来定义),则intern方法返回的就是这个P中的String的引用.否则的话,这个String对象就被加入P并且加入之后再将此String的引用返回.
根据上面的论述,我们知道了,对于任何两个String对象s和t,s.intern()==t.intern()的充要条件是s.equals(t)返回true. 所有的字符串常量和字符串常量计算表达式(例如"ab"+"abc"这种)都会被"拘留"(intern,含义就是存起来,所以拘留这个名字翻译的不错). 注意,字符串常量表达式jvm会做优化,也就是"ab"+"cd",则字符串常量池中只会生成"abcd"这个字符串常量,而不会生成"ab"和"cd"这两个字符串常量.

总而言之,intern方法返回的是一个String对象的引用,这个引用保证是从P中返回的.

注意,上面的注释是从JDK8中的(但是JDK6中也是这么说的). 综上所述,JDK6中,String的内存结构是下图

情形1:

String a = “111”;

情形2:

String a = “1”+”11”;

同情形1

String a = new String(“111”);

一旦调用 a.intern(), 则

情形3:

String a = new String(“1”)+new String(“1”)+new String(“1”)

一旦调用A.intern()方法之后

注意,因为JDK6中,字符串常量只能存放在永久区(中的字符串常量池),所以堆中是不存储字符串常量的.

在JDK6下,示例②中 第二行之后

示例2的第三行过后

示例2的第四行过后

所以示例2的第五行的结果显然是 s==s2返回false,因为s中存储的地址值是堆中的,而s2中存储的地址值是永久区中的. 所以肯定不相等.

示例2的第七行过后

第八行过后

第九行过后

所以第十行,s3和s4中存放的地址值显然是不一样的,因为s3中保存的是堆中的地址,而s4中保存的是永久区中的地址,而他俩在JVM内存中是隔离的. 所以s3==s4肯定返回false.

至于示例③中也打印两个false,

示例③的第二行过后

第三行过后

第四行过后

所以第五行s和s2中保存的地址值依旧不等(即s==s2返回false). s中保存的地址值是堆内存中的一个地址,s2中保存的地址值是永久区中的一个地址.

第七行过后

第八行过后

第九行过后

所以s3==s4依旧返回false,因为s3中保存的地址是堆内存中的一个地址,而s4中保存的地址是永久区中的一个地址. 所以肯定不相等.

JDK6下,示例2和示例3中都返回false的根本原因就在于(从上面的图的演变也可以看出),字符串常量仅仅在永久区中存储,堆中并不保存字符串常量的. 堆中的对象(S也好,S3也罢)都只是通过intern方法才能获取到永久区中的字符串常量池的一块内存的引用而已(即永久区中的字符串常量池中的一块内存的地址). 而永久区和堆内存是隔离的. s也好、s3也罢都只是堆内存中的地址值. 所以都只会返回false.

下面来看看JDK7下示例2的代码

第二行过后

第三行过后

第四行过后

所以第五行问s==s2返回的是false.

第7行过后(这一行 最终生成2个对象,一个是下图的S3, 一个是字符串常量池中的”1”, 但是因为”1”已经生成了,所以不会在此行代码又生成一遍,至于两个匿名的new String(“1”)对象,虽然也会生成,但是我们这里不予考虑)

注意,此时字符串常量池中是没有”11”的(这一点和JDK6下是一样的)

第八行过后(第八行是理解的核心关键~)

s3.intern()这一行代码是将 S3中的”11”字符串放入 String 常量池中(因为此时字符串常量池中不存在”11”字符串),这一点和jdk6是一样的——需要在字符串常量池中”生成”一个 “11” 的对象,但是不一样的地方来了—— jdk7 中字符串常量池不再在 Perm 区域了——字符串常量池中不需要再存储一份对象了,而是可以直接存储堆中的引用(即下图的引用s33同样指向了S3)。那问题来了,字符串常量本身呢? 就是”11”呢? 不存在了!不再会创建咯~ 即下图

第九行过后

这里解释一下 s4=”11”为什么s4在上图中会最终指向S3, 因为s4在常量池中发现了”11”的存在(就是s33),而且就是一个指向S3的引用类型变量(即地址值),所以s4=”11”即 s4=s33, 而s33中保存的地址值就是S3的地址,所以s4中保存的地址值自然变成了S3的地址(所以上图才那么画:s4指向S3). 所以自然 第十行中的 s3==s4返回true.

下面来看JDK7中示例3的过程

第二行过后

这一点和JDK7下示例代码2是一样的.

第三行过后

第四行过后

所以从上图可以看出s==s2依旧返回false. 因为s和s2中保存的地址值显然不是一块内存。

第七行过后

第八行过后(与示例2在JDK7中不一样的地方在于,这里字符串常量池抢先一步了~)

第九行过后

所以s3==s4显然是false. 因为s3中保存的地址值和s4中保存的地址值从上图来看显然不是一个地址. s3中保存的地址值是堆内存S3的地址,s4中保存的地址是字符串常量池(虽然字符串常量池也在堆内存中了)

纵观我们刚才的论述,JDK7下为什么示例2和示例3代码的第一次打印都是false,而第二次打印一次是true一次是false? 就是因为创建s和创建s3的方式不一样,创建s的方式就伴随着在字符串常量池中创建了”1”(是字符串常量本身而不是引用),而创建s3的时候,字符串常量池中还没有”11”这个字符串常量. 所以就要比 s4=”11”和s3.intern谁快了(即谁放在前面执行).

后面我们的列出一些练习,完全是照搬【2】(但是做了一些自己的注释)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 下面结果除非特别说明,在JDK6、7下的结论是一样的
String s1 = new String("aaa");
String s2 = "aaa";
System.out.println(s1 == s2); // false, 因为s1指向堆中的地址,s2指向字符串常量池

s1 = new String("bbb").intern();
s2 = "bbb";
System.out.println(s1 == s2); // true,因为都指向字符串常量池的 "bbb" 这个字符串常量本身

s1 = "ccc";
s2 = "ccc";
System.out.println(s1 == s2); // true 因为都指向字符串常量池中的字符串常量"ccc"本身

s1 = new String("ddd").intern();
s2 = new String("ddd").intern();
System.out.println(s1 == s2); // true, 因为都指向字符串常量池中的字符串常量"ddd"本身

s1 = "ab" + "cd"; // jvm会优化,字符串常量池中只有"abcd", 而不会生成"ab"、"cd"
s2 = "abcd";
System.out.println(s1 == s2); // true, 因为都指向字符串常量池中的字符串常量"abcd"本身

String temp = "hh";
s1 = "a" + temp;
// 如果此处调用s1.intern 则最终返回true
s2 = "ahh";
System.out.println(s1 == s2); // false,因为s1指向的是堆内存,而s2指向字符串常量池中的字符串常量"ahh"本身.

temp = "hh".intern();
s1 = "a" + temp;
s2 = "ahh";
System.out.println(s1 == s2); // false,s1指向的是堆内存,s2指向的是字符串常量池中的"ahh"本身

temp = "hh".intern();
s1 = ("a" + temp).intern();
s2 = "ahh";
System.out.println(s1 == s2); // true, 因为都指向字符串常量池中的字符串常量"ahh"本身

s1 = new String("1"); // 同时会生成堆中的对象 以及字符串常量池中"1"这个字符串常量,但是此时s1是指向堆中的对象的
s1.intern(); // 常量池中的已经存在,所以哪怕是JDK7,字符串常量池中的"1"也已经存在了,不会变成s1引用
s2 = "1";
System.out.println(s1 == s2); // false, 因为s1指向的是堆内存,s2指向的是字符串常量池中"1"这个字符串常量本身

String s3 = new String("1") + new String("1"); // 此时生成了四个对象 字符串常量池中的"1"这个字符串常量 + 2个堆中的匿名的"1"对象 + s3指向的堆中的对象(注此时字符串常量池不会生成"11")
s3.intern(); // jdk1.7之后,常量池不仅仅可以存储对象,还可以存储堆中对象的引用,会直接将s3的地址存储在字符串常量池(此时就不再会创建"11"这个字符串常量了)
String s4 = "11"; // jdk1.7之后,常量池中的地址其实就是s3的地址
System.out.println(s3 == s4); // jdk6时是false, jdk1.7之后是true

s3 = new String("2") + new String("2");
s4 = "22"; // 常量池中不存在"22",所以会新开辟一个存储"22"对象的常量池地址, 即字符串常量池中就会创建"22"这个字符串常量.
s3.intern(); // 常量池22的地址和s3的地址不同
System.out.println(s3 == s4); // false

// 此例子由技术微信群中网友提供
String a = "helloworld";
final String b = "hello";
String d = "hello";
String c = b + "world";
String e = d + "world";
System.out.println(a == c); // true 因为final编译器会做优化——直接替换b为常量"hello", 即c就是"hello"+"world",所以a和c都指向字符串常量池中的"helloworld"
System.out.println(a == e); // false 因为a指向字符串常量池中的"helloworld", 而e中保存的是堆内存中一个地址, 所以是false

我们做一个小结

对于什么时候会在常量池存储字符串对象,我想我们可以基本得出结论:

  1. 显式调用String的intern方法的时候;
  2. 直接声明字符串字面常量的时候,例如: String a = “aaa”或者new String(“aaa”)或者其他情形.

字符串直接常量相加的时候,例如: String c = “aa” + “bb”; 其中的aa和bb只要有任何一个不是字符串字面常量形式(String literal),都不会在字符串常量池生成”aabb”. 反之,会在字符串常量池中生成”aabb”, 并且jvm会做优化——不会生成”aa”和”bb”在字符串常量池中.

如果有疑惑的话,可以结合编译产生的字节码文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 字节码为:
* 0: ldc #16; //String 11 --- 从常量池加载字符串常量11(这些字符串常量是编译的时候就准备好的)
2: astore_1 --- 将11的引用存到本地变量1,其实就是将s指向常量池中11的位置
*/
String s = "11";

/**
* 0: new #16; //class java/lang/String --- 新开辟了一个地址,存储new出来的对象
3: dup --- 将new出来的对象复制了一份到栈顶(也就是s1最终指向的是堆中的另一个存储字符串11的地址)
4: ldc #18; //String 11          
6: invokespecial #20; //Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
*/
String s1 = new String("11");

/**
* 0: new #16; //class java/lang/StringBuilder --- 可以看到jdk对字符串拼接做了优化,先是建了一个StringBuilder对象
3: dup
4: new #18; //class java/lang/String --- 创建String对象
7: dup
8: ldc #20; //String 1 --- 从常量池加载了1(此时常量池和堆中都会存字符串对象)
10: invokespecial #22; //Method java/lang/String."<init>":(Ljava/lang/String;)V --- 初始化String("1")对象
13: invokestatic #25; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #29; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V --- 初始化StringBuilder对象
19: new #18; //class java/lang/String
22: dup
23: ldc #20; //String 1
25: invokespecial #22; //Method java/lang/String."<init>":(Ljava/lang/String;)V
28: invokevirtual #30; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #34; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1 ---从上可以看到实际上常量池目前只存了"1",并没有"11"
36: invokevirtual #38; //Method java/lang/String.intern:()Ljava/lang/String; --- 调用String.intern中,jdk1.7以后,常量池也是堆中的一部分且常量池可以存引用,这里直接存的是s2的引用
39: pop --- 这里直接返回的是栈顶的元素
*/
String s2 = new String("1") + new String("1");
s2.intern();

/**
* 0: ldc #16; //String abc --- 可以看到此时常量池直接存储的是:abc, 而不会a、b、c各存一份
2: astore_1
*/
String s3 = "a" + "b" + "c";

/**
0: new #16; //class java/lang/StringBuilder
3: dup
4: ldc #18; //String why --- 常量池的"why"
6: invokespecial #20; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
9: ldc #23; //String true --- 常量池的"true"
11: invokevirtual #25; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: invokevirtual #29; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
17: astore_1
*/
String s1 = new StringBuilder("why").append("true").toString();
System.out.println(s1 == s1.intern()); // jdk1.7之前为false,之后为true

从上面的分析我们知道了jvm对字符串直接使用”+”进行拼接,除非是字符串常量之间的”+”拼接(也就是字符串常量表达式),都会new 一个StringBuilder进行优化.

然后回忆起我们学javase的时候,老师总是教育我们 下面的代码是性能低下的, 让我们不要写这种代码

1
2
3
4
String a = "1";
for (int i=0; i<10; i++) {
a += i;
}

为什么呢? 可以从上述代码生成的字节码角度来分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0:   ldc     #16; //String 1
2: astore_1
3: iconst_0
4: istore_2                   
5: goto 30
8: new #18; //class java/lang/StringBuilder --- 每个循环都建了一个StringBuilder对象,对性能有损耗
11: dup
12: aload_1
13: invokestatic #20; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #26; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
19: iload_2
20: invokevirtual #29; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
23: invokevirtual #33; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
26: astore_1
27: iinc 2, 1 ---- 计数加1
30: iload_2
31: bipush 10
33: if_icmplt 8 ----跳转(了解一点汇编的话,就知道这行啥意思~嘿嘿)

从上面第六行可知,真正的性能瓶颈在于每次循环都新建了一个StringBuilder对象
所以我们优化一下 :

1
2
3
4
StringBuilder sb = new StringBuilder("1");
for (int i=0; i<10; i++) {
sb.append("1");
}

编译产生的字节码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0:   new     #16; //class java/lang/StringBuilder        -- 在循环直接初始化了StringBuilder对象
3: dup
4: ldc #18; //String 1
6: invokespecial #20; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
9: astore_1
10: iconst_0
11: istore_2
12: goto 25
15: aload_1
16: ldc #18; //String 1
18: invokevirtual #23; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: pop
22: iinc 2, 1
25: iload_2
26: bipush 10
28: if_icmplt 15

只创建了一次StringBuilder对象. 所以性能会提高.

waitwaitwait~ 说了这么多废话,我想问一下,本文讲解的String的内存结构和【1】中的String到底有什么关系呢? 哈哈,我们【1】中讨论的String其实就是上面所有画的图中堆内存部分的S、S3这种的. 其实平时我们调用String的各种方法都是调用S、S3对象中的方法,包括String的value属性,也是保存在S、S3中的. 所以String的不可变性也利于字符串常量池的维护(因为根据intern方法的注释,它基于equals方法, 而equals方法就有赖于value属性的不变性.). 而且我们平时调用String的各种方法其实都不涉及字符串常量池中的字符串常量本身,而是与String对应堆内存(S啦,S3啦这种)中的value属性有关. 那什么时候与字符串常量池中的字符串常量有关呢? 就是调用String对象的intern方法的时候.

最后我们来讨论 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]));
arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
}

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

第12行和第13行中选择一行,另一行注释掉. 使用MAT分析最终堆内存中String对象的个数的时候,发现

打开第12行,注释掉第13行,String对象生成了1000w 个字符串对象,占用了大约640MB 空间。

打开第13行,注释掉第12行,仅仅生成了1345个字符串对象,占用总空间区区133KB左右。其实通过观察程序中只是用到了10个字符串,所以准确计算后应该是正好相差100w 倍。虽然例子有些极端,但确实能准确反应出 intern 使用后产生的巨大空间节省。这是典型的享元设计模式.

但是使用了 intern 方法后时间上有了一些增长(大概多出1秒的时间)。这是因为程序中每次都是用了 new String 后,然后又进行 intern 操作的耗时时间,这一点如果在内存空间充足的情况下确实是无法避免的,但我们平时使用时,内存空间肯定不是无限大的,不使用 intern 占用空间导致 jvm 垃圾回收的时间是要远远大于这点时间的。 毕竟这里使用了1000w次intern 才多出来1秒钟多的时间。

最后最后,我们提一下不正确的使用intern方法的后果. 而且例子来自著名的阿里出品的fastjson. 版本是1.1.24之前的版本.

1
2
3
4
5
6
7
8
9
10
11
/**
* Constructs a new entry from the specified symbol information and next entry reference.
*/
public Entry(char[] ch, int offset, int length, int hash, Entry next){
characters = new char[length];
System.arraycopy(ch, offset, characters, 0, length);
symbol = new String(characters).intern();
this.next = next;
this.hashCode = hash;
this.bytes = null;
}

注意到第7行使用intern缓存了字符串常量. 此版本的fastjson对所有的 json 的 key 使用了 intern 方法,缓存到了字符串常量池中,这样每次读取的时候就会非常快,大大减少时间和空间。而且 json 的 key 通常都是不变的。这个地方没有考虑到大量的 json key 如果是变化的,那就会给字符串常量池带来很大的负担。

参考

【1】https://yfsyfs.github.io/2019/07/03/java-lang-String-%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB/

【2】https://www.cnblogs.com/Kidezyq/p/8040338.html

【3】http://www.360doc.com/content/14/0721/16/1073512_396062351.shtml