JVM:字符串常量池

概述

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

java 复制代码
// 在堆中创建字符串对象"ab"
// 将字符串对象"ab"的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象"ab"的引用
String bb = "ab";
System.out.println(aa==bb);// true

HotSpot虚拟机中,字符串常量池是通过StringTable来实现的,可以理解为一个固定大小的HashTable,保存的是字符串(字面量?)(key)和 字符串对象的引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。

下面将通过详细的分析,说清楚字符串对象的创建过程。

字符串的创建方式

参考文章:流程图详解 new String("abc") 创建了几个字符串对象 个人感觉这个带图解会比较清晰。

new方式创建

String s1 = new String("abc") 会创建 1 或 2 个字符串对象。

1、如果字符串常量池中不存在字符串对象"abc"的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。

示例代码(JDK 1.8):

ini 复制代码
String s1 = new String("abc");

对应的字节码:

ldc 命令用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话直接返回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量池中(注意,是在堆中创建了两个对象,一个赋值给变量str1,另一个字符串对象的地址存在字符串常量池)。

代码执行结果如下

2、如果字符串常量池中已存在字符串对象"abc"的引用,则只会在堆中创建 1 个字符串对象"abc"。

示例代码(JDK 1.8):

java 复制代码
// 字符串常量池中已存在字符串对象"abc"的引用
String s1 = "abc";
// 下面这段代码只会在堆中创建 1 个字符串对象"abc"
String s2 = new String("abc");

对应的字节码:

这里就不对上面的字节码进行详细注释了,7 这个位置的 ldc 命令不会在堆中创建新的字符串对象"abc",这是因为 0 这个位置已经执行了一次 ldc 命令,已经在堆中创建过一次字符串对象"abc"了。7 这个位置执行 ldc 命令会直接返回字符串常量池中字符串对象"abc"对应的引用。

字面量创建

实际使用中,大部分是通过字面量的方式创建字符串,JVM虚拟机对这种创建字符串的方式做了优化,以减少内存占用。

大概流程

java 复制代码
String str1 = "abc";

上述代码执行有两种情况:

  • 若字符串常量池没有"abc",会在字符串常量池创建一个字符串对象,并将其引用赋值给str1(注意:这种方式也是在堆中创建对象后,将堆中的对象引用赋值给str1

  • 若字符串常量池已有"abc",则不再创建对象,直接将字符串常量池中的对象引用赋值给str1

上述代码执行结果,在jvm中如下:

stack区中建立了String类型的str1变量,指向了堆中的字符串

StringTable的key是"abc"这个字面量,value是堆区字符串对象地址,也指向了堆区的字符数组。

所以以下代码的运行结果是:

详解字面量方式创建字符串对象的流程

严格地说,字面量在代码运行到它所在语句之前,它还不是字符串对象

  1. 在上面的 java 代码被编译为 class 文件后,"abc" 存储于【类文件常量池】中
less 复制代码
Constant pool: // 常量池
   #1 = Methodref          #19.#41        // java/lang/Object."<init>":()V
   #2 = String             #42            // ab
   ...
  1. 当 class 完成类加载之后,"abc" 这个字面量被存储于【运行时常量池】(归属于方法区)中,其中 #1 #2 都会被翻译为运行时真正的内存地址

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

arduino 复制代码
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
         ...
  1. main 方法被调用时,就会执行里面的字节码指令
yaml 复制代码
0: ldc           #2                  // String abc
2: astore_1
3: return

ldc #2 就是到运行时常量池中找到 #2 的内存地址,找到 "abc" 这个字面量,先判断字符串常量池是否保存了对应的字符串对象的引用,如果有的话直接返回,否则根据它在常量池中创建一个 String 对象。(懒加载)

另外说需要说明:同一个类中的值相同的字面量,只有一份

StringTable

HotSpot 虚拟机中字符串常量池的实现是 src/hotspot/share/classfile/stringTable.cpp ,StringTable 可以简单理解为一个固定大小的HashTable ,容量为 StringTableSize(可以通过 -XX:StringTableSize 参数来设置),保存的是字符串字面量(key)和 字符串对象引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。

  • 字面量方式创建的字符串,会放入 StringTable 中,StringTable 管理的字符串,才具有不重复的特性。

  • 而 char[],byte[],int[],String,以及 + 方式本质上都是使用 new 来创建,它们都是在堆中创建新的字符串对象,不会考虑字符串重不重复。

String的intern方法

字符串提供了 intern 方法来实现去重,让通过new创建的字符串对象有机会受到 StringTable 的管理

arduino 复制代码
public native String intern();

调用intern方法,有以下两种情况:

如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。

java 复制代码
String x = ...;
String s = x.intern();

则会直接返回StringTable中的字符串对象地址给s。

例子

ini 复制代码
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

如果字符串常量池中没有保存对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。(1.7 以上 JDK 的做法)

例子

ini 复制代码
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

StringTable的位置

StringTable 的位置(1.6)

StringTable 的位置(1.8)

如何证明

  • 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

题目实战

String str1 = "abc"; String str2 = new String("abc");分别创建了几个对象

看上面的内容即可

计算输出结果1

java 复制代码
// 在堆中创建字符串对象"Java" ???
// 将字符串对象"Java"的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象"Java"对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象"Java"对应的引用 ???
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true

计算输出结果2

java 复制代码
public static void main(String[] args) {
    String s1 = new String("abc");
    String s2 = "abc";
    String s3 = "abc";
    String s4 = new String("abc");
    System.out.println(s1 == s2);
    System.out.println(s2 == s3);
    System.out.println(s1 == s4);
    System.out.println(System.identityHashCode(s1));
    System.out.println(System.identityHashCode(s2));
    System.out.println(System.identityHashCode(s3));
    System.out.println(System.identityHashCode(s4));
}
上述代码的输出
false
true
false
460141958
1163157884
1163157884
1956725890

分析下面代码的执行结果

java 复制代码
public static void main(String[] args) {
    String str1 = "abc";
    String str2 = "ab" + "c";
    String str3 = new String("ab") + "c";
    String str4 = str3.intern();
    System.out.println(str1 == str2); true
    System.out.println(str1 == str3); fasle
    System.out.println(str1 == str4); true
}

参考文章

  1. Java基础:带你深入了解StringTable(含Java面试资料)
  2. 从字符串到常量池,一文看懂 String 类设计
  3. String及StringTable(二):java中的StringTable
  4. 几张图彻底理解Java字符串常量池、String.intern()(非复制粘贴,准确解释)
相关推荐
31535669135 分钟前
ClipReader:一个剪贴板英语单词阅读器
前端·后端
ladymorgana8 分钟前
【Spring Boot】HikariCP 连接池 YAML 配置详解
spring boot·后端·mysql·连接池·hikaricp
neoooo28 分钟前
别慌,Java只有值传递——一次搞懂“为啥我改了它还不变”!
java·后端·spring
用户77853718369632 分钟前
一力破万法:从0实现一个http代理池
后端·爬虫
拖孩1 小时前
微信群太多,管理麻烦?那试试接入AI助手吧~
前端·后端·微信
Humbunklung1 小时前
Rust枚举:让数据类型告别单调乏味
开发语言·后端·rust
radient1 小时前
Golang-GMP 万字洗髓经
后端·架构
蓝倾1 小时前
如何使用API接口实现淘宝商品上下架监控?
前端·后端·api
舂春儿1 小时前
如何快速统计项目代码行数
前端·后端
Pedantic1 小时前
我们什么时候应该使用协议继承?——Swift 协议继承的应用与思
前端·后端