String s = new String("abc)创建了几个对象问题,引起的思考
前言
由于最近在备战实习offer和一些不可抗拒因素,写文章的时间就大大减少了,不过这只是暂时的。
起因
以前刷面经的时候看到过这样的一个问题:
代码语言:javascript复制String s = new String("abc)创建了几个对象
先给出答案:1或2个
然后今天偶然在一个公众号的推文上,看到了一篇讲述该流程的文章,我就是觉得别扭,查了好多网上的文章还是感觉不太清晰,我的问题主要有两点:
- 字符串是何时进入字符串常量池的
- 我们在JDK7及以后已经将字符串常量池从方法区移到了我们的堆上,那么这时对于字符串常量的存储肯定就与JDK6不同,那么具体的不同点在哪里呢?
本次就让我们由浅入深,来认识一下我们这个熟悉的陌生朋友,"String"!
字符串常量池究竟存了哪些东西
了解过JVM运行时数据区(简称JVM)的同学肯定都知道,在我们的JVM中有一块叫做字符串常量池的内存,在JDK6中它属于我们的方法区,从JDK7开始将它移动到堆中,那么字符串常量池是干嘛的呢?
因为字符串是一个常用对象,频繁的创建会严重影响性能,而字符常量池就好比一个缓冲区,用来存放我们的字符串对象,做全局共享。
上面应该是我们在普遍得到的答案,但是我们知道在JDK7开始我们的字符串常量池已经在堆中了,没必要每个String对象都存放在字符串常量池中了,也可以只存放该字符串对象的引用。
在JDK 6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用. 而JDK 7(以及部分其他虚拟机,例如JRockit)的intern()方法实现就不需要再拷贝字符串的实例 到永久代了,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可. ---深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)p63
实战分析,字符串常量池中可以存放对象引用:
代码语言:javascript复制public class String_Test {
public static void main(String[] args) {
String s1 = new StringBuilder("a").append("b").append("c").toString();
String s2 = s1.intern();
System.out.println(s1==s2);
}
}
输出:true
字符串究竟是何时进入字符串常量池的?
在我们的类文件结构中,String类型的字符串常量有两种类型表示方式,CONSTANT_String_info、CONSTANT_Utf8_info,它们的格式如下:


我们从其命名可以看出,CONSTANT_String_info应该就是我们所说的String类型在常量池中的表述,但是根据其结构我们可以发现这其实就是个壳子,里面并没有实质内容,有的只是一个index用来指向字符串字面量的索引,所以我们可以得出其实CONSTANT_Utf8_info才是字符串的真正持有者。
我们要了解的是JVM规范中允许resolve阶段是可以lazy的。
- 字符串常量在编译时,将会以CONSTANT_Utf8_info CONSTANT_String_info的形式存放在class文件常量池中;
- 字符串常量在类加载之后,将会以"JVM_CONSTANT_String"的形式存放在运行时常量池中,此时内容跟Class文件里的CONSTANT_String_info一样只是一个 index;
- 字符串常量在resolve之后,将会变成最终的 JVM_CONSTANT_String,以真正的String对象方式存放在字符串常量池中。而触发resolve的时机就是首次使用该字符串常量,也执行到就是ldc字节码指令;
常见面试题:
JVM是如何执行String s = "abc"的,会创建几个对象?
首先明确String s = "abc",这种直接使用双引号声明出来字面量字符串肯定是会直接存储在常量池中。
执行String s = "abc"语句的具体情况是:
- 如果字符串常量池中不存在"abc"对象,那么会在字符串常量池创建一个"abc"对象
- 如果在字符串常量池中已经有"abc"对象,那么直接将s指向字符串常量池中该String对象,也就是创建0个对象;
代码实战:
1.
代码语言:javascript复制 public static void main(String[] args) {
String s = "abc";
}

2.
代码语言:javascript复制 public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
System.out.println(s1==s2);
}
代码语言:javascript复制输出结果:true
JVM是如何执行String s = new String("abc")的,会创建几个对象?
首先明确如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
先让我们看一下,当执行String s = new String("abc")时,字节码指令:
代码语言:javascript复制 public static void main(String[] args) {
String s = new String("abc");
}

与上面String s = "abc"的字节码指令相比,增加了对象的创建和初始化,而且我们还可以得出一条String s = new String("abc"),其实就相当于一条String s = new String(String temp = "abc");
所以执行String s = new String("abc")的流程就是:
- 先执行String temp = "abc";其流程与上文一致,可以创建0或1个对象
- 再在堆区创建一个String对象指向常量池中该"abc"对象
代码实战:
代码语言:javascript复制 public static void main(String[] args) {
String s1 = new String("abc");
String s2 = "abc";
System.out.println(s1==s2);
}
代码语言:javascript复制输出结果:false
JVM是如何执行String s = "1" "1"的,会创建几个对象?
话不多说,直接上代码:
代码语言:javascript复制 public static void main(String[] args) {
String s1 = "1" "1";
}

可以看到在字节码指令中,"1" "1"操作将会被优化为"11",所以该操作相当于String s = "11",创建0或1个对象。
JVM是如何执行String s = new String("1") new String("1")的,会创建几个对象?
代码语言:javascript复制 public static void main(String[] args) {
String s = new String("1") new String("1");
}

从其字节码指令我们可以看出,先进行两个new String("1")操作,再利用StringBuilder的append方法进行拼接。但是我们并没有在其中发现"11"的身影,也就是说"11"并没有进字符串常量池,拼接而成的"11"只是个堆上的对象。问题可能就是出在这个StringBulider身上了,让我们看看StringBuilder的toString方法:
代码语言:javascript复制 @Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
注意这里的new String()的参数是value,在StringBuilder中指代的是char[]数组。
所以String s = new String("1") new String("1")会创建2(1) 1 1 1=5(4)个对象。


