一、前言
之前已经研究过了,但是今天和别人讨论了这个问题,有了点新的见解,于是记录下来。
二、代码测试
public static void main(String[] args) {
String ab =
new String("a")
+
new String("b");
String c = new String("c");
String cd = c + new String("d");
System.out.println(ab + cd);
System.out.println();
}
- 测试结果:
abcd
三、查看字节码中生成了多少个对象
- 字节码:
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init>>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <a>
13 invokespecial #6 <java/lang/String.<init>>
16 invokevirtual #7 <java/lang/StringBuilder.append>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <b>
25 invokespecial #6 <java/lang/String.<init>>
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString>
34 astore_1
35 new #4 <java/lang/String>
38 dup
39 ldc #10 <c>
41 invokespecial #6 <java/lang/String.<init>>
44 astore_2
45 new #2 <java/lang/StringBuilder>
48 dup
49 invokespecial #3 <java/lang/StringBuilder.<init>>
52 aload_2
53 invokevirtual #7 <java/lang/StringBuilder.append>
56 new #4 <java/lang/String>
59 dup
60 ldc #11 <d>
62 invokespecial #6 <java/lang/String.<init>>
65 invokevirtual #7 <java/lang/StringBuilder.append>
68 invokevirtual #9 <java/lang/StringBuilder.toString>
71 astore_3
72 getstatic #12 <java/lang/System.out>
75 new #2 <java/lang/StringBuilder>
78 dup
79 invokespecial #3 <java/lang/StringBuilder.<init>>
82 aload_1
83 invokevirtual #7 <java/lang/StringBuilder.append>
86 aload_3
87 invokevirtual #7 <java/lang/StringBuilder.append>
90 invokevirtual #9 <java/lang/StringBuilder.toString>
93 invokevirtual #13 <java/io/PrintStream.println>
96 getstatic #12 <java/lang/System.out>
99 invokevirtual #14 <java/io/PrintStream.println>
102 return
- 由字节码可以看到,明面上
String ab = new String("a") + new String("b");
是生成了1+1+1=3个对象,但是通过debug后,发现并不是这样的。
三、debug发现剩下的对象
- 首先打断点:
- 此时刚到第一行,这里是还没有任何
"a"
相关的字符串的:
- 我们在String对象初始化方法中打断点,然后再查看,此时出现了一个
"a"
的字符串对象,同时上面还有一个未初始化的对象,我猜它是当前正在初始化的new String("a")
,而忽然出现的"a"
字符串对象,我个人认为是因为String(String original)
的原因,因此此时参数那里就出现了字符串字面量了,根据3.10.5 of the The Java™ Language Specification
可以看到这两句话:- A string literal is always of type String (§4.3.3).
- A string literal always refers to the same instance (§4.3.1) of class String.
- 我猜测这个字符串就是当引用
"a"
的时候,发现字符串常量池没有它的引用,于是就创建并intern
将它的引用放入字符串常量池中,然后再引用,在intern()
的API文档中就讲到了字符串字面量会自动调用intern
:- All literal strings and string-valued constant expressions are interned.
- 因此得出结论,当在
new String("a")
的时候,出现了字符串字面量"a"
,又因为字符串字面量总是字符串对象,因此它会自动创建,然后返回一个引用,这个引用作为参数传入到new String("a")
这个构造方法里,就出现了上图情况。 - 我用猜测而不是肯定,是因为我找不到官方文档关于字符串字面量何时创建的相关资料,如果有大佬找到,欢迎补充。
- 然后再看我框起来的那个正在初始化的对象,我猜它就是当前正在初始化的
new String("a")
,接下来我给它打个标记并看看是不是真的:
- 可以发现,我猜的正确。然后接下来就是初始化
this.value
和this.hash
了,看源代码,可以得知,会直接将original
的字符数组赋值过去,这样就保证了它们的字符串是一样的,又因为字符串无法修改,这样子这个字符数组也改不了了,因此都可以用同一个,不需要多份字符数组。 - 我们再标记一下
original
字符串:
- 可以看到,的确是同一个,同时,无论是正在new的String(“a”)和字符串字面量
"a"
,它们的this.value
都是同一个。那么,我们可以判断出:new String("a")
是创建了两个字符串对象。- 如果
"a"
字符串字面量出现过的话,那么new String("a")
只会创建一个字符串对象,因为相同的字符串字面量都是调用的同一个字符串对象,而"a"
早已出现并创建过一个字符串对象,然后通过intern
将它的引用放入字符串常量池。 - 官方文档中有讲相同的字符串字面量都是调用的同一个字符串对象:
- The Java programming language requires that identical string literals (that is, literals that contain the same sequence of code points) must refer to the same instance of class String (JLS §3.10.5).
- 如果
- 然后出来
new String("a")
的方法:
- 出来后,刚刚new出来的
String("a")
,却不见了,然后我看了下局部变量表(图片右侧),发现编译期间居然都没有给它分配一个槽位,这应该是编译器编译的时候优化了。(我记错了,因为我都没将这个new出来的对象地址赋值给局部变量,因此自然不会保留) - 此时是找不到
"b"
的:
- debug进入
new String("b")
的方法:
- 发现
"b"
了,同时也发现一个未初始化对象,对它们做标记:
- 此时又出现了两个字符串对象,一个是字符串字面量
"b"
第一次出现生成的字符串对象,另外一个是new String("b")
对象。 - 继续debug,跳出
new String("b")
:
- 可以看到,此时并没有
"ab"
字符串对象,那么它究竟在哪里生成呢?毕竟现在已经生成了5个对象了,1个StringBuilder
对象,4个String
对象; - 然后我们知道
StringBuilder
在拼接完字符串后,会用toString()
方法,因此我们在那里打断点:
- 可以看到
"ab"
的字符串对象还没出现,但是为什么看下方的this
有"ab"
呢?其实那时字符数组,并不是字符串对象,而下面的new String的方法,调用的也不是之前那个String(String original)
,而是public String(char value[], int offset, int count)
,因此我们在那个方法打断点:
- 进去后,
"ab"
还是没出现,其中有个未初始化的对象,我标记一下它:
- 它就是当前正在初始化的字符串对象,可以看到value数组是
'a', 'b'
,但是当前初始化字符串对象的this.value
还没有被赋值,我们直接到最后一行并运行,中间是非法判断,不需要管。
- 此时进入到最后一行的方法里,看看最后的
copy
是什么?同时看看"ab"
出现了没有。(中间的System.arraycopy
是native方法,看不到原码)
- 此时
"ab"
字符串对象还是不存在,而copy
是一个字符数组:{'a', 'b'}
,很明显了,这个字符数组就是返回赋值给正在new的字符串对象的this.value
了。
- 此时就生成了
"ab"
字符串对象了,它的this.value
地址都和前一张图的copy
字符数组地址一样。 - 到目前为止,已经生成了6个对象了,1个
StringBuilder
对象,2个因为字符串字面量"a"
和"b"
第一次出现而生成的两个字符串对象,2个是直接显式new出来的字符串对象,1个则是StringBuilder
在append完后,通过toString
生成的字符串对象。 - 继续debug:
- 变量
c
引用的是显式new出来的字符串,而不是字符串字面量"c"
,众所周知,通过+来拼接字符串对象的时候,会通过隐式new一个StringBuilder对象来append,我们看看append的是哪一个。因为不小心debug过了,所以重新运行并debug到那一步:
- append时候,用的是
c
变量的地址指向的字符串对象,就是显式new出来的。后面的new String("d")
就和前面操作一样了。 - 当然这个是要看你究竟是变量c还是字符串字面量
"c"
,如果是变量c自然是用的变量c指向的字符串对象,但是要是直接用字符串字面量"c"
,那么append的就是字符串字面量"c"
对象了,而不是new出来的那个。
四、补充1:字符串字面量不是首次出现
-
如果
"a"
不是第一次出现的话,那么String ab = new String("a") + new String("b")
,就只会生成5个对象了。(假设"b"
是第一次出现) -
如下图的debug:
- 此时并没有
"a"
字符串对象,下一行:
- 出现了
"a"
的字符串对象,我这里做了标记,此时在到String ab = new String("a") + new String("b")
开始的时候,"a"
已经不是第一次出现了,那么此时这一行会生成多少个字符串对象了,我们现在知道的是5个,但是不知道"a"
会不会重复生成,于是我们在String初始化方法打断点,并进入,看看new String("a")
中传入的字符串original是谁,究竟是刚刚生成的那个字符串对象,还是会生成一个新的字符串对象再传入:
- 答案显而易见,用的是刚刚生成的
"a"
字符串对象,而不会再新生成一个,因为根据:- The Java programming language requires that identical string literals (that is, literals that contain the same sequence of code points) must refer to the same instance of class String (JLS §3.10.5).
- 可以知道,拥有相同字符的字符串字面量,它们的引用必定是同一个。(原文是叫码点值序列)
- 因此,
String ab = new String("a") + new String("b")
此时生成的是5个对象。
五、补充2:字符串字面量加载时机
- 最近重新看jvm相关的知识,才发现当字节码文件加载进内存的时候,在解析resolve阶段,会将常量池里面的符号引用转为直接引用,而对于常量池的字面量中的字符串字面量来讲,它被生成为字符串对象的时机并不是字节码文件被加载进内存的解析阶段。JVM规范里明确指定resolve阶段可以是lazy的。
- CONSTANT_Utf8会在类加载的过程中就全部创建出来,而CONSTANT_String则是lazy resolve的,例如说在第一次引用该项的ldc指令被第一次执行到的时候才会resolve。
- 执行ldc指令就是触发这个lazy resolution动作的条件。
- 也就是当ldc出现且字符串字面量是第一次出现的时候,才会触发这个lazy resolve动作,new出一个字符串对象,并且自动调用这个字符串对象的
intern()
方法将其地址放入字符串常量池中。 - 来源知乎这个问答的高赞回答:https://www.zhihu.com/question/55994121/answer/147296098
六、总结
-
String ab = new String("a") + new String("b")
总共创建了6个对象,1个StringBuilder
对象,2个因为字符串字面量"a"
和"b"
第一次出现而生成的两个字符串对象,2个是直接显式new出来的字符串对象,1个则是StringBuilder
在append完后,通过toString
生成的字符串对象。 -
但是对于
"a"
和"b"
字符串字面量不是第一次出现的时候,它们并不会新建新的字符串对象。 -
总而言之,要视情况而定。