理解字符串常量池(StringTable)

文章目录

  • 一、什么是StringTable?
  • 二、字符串的创建
    • 1. 字面量创建
    • 2. 拼接创建
  • 三、intern方法
  • 四、常量池的位置
  • 五、一些例子

一、什么是StringTable?

  • 字面量方式创建的字符串,会放入 StringTable 中,StringTable 管理的字符串,才具有不重复的特性。
  • 而 char[],byte[],int[],String,以及 + 方式本质上都是使用 new 来创建,它们都是在堆中创建新的字符串对象,不会考虑字符串重不重复,这种字符串的缺点就是如果存在大量值相同的字符串,对内存占用非常严重。

类似的Integer也存在这样的特性。

那么为什么以字面量的创建的字符串不会重复呢?如下:

String str= "abc";

jdk使用的是字符串常量池的方法来解决这个问题,其数据结构上就是一个 hash 表,字符串对象就充当 hash 表中的 key,key 的不重复性,是 hash 表的基本特性。

当代码运行到一个字面量 “abc” 时,会首先检查 StringTable 中有没有相同的 key,如果没有,创建新字符串对象加入;否则直接返回已有的字符串对象。

二、字符串的创建

1. 字面量创建

上面我们说过以字面量的方式来创建字符串,其实从jvm的角度,字面量在代码运行到它所在语句之前,它还不是字符串对象
当 java 代码被编译为 class 文件后,“abc” 存储于【类文件常量池】中。

Constant pool: // 常量池
   #1 = Methodref          #19.#41        // java/lang/Object."":()V
   #2 = String             #42            // abc
   ...

当 class 完成类加载之后,“abc” 这个字面量被存储于【运行时常量池】(归属于方法区)中,其中 #1 #2 都会被翻译为运行时真正的内存地址。

再看一下 class 中 main 方法的字节码:

public static void main(java.lang.String[]); // 字节码指令
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #2                  // String abc
         2: astore_1
         3: return
         ...

将来 main 方法被调用时,就会执行里面的字节码指令:

0: ldc           #2                  // String abc
2: astore_1
3: return

ldc #2 就是到运行时常量池中找到 #2 的内存地址,找到 “abc” 这个字面量,再根据它创建一个 String 对象。
理解字符串常量池(StringTable)_第1张图片

2. 拼接创建

例1

String s = "a" + "b";

例2

final String x = "b";
String s = "a" + x;

例3

String x = "b";
String s = "a" + x;

例4

String s = "a" + 1;

有同学会问,例1与例2与例3 不同吗?还别说,真就不同,其中例1 与例2 原理是一样的,例3 与例4 原理是一样的,反编译一下

例1

String s = "a" + "b";

常量池

直接拼接成 a b

Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."":()V
   #2 = String             #21            // ab
   ...

主方法

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #2                  // String ab
         2: astore_1
         3: return
         ...

可以看到,其实并没有真正的【拼接】操作发生,从源码编译为字节码时,javac 就已经把 “a” 和 “b” 串在一起了,这是一种编译期的优化处理

例2

final String x = "b";
String s = "a" + x;

常量池

Constant pool:
   #1 = Methodref          #5.#22         // java/lang/Object."":()V
   #2 = String             #23            // b
   #3 = String             #24            // ab
   ...

主方法

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: ldc           #2                  // String b   final b
         2: astore_1
         3: ldc           #3                  // String ab
         5: astore_2
         6: return
         ...

可以看到,还是没有真正的【拼接】操作发生,final 意味着 x 的值不可改变,因此其它引用 x 的地方都可以安全地被替换为 “b”,而不用担心 x 被改变,从源码编译为字节码时,javac 就也进行了优化,把所有出现 x 的地方都替换成为了 “b”

那么,什么是真正的【拼接】操作呢?看一下例3 反编译后的结果

String x = "b";
String s = "a" + x;

常量池

Constant pool:
   #1 = Methodref          #9.#26         // java/lang/Object."":()V
   #2 = String             #27            // b
   #3 = Class              #28            // java/lang/StringBuilder
   #4 = Methodref          #3.#26         // java/lang/StringBuilder."":()V
   #5 = String             #29            // a
   ...

可以看到常量池中并没有 ab 字面量

主方法

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // String b
         2: astore_1
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."":()V
        10: ldc           #5                  // String a
        12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: aload_1
        16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        22: astore_2
        23: return

翻译人能读懂的就是:

String x = "b";
String s = "a" + x;

String x = "b";
String s = new StringBuilder().append("a").append(x).toString();

StringBuilder 的 toString() 方法又是怎么实现的呢?

public final class StringBuilder 
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {
     
    
    // 从 AbstractStringBuilder 继承的属性,方便阅读加在此处
    char[] value;
    
    @Override
    public String toString() {
     
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }    
}

可以看到,本质上就是根据 StringBuilder 维护的 char[] 创建了新的 String 对象。

三、intern方法

String str = "abc";
str.intern();

如果常量池中已有:

x s StringTable intern() 如果已有 返回 StringTable 对象 x s StringTable

例子:

String x = new String(new char[]{
     'a', 'b', 'c'}); // 新创建
String y = "abc"; // 将 "abc" 加入 StringTable
String z = x.intern(); // 已有,返回 StringTable 中 "abc",即 y
System.out.println(z == y);//true
System.out.println(z == x);//false

如果常量池中没有,jdk1.7:

x s StringTable intern() 如果没有 将x引用的对象加入 返回 StringTable 对象 x s StringTable
String x = new String(new char[]{
     'a', 'b', 'c'});
String z = x.intern(); //  x 加入 StringTable,StringTable 中有了 "abc"
String y = "abc"; // 已有,不会产生新的对象,用的是 StringTable 中 "abc"
System.out.println(z == x);//true
System.out.println(z == y);//true

jdk1.6 :

x s StringTable intern() 如果没有 将x引用的对象复制 将复制后的对象加入 返回 StringTable 对象 x s StringTable

intern(去重的好处):减少内存占用

四、常量池的位置

在jdk版本的发展中,常量池在JVM的位置随版本有些变化。

jdk1.6:
理解字符串常量池(StringTable)_第2张图片

jdk1.8:
理解字符串常量池(StringTable)_第3张图片
如何证明

  • 1.6 不断将字符串用 intern 加入 StringTable,最后撑爆的是永久代内存,为了让错误快速出现,将永久代内存设置的小一些:-XX:MaxPermSize=10m,最终会出现 java.lang.OutOfMemoryError: PermGen space
  • 1.8 不断将字符串用 intern 加入 StringTable,最后撑爆的是堆内存,为了让错误快速出现,将堆内存设置的小一些:-Xmx10m -XX:-UseGCOverheadLimit 后一个虚拟机参数是避免 GC 频繁引起其他错误而不是我们期望的 java.lang.OutOfMemoryError: Java heap space
  public static void main(String[] args) throws InterruptedException {
     
        List<String> list = new ArrayList<String>();
        int i = 0;
        try {
     
            for (int j = 0; j < 260000; j++) {
     
                list.add(String.valueOf(j).intern());
                i++;
            }
        } catch (Throwable e) {
     
            e.printStackTrace();
        } finally {
     
            System.out.println(i);
        }
    }

存在字符串常量池内的字符串也可能被垃圾回收。

五、一些例子

1. 判断输出

String str1 = "string"; // 家
String str2 = new String("string"); // 野生
String str3 = str2.intern(); // 家

System.out.println(str1==str2);//#1  false
System.out.println(str1==str3);//#2  true

2. 判断输出

String baseStr = "baseStr";
final String baseFinalStr = "baseStr";

String str1 = "baseStr01"; // 家
String str2 = "baseStr"+"01"; // 家
String str3 = baseStr + "01"; // 野生
String str4 = baseFinalStr+"01";// 家
String str5 = new String("baseStr01").intern(); // 家

System.out.println(str1 == str2);//#3 true
System.out.println(str1 == str3);//#4 false 
System.out.println(str1 == str4);//#5 true
System.out.println(str1 == str5);//#6 true

3. 判断输出(注意版本)

String str2 = new String("str")+new String("01");
str2.intern(); //1.6
String str1 = "str01";
System.out.println(str2==str1);//#7 1.7 true, 1.6 false

4. 判断输出

String str1 = "str01";
String str2 = new String("str")+new String("01");
str2.intern();
System.out.println(str2 == str1);//#8 false

5. String s = new String(“xyz”),创建了几个String Object?

第一个对象是字符串常量"xyz" 第二个对象是new String(“xyz”)的时候产生的,
在堆中分配内存给这个对象,只不过这个对象的内容是指向字符串常量"xyz"
另外还有一个引用s,指向第二个对象。这是一个变量,在栈中分配内存。

6. 判断输出

String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2); // true

7. 判断输出

String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1 == s2); //false

8. 判断输出

String s1 = "abc";
String s2 = "a";
String s3 = "bc";
String s4 = s2 + s3;
System.out.println(s1 == s4); //false

9. 判断输出

String s1 = "abc";
final String s2 = "a";
final String s3 = "bc";
String s4 = s2 + s3;
System.out.println(s1 == s4);//true

10. 判断输出

String s = new String("abc"); // 野生
String s1 = "abc"; // 家
String s2 = new String("abc"); // 野生
System.out.println(s == s1.intern()); // false
System.out.println(s == s2.intern()); // false
System.out.println(s1 == s2.intern()); // true

参考:

String s=new String(“xyz”);创建了几个String Object?

你可能感兴趣的:(jvm,jvm,java,字符串)