【JVM内存结构】String在JVM中的深度剖析(四)

案例

OK,学习到这里,是不是感觉自己懂了?我出一道题目来考考大家,下面这段程序的运行结果是什么?

public static void main(String[] args) {  
    String a =new String(new char[]{'a','b','c'});  
    String b = a.intern();  
    System.out.println(a == b);  //true
    String x =new String("def");  
    String y = x.intern();  
    System.out.println(x == y);  //false
    
    //答案是:true false
}
第二个输出结果:

输出为false可以理解,因为new String(“def”)会做两件事:

  1. 在字符串常量池中创建一个字符串def。
  2. new关键字创建一个实例对象string,并指向字符串常量池def的引用。

而x.intern(),是从字符串常量池获取def的引用,他们的指向地址不同,我后面的内容还会详细解释。

第一个输出结果:

输出为true是为啥呢?

JDK文档中关于intern()方法的说明:

当调用intern方法时,如果常量池(内置在 JVM 中的)中已经包含相同的字符串,则返回池中的字符串。否则,将此String对象添加到池中,并返回对该String对象的引用。

  1. 在构建String a的时候,使用new String(new char[]{'a','b','c'});,因为并不是字面量,初始化字符串时(不会自动调用intern()).
  2. 字符串采用懒加载方式进入到常量池,并没有在字符串常量池中构建abc这个字符串实例。
  3. 所以当调用a.intern()方法时,会把该String对象添加到字符常量池中,并返回对该String对象的引用,所以a和b指向的引用地址是同一个。

面试问题回答

面试题:String a = “ab”; String b = “a” + “b”; a == b 是否相等?

回答: a==b是相等的,原因如下:

  1. 变量a和b都是常量字符串,其中b这个变量,在编译时,由于不存在可变化的因素,所以编译器会直接把变量b赋值为ab(这个是属于编译器优化范畴,也就是编译之后,b会保存到Class常量池中的字面量)。

  2. 对于字符串常量,初始化a时, 会在字符串常量池中创建一个字符串ab并返回该字符串常量池的引用。

  3. 对于变量b,赋值ab时,首先从字符串常量池中查找是否存在相同的字符串,如果存在,则返回该字符串引用。

  4. 因此,a和b所指向的引用是同一个,所以a==b成立。

问题总结

关于常量池部分的内容,要比较深入和全面的理解,还是需要花一些时间的。
比如大家通过阅读上面的内容,认为对字符串常量池有一个非常深入的理解,可以,我们再来看一个问题:

public static void main(String[] args) {  
    String str = new String("Hello World");  
    String str1=str.intern();  
    System.out.print(str == str1);
}

上面这段代码,很显然返回false,原因如下图所示。很明显str和str1所指向的引用地址不是同一个。
在这里插入图片描述

但是我们把上述代码改造一下:

public static void main(String[] args) {  
    String str = new String("Hello World")+new String("!");  
    String str1=str.intern();  
    System.out.print(str == str1);//true
}

上述程序输出的结果变成了:true。 为什么呢?
这里也是JVM编译器层面做的优化,因为String是不可变类型,所以理论上来说,上述程序的执行逻辑是:

  1. 通过 “+” 进行字符串拼接时,相当于把原有的String变量指向的字符串常量HelloWorld取出来,加上另外一个String变量指向的字符串常量!,再生成一个新的对象。

假设我们是通过for循环来对String变量进行拼接,那将会生成大量的对象,如果这些对象没有被及时回收,会造成非常大的内存浪费。

所以JVM优化之后,其实是通过StringBuilder来进行拼接,也就是只会产生一个对象实例StringBuilder,然后再通过append方法来拼接。

为了证明我说的情况,来看一下上述代码的字节码。

public static void main(java.lang.String[]);    
descriptor: ([Ljava/lang/String;)V    
flags: ACC_PUBLIC, ACC_STATIC    
Code:      stack=4, locals=3, args_size=1         
0: new           #3                  // class java/lang/StringBuilder         
3: dup         
4: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V         
7: new           #5                  // class java/lang/String        
10: dup        
11: ldc           #6                  // String Hello World        
13: invokespecial #7                  // Method java/lang/String."<init>":(Ljava/lang/String;)V        
16: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;        
19: new           #5                  // class java/lang/String        
22: dup        
23: ldc           #9                  // String !        
25: invokespecial #7                  // Method java/lang/String."<init>":(Ljava/lang/String;)V        
28: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;        
31: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;        
34: astore_1        
35: aload_1        
36: invokevirtual #11                 // Method java/lang/String.intern:()Ljava/lang/String;        
39: astore_2        
40: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;        
43: aload_1        
44: aload_2        
45: if_acmpne     52        
48: iconst_1        
49: goto          53        
52: iconst_0        
53: invokevirtual #13                 // Method java/io/PrintStream.print:(Z)V        
56: return

从字节码中可以看到,构建了一个StringBuilder,

0: new           #3                  // class java/lang/StringBuilder

然后把字符串常量通过append方法进行拼接,最后调用toString()方法得到一个字符串常量。

16: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

因此,上述代码,等价于下面这种形式。

public static void main(String[] args) {  
    StringBuilder sb=new StringBuilder().append(new String("Hello World")).append(new String("!"));  
    String str=sb.toString();  
    String str1=str.intern();  
    System.out.print(str == str1); //true
}

所以,得到的结果是true。

基于这个问题的变体还有很多,比如再来变一次,下面这段程序的运行结果是多少?

public static void main(String[] args) {  
    String s1 = "a";  
    String s2 = "b";  
    String s3 = "ab";  
    String s4 = s1 + s2;  
    System.out.println(s3 == s4);//false
}

答案是false。

public static void main(String[] args) {  
    String s1 = "a";  
    String s2 = "b";  
    String s3 = "ab";  
    StringBuilder sb=new StringBuilder().append(s1).append(s2);  
    String s4 = sb.toString();
    // sb ==> new String("ab");
    
    System.out.println(s3 == s4);//false
}

下面这几题必看,分析过程。

public static void test1() {
        String s1 = "a";
        String s2 = "b";

        // 串池 ["a","b"]
        // 堆 new String("a") new String("b") new String("ab")
        String s4 = new String(s1) + new String(s2);
        

        // 串池 ["a","b","ab"]
        String s5 = s4.intern();
        // 尝试将s4放入串池,因为串池没有,所以将s4对象放入串池,然后返回串池对象

        // 所以s4 == "ab" ,s5 == "ab"
        System.out.println(s4 == "ab");// true
        System.out.println(s5 == "ab");// true
    }
public static void name1() {
        String s1 = "a";
        String s2 = "b";

        String s3 = "ab";
        // 串池 ["ab","a","b"]
        // 堆 new String("a") new String("b") new String("ab")
        String s4 = new String(s1) + new String(s2);

        // 串池 ["a","b","ab"]
        String s5 = s4.intern();
        // 尝试将s4放入串池,但是串池中已经有了,但是还是会返回串池对象。

        // 因为s4并没有放入串池对象,所以s4和s3肯定不相同。
        System.out.println(s4 == s3);// false
        // 因为s5就是串池返回的对象,所以s5==s3
        System.out.println(s5 == s3);// true
    }
public static void name3() {
    String a = new String(new char[] { 'a', 'b', 'c' });
   // 在构建String a的时候,使用new String(new char[]{'a','b','c'});
   // 初始化字符串时(不会自动调用intern()).

   String b = a.intern();
   // 所以当这里调用intern时候,就会将对象a放入串池,所以 a==b;

   System.out.println(a == b); // true

}

上一篇:【JVM内存结构】String在JVM中的深度剖析(三)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值