字符串涉及到的所有空间在内存中存在于三个位置:方法区的常量池、堆中的 String 类型对象和虚拟机栈中 String 对象引用
new String 与 “hello”
String a = new String("hello ")
是创建对象的过程,String 对象创建过程与其他对象创建的过程大致相同。但是在<init>
阶段,会从常量池中寻找 “hello” 常量,若成功找到,则复制一份到堆中,构成 String 类型对象的一部分;否则,在堆中自行创建 “hello” 字符串,用于对 String 类中的属性进行赋值。
因此,无论怎么样,创建对象时,字符串一定存在于堆中,a 引用的指向是堆中的字符串,而不是常量池中的常量
String b = "hello "
先创建一个栈引用,然后从常量池中进行常量寻找(因为在 JVM 中,常量是一种资源,应当合理的重复使用),如果找到,则将引用指向它;否则,在常量池中创建新的常量 “hello”,然后进行引用指向。
字符串的连接
字符串的连接分为两种:编译期间的连接和运行期间的链接
编译期间的连接
final String a = "hello ";
final String b = "world";
String c = a + b;
上述代码的执行过程为:
因为 a 与 b 都是常量,在编译器中 a 就是 “hello”(同理,b 就是 “world”),两者无论使用哪个都是指的一个东西,所以在在编译的时候(要注意,这时还没有进行内存分配,字符串常量池还没有被初始化,所有的工作都停留在字节码层面)代码变成了这样:
final String a = "hello "; final String b = "world"; String c = "hello" + "world";
在拼接静态字符串的时候,编译器通常要进行进行优化,对 c 中字符串的拼接进行优化:
String c = "hello" + "world"; // 编译器将视为 String c = "hello world";
最后,等到内存分配阶段,字符串常量池中会有:”hello” “world” “hello world” 三个常量的存在。
运行期间的链接
final String constant_hello = "hello ";
final String constant_world = "world";
string var_hello = "hello ";
String concatenate_c = constant_hello + constant_world;
String concatenate_f = var_hello + constant_world;
System.out.println(concatenate_c == concatenate_f); // ?会相等吗,false
为什么 concatenate_c 和 concatenate_f 不相等呢?
我们知道,concatenate_c 在经过编译器的处理与优化过后,会变成:
concatenate_c = "hello world";
原因是 constant_hello 和 constant_world 为常量,编译器能够认识他们。而 var_hello 并非常量,编译器不认识它,所以会变成这样:
String concatenate_f = var_hello + "world";
在内存分配的时候,才会得知 var_hello 的值,这时候不会再做静态的拼接,而是进行动态的链接
concatenate_f --> "hello " --> "world"
concatenate_c == concatenate_f
自然不成立了。
字符串相等不想等(而非
equals()
),这是一个很有意思的问题,既要考虑编译器的优化,还要考虑字符串内存的分配与管理
参考资料:
- IBM - Java 性能优化之 String 篇
- 《深入理解 JVM 虚拟机》