Java中String创建过程浅揭秘

2,617 阅读7分钟

一、直接创建

1、图解创建过程

直接创建String过程图示.png

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,只是可不扩容,存储的是字符串的地址

直接创建String过程图示和intern函数.png

补充

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

思想抽象化之常量池技术

编译器加载和运行期加载

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

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

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

参考资料:

  1. [java两个字符串对象相加和字符串字面量比较?]  www.zhihu.com/question/26… 
  2. [看完这篇JVM,阿里面试官都不怕!看完就能拿offer]  blog.csdn.net/Java_3y/art… 
  3. [String创建方式内存终极分析]  blog.csdn.net/kevindai007… 
  4. [The Java® Virtual Machine Specification]  docs.oracle.com/javase/spec…