写在前面
对于String s1 = new String("askcto"),这行代码到底会不会在常量池中创建字符串实例,这个是一个值得考虑和动手去证明的一个问题。网上对于这个问题也是众说纷纭。首先说明一点,使用的JDK版本是1.7。那么就一起开启我们的探索之旅。顺便提一句,JDK1.7,常量池已经移动到堆内存中了,以前是在方法区里面(也即永久代,永久代是用来实现方法区的,JDK1.8,永久代就被废弃了,使用了元空间来代替了)
分析过程
对于String s1 = new String("askcto"),肯定是会在堆中创建一个对象的。因为有new关键字,当虚拟机遇到一条new的指令时,首先会检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个类的符号引用代表的类是否已被加载、解析、初始化过。如果没有的话,就必须执行类的加载过程了。
但是今天我们的关注点,不在于是否在堆中创建了对象,而且关注是否在常量池中也创建了一个新的实例。换个说法,也就是new String("askcto"),在运行时创建了几个String实例?
先说一下答案:两个,一个是字符串字面量"askcto"所对应的、驻留(intern)在一个全局共享的字符串常量池中的实例,另一个是通过new String(String)创建并初始化的、内容与"askcto"相同的实例。
不过我们还是要动手去验证一下,不能你说两个就是两个。看一下下面的代码
public class UserInfoClient {
public static void main(String[] args) {
1.String str = new String("askcto");
2.String strIntern = str.intern();
3.System.out.println(strIntern == str);
}
}
这行代码运行的结果是false,str指向的肯定是堆中生成的字符串“askcto”的实例地址,str.intern()返回的是常量池中字符串“askcto”的实例地址。他们现在肯定不是指向同一个实例的,要不他们就是会返回true的比较结果了。目前的内存中的情况如下图:
但是这个时候,你也没法没法判断字符串常量池中的askcto到底是new String("askcto")产生的,还是str.intern()产生的呢,因为在执行str.intern()后,它的作用就是先去常量池中寻找使用equals比较有没有相同的字符串,如果没有,就将该字符串的加入到常量池中,然后返回常量池中的引用。
不过,好就好在,我们这里用的是JDK1.7,这个时候,常量池已经在堆中了,不像JDK1.6那样,常量池是在永久代中。也就是在JDK1.6,调用str.intern()方法后,它会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,然后返回永久代里面的字符串实例的引用。但是JDK1.7,你在调用str.intern()方法,它就不需要在拷贝字符串实例到永久代的常量池中了,因为常量池已经移动到堆里面了。它只需要在堆中的常量池记录一下首次出现该字符串的实例引用就可以了,这样也节省了内存空间。
我们先假设,假设上面第一行new String("askcto"),只是在堆中创建了一个对象,并没有在常量池中创建该实例对象或者让其指向堆中实例对象的引用。也即是常量池一点变化也没有,在执行第一行代码的时候。有的只是堆中生成了一个实例对象。当执行第二行代码str.intern()的时候,它发现这个时候的常量池并没有"askcto",然后它就将堆中的这个实例的引用保存在了常量池。那这样的话strIntern与str其实就是指向同一个堆中的对象了。那比较 == 的结果应该是相等才是。显然现实不是我们假设的那样。假设就不成立了。
再继续假设,上面第一行new String("askcto"),先在堆中创建了一个对象,然后将堆中的这个实例引用指向了常量池保存下来了。当执行第二行代码str.intern()的时候,它发现这个时候的常量池已经存在了"askcto",就直接返回常量池中的引用。而该引用也是指向堆中的"askcto"引用的。也就是strIntern与str还是指向同一个堆中的对象了。显然这个假设还是不成立的。
进行第三次假设,上面第一行new String("askcto"),先在堆中创建了一个对象,然后在常量池也创建了一个实例"askcto",这次假设好像越来越接近真像了。当执行第二行代码str.intern()的时候,它发现这个时候的常量池已经存在了"askcto",就直接返回常量池实例的引用。注意这次,常量池中实例和堆中的实例不到同一个实例奥,当你继续执行第三行代码,strIntern == str的时候,就返回了false。这次和结果也吻合住了。不过还是要继续验证。
继续验证
我们通过IDEA的Memory继续验证生成的实例。在new String("askcto")处打上断点。查看这个时候内存中的String实例对象个数是2119。
向下运行一步,也即执行完编号1代码,new String("askcto"),再次查看内存中的String实例对象个数是2121,比之前增加了2个实例。
双击java.lang.String,过滤查找字符串"askcto",可以看到2个字符串实例。一个是堆中生成的,一个是常量池中生成的。至于这2个字符串的value值是一样解释,常量池中是复制过去的。也是一个新的实例。
也可以直接让代码执行过编号2,str.intern(),然后查询内存的实例变化。查看内存中的String实例对象个数还是2121,比之前增加了2个实例。从这里也验证,执行str.intern()前,常量池中是已经存在了字符串"askcto"的。
最后总结
new String("askcto"),是会在内存中创建2个字符串实例的(一个在堆中,一个在字符串常量池中)。回到我们的题目,也即会在字符串常量池中创建一个新的字符串实例。如果当你看到有同事通过这样的形式去定义字符串,可以告诉他一下,这样会浪费内存空间的。