什么时候字面量会主动加入到stringtable&&new String(“xx“)到底创建了几个对象实例

先来几道题,看看大家是否都能答对

代码块1

String s1 = new String("小爽帅到拖网速");
String intern = s1.intern();
String s2 = "小爽帅到拖网速";
System.out.println(intern == s1);  // false
System.out.println(intern==s2);  // true

如果上面的题你都能答对,那下面的题呢

代码块2

String a = "aa";
String b = "bb";
String ab = a + b;
String intern1 = ab.intern();
String c = "aabb";
System.out.println(intern1 == ab); // true
System.out.println(intern1 == c);  // true

为什么ab都能通过intern()方法主动放入StringTable中,而s1却不能呢?下面我将通过字节码的角度为大家分析

首先我们先看一下代码块1的字节码
  1. 首先在main方法中执行到String s1 = new String(“小爽帅到拖网速”);对应的字节码如下

    0: new           #2                  // class java/lang/String   堆内存中分配字符串内存
    3: dup 															 // 复制一份引用保存在操作数栈中
    4: ldc           #3                  // String 小爽帅到拖网速  去运行时常量池中查找对应的值
    6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V 调用构造方法
    9: astore_1													 // 把刚才dup复制指向堆内存的引用存入局部变量表中
    

    这里的ldc指的是去常量池中查找对应的值,而常量池的值是字面值,只有字符串或者大于215的数值会被当做字面值放入常量池中,小于215会使用jvm指令i_const、bipush、sipush进行加载;

    JVM规范里Class文件的常量池项的类型,有两种东西:

    CONSTANT_Utf8_info    // 维护两个字节大小的字节数组
    CONSTANT_String_info  // 简单了解一下,是String常量类型,但是并不直接持有String常量内容,而是有一个index指向另一个常量池为CONSTANT_Utf_info类型的常量,它才是真正保存字符串的内容
    

    使用ldc加载字面量,会涉及的一个懒加载的操作,jvm走ldc命令时会去判断该常量是否已经resolve(没有被resolve会打上JVM_CONSTANT_UnresolvedString标记),在resolve过程中如果发现StringTable已经有了内容匹配的String引用,则直接返回引用,如果没有,则会在堆中创建该对象,并且该对象引用放在StringTable中,并返回引用

    这里我们注意一个问题,String s1 = new String(“小爽帅到拖网速”);到底创建了几个对象

    答案是一个或者两个都有可能,这是为什么呢?

    在加载类的时候,字符串的字面量会进入到当前类的运行时常量池中,不会直接进入全局的字符串常量池(StringTable中并没有这个引用,在堆内也没有对应的对象产生),而java是懒加载的,等到运行到new String(“小爽帅到拖网速”);执行ldc指令才会去resolve,如果发现StringTable中已经有了匹配的内容,则只会通过new 在堆中创建新的实例;如果发现StringTable中没有匹配的对象,除了通过new在堆中创建新的对象,还会额外在堆里创建新的对象并且把引用放入StringTable,这就是两个对象了

    总之ldc是否创建对象,全看这个字面值是否能在StringTable中匹配到对应值,在接着判断是否创建对象实例,但是如果使用到了new就一定会在堆内创建新的对象

  2. s1.intern()

    intern()在jdk8中,调用者的值如果已经被放入StringTable中,则会返回在StringTable的引用;如果在StringTable匹配不到对应的值则会将调用者的引用存入StringTable,并返回该引用;

    在这里s1.intern()是放不进去的,应为在new String(“小爽帅到拖网速”);就已经创建了对象并把引用放入到了StringTable

  3. String s2 = “小爽帅到拖网速”;

    15: ldc           #3                  // String 小爽帅到拖网速
     7: astore_3
    

    同样使用ldc,同样会去resolve,并且在StringTable 中匹配到值,会把StringTable中的引用返回,不会创建新的对象

  4. System.out.println(intern == s1); // false

    String intern = s1.intern(); s1并没有把该引用放入到StringTable中,intern()返回的是new String(“小爽帅到拖网速”); 执行的ldc命令所创建的对象的引用,即intern 跟 s1 都是指向堆内不同的对象

  5. System.out.println(intern==s2); // true

    创建s2的执行的ldc返回的是StringTable中对应字面量的引用,所以这里自然是true

看完代码块1之后我们再来看看代码块2
  1. String a = “aa”;
    String b = “bb”;

​ 对应的字节码如下

0: ldc           #2                  // String aa
2: astore_1
3: ldc           #3                  // String bb
5: astore_2

两次ldc都在StringTable没有匹配到对应的字面量,所以都会在堆内创建对象,并且把引用放入StringTable中,并且通过astore_x 字节码指令存入到main方法对应栈帧的局部变量的对应槽位

  1. String ab = a + b;

在通过javac编译成.class文件时,会做编译时的优化成(new StringBuilder()).append(a).append(b).toString();

查看对应的字节码

 6: new           #4                  // class java/lang/StringBuilder
 9: dup
10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
13: aload_1   // 对应astore_1
14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17:astore_1   // 对应astore_2
18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3

通过字节码我们发现,如果直接将a1+a2相加, 会使用StringBuilder的append方法进行追加,最后使用toString()方法,本质也是使用了new String()的方式,但是拼接后的字面量并不是一开始就存在常量池当中的,自然不会走ldc指令,也就没有进入StringTable的机会了,只会在堆里边创建了一个对象
注意:创建什么样的字符串实例才会去走ldc指令,我们发现有些字符串创建会走ldc,而有些不会走ldc,这里涉及到类加载的一些知识

  • 编译后的.class文件被类加载器加载到jvm时,会在方法区的元空间构建instanceKlass,并且把常量池的符号引用转存到运行时常量池,在类加载的第二阶段链接中的验证、准备阶段后,根据该符号引用对应的字面值在堆中生成驻留字符串的实例对象(java是懒加载的,只有运行到这行代码时才会执行该操作)然后将这个对象的引用存到StringTable中,最后在链接的第三个阶段解析,把运行时常量池中的符号引用替换成指向堆内存的直接引用(这个直接引用指向了堆内存中的实例对象,其中实例对象的对象头保存了java_mirror.class对象,该对象指向了方法区元空间中的instanceKlass,在调用这个实例的方法时都需要去instanceKlass中查方法表拿到对应的字节码地址去调用执行对应的方法),保证StringTable里的引用与运行时常量池中的引用值一致,整个过程大概就是这样
  • 走ldc的都是在.class中已经留有的字面量,其中只会有字符串和大于2^15的数值(int,float,double),可能这么说有点牵强,我觉得还可以理解为走ldc是保留在常量池的静态字面量,在编译阶段就可以确定的,而String ab = a+b,包括一些字符串的操作都是运行时动态确定的,所以不会被常量池收留
  1. String intern1 = ab.intern();

    执行String ab = a + b; 并没有把“aabb”放入到StringTable中,所以主动调用intern()方法是能够把ab引用放入到StringTable中

  2. String c = “aabb”;

    31: ldc           #9                  // String aabb
    33: astore        5
    
    LocalVariableTable:
    Start  Length  Slot  Name   Signature
    35      36     5     c   Ljava/lang/String;
    

    观察字节码可以发现执行的还是ldc命令,去resolve,并且在StringTable 中匹配到值,会把StringTable中的引用返回,不会创建新的对象

  3. System.out.println(intern1 == ab); // true
    System.out.println(intern1 == c); // true

    从上面可知,intern1的引用是ab调用intern()主动放进StringTable中的,所以intern1==ab为true,而c是从StringTable拿到的自然也是true

看完以上的分析,相信你已经对字符串对象进入StringTable的实际、堆中创建对象的个数有了一定的了解,那么下面这道题再试试看

String a = "aa";
String b = "bb";
String c = "aa"+"bb";
String d = new String("aabb");
String intern = d.intern();
System.out.println(intern==c); // true
System.out.println(intern==d); // false

这次不会再逐句分析,直接贴出字节码

  • 变量c在编译阶段就把字面量放入常量池,使用ldc发现StringTable中没有,就在堆内创建后把引用放入了StringTable,有没有小伙伴好奇为什么在编译阶段aabb会被放入常量池中,这是java编译时的优化,可以看看反编译后的字节码

    public class TestInstance {
      public TestInstance() {
      }
    
      public static void main(String[] args) {
        String a = "aa";
        String b = "bb";
        String c = "aabb";
        String d = new String("aabb");
        String intern = d.intern();
        System.out.println(intern == c);
        System.out.println(intern == d);
      }
    }
    
  • 创建变量d的时候按照我们之前的流程,StringTable中匹配到对应字面量,不会在将新创建的引用放入了StringTable,且主动调用intern()也是不行的,只会返回c 在ldc时放入StringTable中的引用

Constant pool:
   #1 = Methodref          #11.#35        // java/lang/Object."<init>":()V
   #2 = String             #36            // aa
   #3 = String             #37            // bb
   #4 = String             #38            // aabb
 
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=6, args_size=1
         0: ldc           #2                  // String aa
         2: astore_1
         3: ldc           #3                  // String bb
         5: astore_2
         6: ldc           #4                  // String aabb
         8: astore_3
         9: new           #5                  // class java/lang/String
        12: dup
        13: ldc           #4                  // String aabb
        15: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        18: astore        4
        20: aload         4
        22: invokevirtual #7                  // Method java/lang/String.intern:()Ljava/lang/String;
        25: astore        5
        27: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        30: aload         5
        32: aload_3
        33: if_acmpne     40
        36: iconst_1
        37: goto          41
        40: iconst_0
        41: invokevirtual #9                  // Method java/io/PrintStream.println:(Z)V
        44: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        47: aload         5
        49: aload         4
        51: if_acmpne     58
        54: iconst_1
        55: goto          59
        58: iconst_0
        59: invokevirtual #9                  // Method java/io/PrintStream.println:(Z)V
        62: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      63     0  args   [Ljava/lang/String;
            3      60     1     a   Ljava/lang/String;
            6      57     2     b   Ljava/lang/String;
            9      54     3     c   Ljava/lang/String;
           20      43     4     d   Ljava/lang/String;
           27      36     5 intern   Ljava/lang/String;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值