JVM之StringTable

题目

    public static void main(String[] args) {
        String str1 = "a";
        String str2 = "b";
        String str3 = "ab";
        String str4 = "a" + "b";
        String str5 = new String("ab");
        String str6 = str5.intern();
        String str7 = str1 + str2;
        String str8 = str1 + "b";
        String str9 = new String("a") + new String("b");

        System.out.println("str3 == str4 :" + (str3 == str4));
        System.out.println("str3 == str5 :" + (str3 == str5));
        System.out.println("str3 == str6 :" + (str3 == str6));
        System.out.println("str3 == str7 :" + (str3 == str7));
        System.out.println("str3 == str8 :" + (str3 == str8));
        System.out.println("str3 == str9 :" + (str3 == str9));
    }

背景知识

要想解决以上的问题,需要一点JVM的知识,我们来看几个概念

1.常量池

java文件经过编译器编译后,生成class文件。class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用来存放各种字面量和符号引用。可以使用javap -v命令反编译class文件,查看class文件中的常量池。下面只是截取了反编译结果的一部分,可以看到有一个Constant pool,就是常量池。

D:\workspace\target\classes\org\example\jvm\stringtable>javap -v StringTableTest.class
Classfile /D:/workspace/target/classes/org/example/jvm/stringtable/StringTableTest.class
  Last modified 2021-3-6; size 1583 bytes
  MD5 checksum dcee7d9fa800d5e955616c75868e56b6
  Compiled from "StringTableTest.java"
public class org.example.jvm.stringtable.StringTableTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #15.#43        // java/lang/Object."<init>":()V
   #2 = String             #44            // a
   #3 = String             #45            // b
   #4 = String             #46            // ab
   #5 = Class              #47            // java/lang/String
   #6 = Methodref          #5.#48         // java/lang/String."<init>":(Ljava/lang/String;)V
   #7 = Methodref          #5.#49         // java/lang/String.intern:()Ljava/lang/String;

2.方法区

 方法区,是各个线程共享的一块内存区域,用于存储已经被虚拟机加载的类信息、常量、静态变量等。所谓的方法区,是一个逻辑区域,不同的虚拟机厂商有不同的实现。以HotSpot为例,jdk1.6中方法区的实现是永久代,它位于jvm内存中;jdk1.8中方法区的实现是元空间(meta space),他本身不属于jvm的内存。

3.运行时常量池

运行时常量池是方法区的一部分,class中的常量池在类加载后会存放至运行时常量池。

4.StringTable(串池)

jvm中用于存储字符串对象的一个容器,底层采用HashTable的数据结构实现。jdk1.8及以后,StringTable位于堆空间中,主要是堆中的垃圾回收比方法区较频繁,可以将不使用的字符串及时清理。那么什么样的字符串才会放进这个容器呢?总的来说有两种,第一种是运行时常量池中的字符串;第二种是String对象主动调用了intern()方法,将自己放进串池中。串池的存在,可以减少创建重复的字符串,节省内存空间。

这里需要说明的一点是,运行时常量池中的字符串,在被程序使用之前,仅仅是一个符号,并不是一个可以直接使用的String对象。当程序使用常量池的字符串时,会首先去串池中找是否存在该对象,如果存在,直接返回串池中的对象;如果不存在,创建一个字符串对象,将其放入串池,然后返回串池中的对象。

intern()方法,对于不同版本的jdk,其实现略微有所差异。jdk1.6及以前,调用该方法将字符串放进串池,如果串池没有值相同的字符串对象,则将原字符串复制一份,放进串池,并返回串池中的对象;如果有值相同的字符串对象,直接返回串池中的对象。jdk1.8略有不同,调用该方法将字符串放进串池,如果串池没有值相同的字符串对象,则将原字符串对象放进串池,并返回串池中的对象;如果有值相同的字符串对象,直接返回串池中的对象。

解题

有了上面的基础知识,我们来解答一下开始的题目。

1. String str3 = "ab"

String str3 = "ab",编译以后会变成以下两条虚拟机指令。ldc #4,就是加载常量池#4位置的常量,也就是“ab”。astore_3,是将上一步得到的值存放至局部变量表的3号槽位。在程序运行时,会首先根据#4代表的真实地址,去运行时常量池找到对应的符号为“ab”。然后,以“ab”作为key,去串池中查找对应的字符串对象。显然不存在“ab”对象,这时会创建一个“ab”对象,放入串池中,最后返回这个对象。

         6: ldc           #4                  // String ab
         8: astore_3

2. String str4 = "a" + "b"

String str4 = "a" + "b",虽然源代码是"a" + "b",但是编译器会对这种两个常量相加的场景做优化,其结果就和str3完全一样。程序运行时,会首先根据#4代表的真实地址,去运行时常量池找到对应的符号为“ab”。然后,以“ab”作为key,去串池中查找对应的字符串对象,由于上一步已经在串池中存放了值为"ab"的对象,所以此时会直接返回串池中的"ab"对象。因此,str3 == str4。

        9: ldc           #4                  // String ab
        11: astore        4

3.String str5 = new String("ab")

String str5 = new String("ab"),根据编译后的虚拟机指令可以看出,这一行代码是在堆中创建了一个String对象,所以str5 != str3。

        13: new           #5                  // class java/lang/String
        16: dup
        17: ldc           #4                  // String ab
        19: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        22: astore        5

4.String str6 = str5.intern()

根据之前分析的intern()方法的特点,str5.intern()尝试将str5放入串池,此时发现串池已经包含了值为"ab"的对象,所以该方法的返回值为串池中的"ab"对象,所以str3 == str6。

5.String str7 = str1 + str2

String str7 = str1 + str2编译后的虚拟机指令如下,我们来逐步分析下。第一步,创建一个StingBuilder对象;第二步,调用StringBuilder的初始化方法;第三步,加载1号槽位的变量,也就是str1,并将str1作为参数,调用StringBuilder的append方法;第四步,加载2号槽位的变量str2,并将str2作为参数,调用StringBuilder的append方法;第五步,调用StringBuilder的toString方法;第六步,将上一步的结果,存入7号槽位。通过跟踪StringBuilder的toString方法,发现该方法,其实是在堆中创建了一个字符串对象。因此,str3 != str7。

        31: new           #8                  // class java/lang/StringBuilder
        34: dup
        35: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V
        38: aload_1
        39: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        42: aload_2
        43: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        46: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        49: astore        7

StringBuilder的toString方法

    @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

 6.String str8 = str1 + "b"

这种情况和上一种情况类似,唯一不同的是,"b"是直接从常量池读取的。因此,str3 != str8。

        51: new           #8                  // class java/lang/StringBuilder
        54: dup
        55: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V
        58: aload_1
        59: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        62: ldc           #3                  // String b
        64: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        67: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        70: astore        8

 7.String str9 = new String("a") + new String("b");

这种情况和上一种情况类似,不同的是,还在堆中创建了两个字符串对象。因此,str3 != str9。

        72: new           #8                  // class java/lang/StringBuilder
        75: dup
        76: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V
        79: new           #5                  // class java/lang/String
        82: dup
        83: ldc           #2                  // String a
        85: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        88: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        91: new           #5                  // class java/lang/String
        94: dup
        95: ldc           #3                  // String b
        97: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
       100: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
       103: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
       106: astore        9

分析完了,验证下结果(需要注意的是,这是在jdk1.8下运行的,其他的版本结果可能会稍有不同)

str3 == str4 :true
str3 == str5 :false
str3 == str6 :true
str3 == str7 :false
str3 == str8 :false
str3 == str9 :false

附加题

    public static void main(String[] args) {
        String str5 = new String("a") + new String("b");
        str5.intern();
        String str3 = "ab";

        System.out.println("str3 == str5 :" + (str3 == str5));
    }

 String str5 = new String("a") + new String("b")的执行流程如下:

  • 创建一个StingBuilder对象。

  • 调用StringBuilder的初始化方法。

  • 加载运行时常量池的字符串"a",由于此时运行常量池的"a"还只是一个符号,所以会首先创建一个"a"对象,放入串池中,然后返回。

  • 在堆中创建一个字符串对象,构造参数是"a"。

  • 将上一步创建的对象作为参数,调用StringBuilder的append方法

  • 加载运行时常量池的字符串"b",由于此时运行常量池的"b"还只是一个符号,所以会首先创建一个"b"对象,放入串池中,然后返回。

  • 在堆中创建一个字符串对象,构造参数是"b"。

  • 将上一步创建的对象作为参数,调用StringBuilder的append方法

  • 调用StringBuilder的toString方法,在堆中创建一个值为"ab"的字符串对象。

str5.intern()将上一步在堆中创建的"ab"对象放入了串池。str3会在运行时常量池找到符号"ab",然后根据符号"ab"去串池中查找对象,发现已经有了,就直接返回串池中的"ab"对象。因此,str3 == str5。

         0: new           #2                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         7: new           #4                  // class java/lang/String
        10: dup
        11: ldc           #5                  // String a
        13: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: new           #4                  // class java/lang/String
        22: dup
        23: ldc           #8                  // String b
        25: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        28: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        31: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: astore_1
        35: aload_1
        36: invokevirtual #10                 // Method java/lang/String.intern:()Ljava/lang/String;
        39: pop
        40: ldc           #11                 // String ab
        42: astore_2

 

总结

  1. 运行常量池中的字符串是延迟加载,只有在第一次访问的时候,才会创建一个字符串对象,并将这个对象放入常量池。
  2. 对于字符串相加的场景,编译器在编译时会做优化。如果时两个字符串常量直接相加,编译器会直接把相加的结果赋值给变量(String str = "a" + "b" ==> String str = "ab")。其他情况,都会创建一个StringBuilder对象,然后调用append和toString方法,最后在堆中创建一个字符串对象。
  3. intern()方法,调用该方法将字符串放进串池,如果串池没有值相同的字符串对象,则将原字符串对象放进串池,并返回串池中的对象;如果有值相同的字符串对象,直接返回串池中的对象。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值