由于最近在备战实习offer和一些不可抗拒因素,写文章的时间就大大减少了,不过这只是暂时的。
以前刷面经的时候看到过这样的一个问题:
String s = new String("abc)创建了几个对象
先给出答案:1或2个
然后今天偶然在一个公众号的推文上,看到了一篇讲述该流程的文章,我就是觉得别扭,查了好多网上的文章还是感觉不太清晰,我的问题主要有两点:
本次就让我们由浅入深,来认识一下我们这个熟悉的陌生朋友,"String"!
了解过JVM运行时数据区(简称JVM)的同学肯定都知道,在我们的JVM中有一块叫做字符串常量池的内存,在JDK6中它属于我们的方法区,从JDK7开始将它移动到堆中,那么字符串常量池是干嘛的呢?
因为字符串是一个常用对象,频繁的创建会严重影响性能,而字符常量池就好比一个缓冲区,用来存放我们的字符串对象,做全局共享。
上面应该是我们在普遍得到的答案,但是我们知道在JDK7开始我们的字符串常量池已经在堆中了,没必要每个String对象都存放在字符串常量池中了,也可以只存放该字符串对象的引用。
在JDK 6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用. 而JDK 7(以及部分其他虚拟机,例如JRockit)的intern()方法实现就不需要再拷贝字符串的实例 到永久代了,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可. ---深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)p63
实战分析,字符串常量池中可以存放对象引用:
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的。
首先明确String s = "abc",这种直接使用双引号声明出来字面量字符串肯定是会直接存储在常量池中。
执行String s = "abc"语句的具体情况是:
代码实战:
1.
public static void main(String[] args) {
String s = "abc";
}
2.
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
System.out.println(s1==s2);
}
输出结果:true
首先明确如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
先让我们看一下,当执行String s = new String("abc")时,字节码指令:
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")的流程就是:
代码实战:
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = "abc";
System.out.println(s1==s2);
}
输出结果:false
话不多说,直接上代码:
public static void main(String[] args) {
String s1 = "1"+"1";
}
可以看到在字节码指令中,"1"+"1"操作将会被优化为"11",所以该操作相当于String s = "11",创建0或1个对象。
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方法:
@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)个对象。