StringTable 常量池 运行时常量池 字符串加载 StringTable的特性

文章详细解释了Java中StringTable的工作原理,包括字符串的懒加载机制,如何在运行时常量池中存储和查找字符串对象,以及StringBuilder的使用。还探讨了字符串拼接的不同方式对内存的影响,以及intern()方法的作用,强调了编译期优化和运行时复用的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

StringTable

先看几个面试题

StringTable面试题

解析String s1 = "a";

使用javap -v 程序.class 反编译当前的程序,查看字节码文件

字节码文件1
对应虚拟机指令:

ldc #2 // 加载常量池中 符号地址为#2 的数据(其实就是字符串 “a”)
astore_1 // 将加载的数据,保存到方法的本地变量表LocalVariableTable中

localVariableTable

当程序真正运行的时候,常量池中的数据,全都被加载到运行时常量池中,并且出现了一个串池StringTable,用来记录字符串对象。

StringTable [ ]

“a”在常量池中并不是一个对象,当程序运行到main方法的第一行,执行了上面提到的两条虚指令,执行ldc命令的时候,才会将“a”变成对象。这时,会先查看StringTable中是否存在 “a” 对象,第一次执行,所以没有,就把 “a” 对象放入其中。

StringTable [ "a" ]

这种直到运行到某行,才将字符串转换为对象的做法,属于一种懒加载。
后面两行代码的执行过程也是一样。
解析加载字符串

解析 String s4 = s1 + s2;

使用命令javap -v 程序.class 反编译程序,找到这行代码对应的虚拟机指令:
虚拟机指令2
可以看到虚拟机指令做了这些动作
9:创建一个StringBuilder类对象
12:
13:执行特殊方法,StringBuilder类的构造器方法
16:加载LocalVariableTable 中卡槽1的数据(也就是“a”对象)
17:执行特殊方法,StringBuilder类的append方法
20:加载LocalVariableTable 中卡槽2的数据(也就是“b”对象)
21:执行特殊方法,StringBuilder类的append方法
24:执行特殊方法,StringBuilder类的toString方法
25:将结果保存到LocalVariableTable 中卡槽4中

也就是

new StringBuilder().append("a").append("b").toString()

而查看StringBuilder类的toString方法,会发现,执行了一个 new 操作。这会创建一个新对象,新对象的地址和原先不同。
StringBuilder的toString方法

所以 s3 == s4的结果是 false

解析String s5 = "a" + "b";

使用命令javap -v 程序.class 反编译程序,找到这行代码对应的虚拟机指令:
虚拟机指令3
可以看到
29:加载LocalVariableTable 中卡槽4的数据(也就是“ab”对象)
17:将结果保存到LocalVariableTable 中卡槽5

只是做了一个从常量池加载 “ab”对象的动作。
这是因为javac 对编译进行了优化,这里“a”,"b"都是确定的结果,可以在编译期间确定,变量为“ab”,所以可以直接加载StringTable 中的 “ab”对象。

因此 s3 == s5 答案为true

字符串延迟加载

StringTable 是位于堆的的一个保存运行时字符串对象的HashTable,并且其中的字符串对象,只有在第一次被使用的时候才会被加载,这就是字符串延迟加载。

下面演示一下,使用Idea的Memory功能,可以看到堆中对象的个数:
idea的memory
刚开始字符串类型对象,有2275个
执行到第一个断点,由于每个被执行到的字符串都是第一次被使用,所以每个字符串都被创建了一个新的对象,并放在StringTable 中。堆中字符串对象变为2285个。
idea的memory2
再次使用和上面相同的字符串字面量,这时由于StringTable中已经存在这样的字符串字面量的对象,就可以直接复用。字符串对象不变,仍未2285个。
idea的memory3

StringTable 的 intern 方法

String s1 = new String("a") + new String("b");
String s2 = s1.intern();
boolean b2 = s2 == "ab";
boolean b1 = s1 == "ab";

编译之后
常量池:“a”,“b”

运行期间:

代码执行到第一行,new String(“a”),首先将常量池中的 “a” 字符串生成对象放到串池中,并且使用这个串的值创建了一个新的对象放在堆中
StringTable:[ “a” ]
堆:new String(“a”)

new String(“b”) 同理
StringTable:[ “a” ,“b”]
堆:new String(“a”) new String(“b”)

两者相加,由于都是动态变量,在编译期间无法直接确定,所以不会放到常量池,在运行期间也不会放到 StringTable 中。而是会先创建一个StringBuilder对象,将堆中的new String(“a”) new String(“b”)两个字符串对象拼接起来,获得 new StringBuilder(“ab”),再toString(),生成对象 new String(“ab”) 在堆中。这是变量对象,所以不会放到StringTable中。
StringTable:[ “a” ,“b”]
堆:new String(“a”) new String(“b”) new String(“ab”)

执行到 String s2 = s1.intern();

将 s1 字符串对象放入 StringTable(若StringTable 中已经存在 “ab” 字符串对象就不会再放入),然后返回StringTable 中 “ab” 字符串对象的地址。

执行到 boolean b2 = s2 == “ab”;

“ab” 就会到 StringTable 中查找值为 “ab” 的字符串对象,获得该地址。

因此s2,和“ab” 其实都指向StringTable 的 同一个“ab” 字符串对象,也地址相等。

执行到 boolean b1 = s1 == “ab”;
此时StringTable 中的字符串对象,正是刚刚放进去的 s1 对象,所以这两个也是地址相等的。

String x = "ab";
String s1 = new String("a") + new String("b");
String s2 = s1.intern();

boolean b2 = s2 == "ab";
boolean b1 = s1 == "ab";

编译之后
常量池:“ab”,“a”,“b”

运行期间:

代码执行到第一行,String x = “ab”;,首先将常量池中的 “ab” 字符串生成对象放到串池中
StringTable:[ “ab” ]
堆:

代码执行 new String(“a”),首先将常量池中的 “a” 字符串生成对象放到串池中,并且使用这个串的值创建了一个新的对象放在堆中
StringTable:[ “ab”,“a” ]
堆:new String(“a”)

new String(“b”) 同理
StringTable:[ “ab”,“a” ,“b”]
堆:new String(“a”) new String(“b”)

两者相加,由于都是动态变量,在编译期间无法直接确定,所以不会放到常量池,在运行期间也不会放到 StringTable 中。而是会先创建一个StringBuilder对象,将堆中的new String(“a”) new String(“b”)两个字符串对象拼接起来,获得 new StringBuilder(“ab”),再toString(),生成对象 new String(“ab”) 在堆中。这是变量对象,所以不会放到StringTable中。
StringTable:[ “ab”,“a” ,“b”]
堆:new String(“a”) new String(“b”) new String(“ab”)

执行到 String s2 = s1.intern();

将 s1 字符串对象放入 StringTable,StringTable 中已经存在 “ab” 字符串对象就不放入,然后返回StringTable 中 “ab” 字符串对象的地址。

执行到 boolean b2 = s2 == “ab”;

“ab” 就会到 StringTable 中查找值为 “ab” 的字符串对象,获得该地址。

因此s2,和“ab” 其实都指向StringTable 的 同一个“ab” 字符串对象,也地址相等。

执行到 boolean b1 = s1 == “ab”;
刚刚s1 并没有放到 StringTable 中,所以此时StringTable 中的字符串对象与它并不相等

StringTable的特性

由上面,可以得出StringTable 的特性

  1. 常量池中的字符串仅是符号,只有第一次使用后才会变成对象,并保存到StringTable中
  2. 字符串转换为对象的时候,会尽量复用StringTable中已有的对象,避免每次重新新建对象
  3. 编译期间能确定的字符串常量,会被优化,直接加载拼接结果,如果StringTable中已经存在,就会复用StringTable 中已有的对象
  4. 只有编译期间能确定的字符串常量,才能直接使用StringTable中已有的对象,如果是字符串变量的拼接,编译期间无法预知结果,就会使用StringBuilder新建对象
  5. 可以使用instern方法,主动将串池中还没有的字符串对象放入串池
    1) 1.8版本:将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
    2)1.6 版本:将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份(对象地址改变),放入串池,会把串池中的对象返回
### ### Java String 常量池详解 在 Java 中,String 是一个特殊的类,它不仅被广泛使用,还通过常量池机制进行了深度优化。为了提升性能和减少内存开销,JVM 引入了 **字符串常量池StringTable)** 来管理字符串字面量。 #### 字符串常量池的定义与作用 字符串常量池是一种特殊的内存区域,用于存储字符串字面量(literal)和通过 `intern()` 方法添加的字符串对象。当使用双引号定义字符串,JVM 会优先检查常量池中是否已存在该字符串,若存在则直接返回其引用,否则新建一个并放入池中[^2]。 例如: ```java String a = "hello"; String b = "hello"; ``` 变量 `a` 和 `b` 指向的是同一个字符串对象,因为它们都指向常量池中的同一个实例。 #### 字符串常量池运行机制 在程序运行期间,字符串常量池会动态管理字符串实例。当使用 `new String("hello")` 创建字符串,JVM 会先检查常量池中是否存在 `"hello"`,若不存在则将其加入常量池。随后,会在堆中创建一个新的 `String` 对象,并指向常量池中的字符串[^4]。 ```java String c = new String("hello"); ``` 上述代码中,`c` 是一个堆中的新对象,但它内部的字符数据仍然指向常量池中的 `"hello"`。 #### intern() 方法与字符串池的扩展 Java 提供了 `String.intern()` 方法用于手动将字符串加入常量池。当调用该方法,JVM 会查找常量池中是否存在相同 Unicode 编码的字符串,如果存在则返回其引用,否则将当前字符串加入常量池并返回其引用[^3]。 ```java String d = new String("world").intern(); String e = "world"; ``` 此,`d` 和 `e` 指向的是同一个常量池中的字符串对象。 #### 常量池的分类与关系 字符串常量池是 JVM 中多种常量池机制的一部分。在编译,类的常量信息(如字符串字面量)会被存储在 **静态常量池** 中。类加载后,这些信息会被加载到 **运行常量池** 中,而字符串常量池则是运行常量池的一部分,专门用于管理字符串字面量和通过 `intern()` 方法加入的字符串[^1]。 例如,在编译后的 `.class` 文件中,以下代码: ```java String a = "a"; String b = "b"; String ab = "ab"; ``` 会被编译为常量池中的符号信息,在类加载后成为运行常量池的一部分,并在运行由 JVM 管理其在字符串常量池中的存在[^5]。 #### 总结 字符串常量池通过复用字符串对象,减少了内存的使用并提升了程序性能。它与静态常量池运行常量池共同构成了 Java 的常量管理机制。开发者可以通过 `intern()` 方法显式控制字符串常量池中的存在状态,从而优化内存使用。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值