Java中String创建过程浅揭秘

新旧博客已经迁移、维护在掘金平台,点这里 访问我的掘金博客主页

一、直接创建

1、图解创建过程

在这里插入图片描述

2、代码和字节码实战
2.1、常量池中创建字符串常量

执行过程:

​ 直接创建字符串,压栈到字符串常量池,然后将字符串引用保存到本地变量池。

// Java代码
public class Test {
    public static void main(String[] args) {
        String a = "ab";
    }
}
// 反编译出来的字节码文件
public class Test {
  // 此处省去类的加载和调用main方法过程
  public static void main(java.lang.String[]);
    Code:
       // 直接创建字符串对象,压栈到字符串常量池
       0: ldc           #2                  // String ab
       2: astore_1
       3: return
}
2.2、堆中创建字符串对象

执行过程:

  1. 在堆中创建String对象;
  2. 常量池中声明字符串”ab“;
  3. 根据"ab"值调用String初始化方法初始化String对象
// Java代码
public class Test {
    public static void main(String[] args) {
        String a = new String("ab");
    }
}
// 反编译出来的字节码文件
public class Test {
  // 此处省去类的加载和调用main方法过程
  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/String
       3: dup
       4: ldc           #3                  // String ab
       6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
       9: astore_1
      10: return
}

二、组合创建

在字符串拼接过程中,只要出现变量或者堆中字符串对象的拼接,会先行创建StringBuilder对象,使用该对象完成一个个字符串的拼接过程。

场景一:常量池中(字符串对象 + 字符串对象)

执行过程:

  1. 执行"a" + “b"的过程和创建"a”、"b"的过程一样,直接在字符串常量池中创建。
  2. 执行ab创建过程中,因为需要用到"a"、“b”,而此时字符串常量池中已经存在需要用到的"a"、“b”,不再重复创建,直接完成字符串的拼接,在常量池中创建字符串"ab",将地址赋值给字符串对象引用ab。
// Java代码
public class Test {
    public static void main(String[] args) {
        String a = "a";
        String b = "b";
        String ab = "a" + "b";
    }
}
// 反编译出来的字节码文件
public class Test {
  // 此处省去类的加载和调用main方法过程
  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String a
       2: astore_1
       3: ldc           #3                  // String b
       5: astore_2
       // 像上面字符串a和b的创建方式一样,直接在常量池中创建
       6: ldc           #4                  // String ab
       8: astore_3
       9: return
}
场景二:常量池中(字符串对象引用 + 字符串对象引用)

执行过程:

  1. 字符串 a 和 b 的创建过程并不让人意外,直接在字符串常量池中声明。
  2. 在执行String ab = a + b;的时候做了什么呢?创建了StringBuilder对象,然后调用初始化方法初始化对象。
  3. 执行aload_1指令从本地变量中加载序号为1的变量也就是 字符串a的引用。
  4. 执行invokespecial指令调用StringBuilder内部append方法完成对字符串a的拼接。
  5. 重复3-4过程完成字符串b的拼接。
  6. 执行invokevirtual指令调用StringBuilder内部方法toString创建一个新的String对象。
  7. 将新String对象引用ab压栈。
// Java代码
public class Test {
    public static void main(String[] args) {
        String a = "a";
        String b = "b";
        String ab = a + b;
    }
}
// 反编译出来的字节码文件
public class Test {
  // 此处省去类的加载和调用main方法过程
  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String a
       2: astore_1
       3: ldc           #3                  // String b
       5: astore_2
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V

      13: aload_1
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      17: aload_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
      25: return
}
场景三:常量池中字符串对象 + final常量

执行过程:

  1. 声明字符串"a"到常量池。
  2. 添加了final修饰词,为什么不再有字符串"b"的声明过程了呢?TODO 疑问待解决。
  3. 对于字符串ab直接在常量池中声明"ab"。
// Java代码
public class Test {
    public static void main(String[] args) {
        String a = "a";
        final String b = "b";
        String ab = "a" + b;
    }
}
// 反编译出来的字节码文件
public class Test {
  public Test();
  // 此处省去类的加载和调用main方法过程
  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String a
       2: astore_1
       3: ldc           #3                  // String ab
       5: astore_3
       6: return
}
场景四:堆中对象 + 常量池中字符串对象引用

执行过程:

  1. 字符串常量池中创建字符串"b",然后保存字符串引用到本地变量。
  2. 创建StringBuilder对象,调用StringBuilder初始化方法。
  3. 在堆中创建String对象,在字符串常量池中声明字符串"a",然后初始化刚刚创建的String对象。
  4. 调用Stringbuilder对象的append方法拼接所创建的String对象的值。
  5. 再次调用StringBuilder对象的append方法拼接字符串b指向的"b"。
  6. 调用内部方法toString()生成String对象,保存该对象引用到本地变量池。
// Java代码
public class Test {
    public static void main(String[] args) {
        String b = "b";
        String ab = new String("a") + b;
    }
}
// 反编译出来的字节码文件
public class Test {
  // 此处省去类的加载和调用main方法过程
  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String b
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V

      10: new           #5                  // class java/lang/String
      13: dup
      14: ldc           #6                  // String a
      16: invokespecial #7                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      19: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: aload_1
      23: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      26: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      29: astore_2
      30: return
}

三、捣乱的intern()函数

先看下面程序,如果一点不含糊地完全答对了,从珍爱生命的角度出发,就不用再继续看了。

闯关第一层
    /**
     * 直接声明字符串对象,调用intern()
     */
    @Test
    public void test1() {
        String s1 = new String("hello world");
        String s2 = s1.intern();
        String s3 = "hello world";

        System.out.println(s1 == s2);// false
        System.out.println(s3 == s2);// true
    }
闯关第二层
	/**
     * 拿此例子和不调用intern方法相比较
     */
    @Test
    public void test2() {
        String s1 = new String("12") + new String("3");
        String s2 = s1.intern();
        String s3 = "123";

        System.out.println(s1 == s2);// true
        System.out.println(s2 == s3);// true
        System.out.println(s1 == s3);// true
    }
    public void test2() {
        String s1 = new String("12") + new String("3");
        String s3 = "123";

        System.out.println(s1 == s3);// false
    }
闯关第三层
    @Test
    public void test3() {
        String s1 = new String("12") + new String("3");
        String s2 = s1.intern();
        String s3 = new String("12") + new String("3");
        String s4 = s3.intern();
        String s5 = "123";

        System.out.println(s1 == s2);// true
        System.out.println(s2 == s4);// true
        System.out.println(s5 == s3);// false
    }
原理揭密

看图之前首先简单了解hotspot中两个数据结构:

Constant Pool:类似一个数组,存储常量字符串实体内容

String Table:底层结构类似HashTable,只是可不扩容,存储的是字符串的地址

在这里插入图片描述

补充
  1. 在字符串常量池中声明字符串后,我们可以认为hotspot就会默认调用intern()方法将地址保存在String Table中,实际上不是这样的,但是这个过程过于复杂,牵扯到加载和解析的细分,这里这么粗略的理解就可以。

思想抽象化之常量池技术

关于编译器加载和运行期加载

​ 在将源代码编译为字节码的过程中,已经能确定的字符串资源存储在常量池中;而在编译器才能确认的字符串资源等等统统存放在堆中,不同类型的资源存放在不同的位置,就像现在住房里,厨房和书房、客厅、卧室的设计一般。

关于Constant Pool和String Table底层结构简单分析

​ Constant Pool中存放的是实际内容,String Table中存放的是引用,而这两个数据结构需要满足的要求之一其实都是快速查找,其中String Table更要满足快速查找功能,所以将地址引用设计成了散列码对应数据的结构,我们可以取出地址值,达到以最小的内存资源开销成本实现快速查找Constant Pool中资源的效果。当然这里还有很多疑问,比如如何设计String Table可以满足很久内容值快速查找定位,我们把堆中对象调用intern()的地址存放在String Table中做什么毕竟堆中的对象会被回收掉的,如果堆中对象被回收掉了,那么String Table中存储的地址是否也会被清除呢?这里面涉及的问题就更多了,你知道的越多你不知道的越多。。。

参考资料:

  1. https://www.zhihu.com/question/267818864?sort=created
  2. https://blog.csdn.net/Java_3y/article/details/104375866
  3. https://blog.csdn.net/kevindai007/article/details/56046570
  4. https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值