String Interning研究

本文深入探讨了Java中字符串的处理方式,特别是字符串常量池和字符串池的工作原理,揭示了如何利用String字面量和intern()方法来优化内存使用。

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

对Java字符串的探究

SEP 8TH, 2013 | COMMENTS

问题的出发点

在网上看到一道题:

1
String str = new String("abc");

以上代码执行过程中生成了多少个 String 对象?

答案写的是两个。”abc”本身是一个,而 new 又生成了一个。

“abc”是什么

查看这句程序的字节码,如下:

1
2
3
4
5
NEW String
    DUP
    LDC "abc"
    INVOKESPECIAL String.<init>(String) : void
    ASTORE 1

指令ldc indexbyte的含义:将两字节的值从 indexbyte 索引的常量池中的项中推到方法栈上。

指令LDC "abc"说明了”abc”并不是直接以对象存在的,而是存在于常量池的索引中。String 的构造函数调用命令实际使用的就是 String 类型作为参数,那么,栈上应该有一个 String 类型的索引。

由此我们得出,在字节码中,ldc 命令在常量池中找到了能索引到“abc”那个 String 对象的索引值。

常量池

常量池是类文件(.class)文件中的一部分,记录了许多常量信息,索引的字符串信息。

由于 Java 是动态加载的,类文件并没有包含程序运行时的内存布局,方法调用等无法直接记录出方法的物理位置,常量池通过索引的方法解决了这个问题。

常量池中存着许多表,其中 Constant_Utf8_info 表中,记录着会被初始化为 String 对象的字符串的字面值(iteral)。 而在 String 的 java doc 中,有对 String 字面值的说明:

All string literals in Java programs, such as “abc”, are implemented as instances of this class.

在 Java 编译的过程中,确定下来的 String 字面值都先被优化记录在常量池中(那些双引号字符串,都是以 CONSTANT_utf8_info 的形式存储在常量池中的)。也就是说,Java 源代码文件中出现的那些诸如”abc”字符串,都已经被提前放在了常量池中。

可以使用如下代码验证这一点:

1
2
3
4
5
6
7
8
9
public class Program
{
    public static void main(String[] args)
    {
       String str1 = "Hello";
       String str2 = "Hello";
       System.out.print(str1 == str2);
    }
}

输出结果是 true.说明”Hello”作为对象是被程序从同一个内存空间读取出来的。

常量池是编译时产生的,存在于类文件中(*.class 文件)。运行时,JVM 中每个对象都拥有自己的运行时常量池(run time constant pool)。

字符串池

我在 String 的 java doc 中又发现了一个有趣的 method:intern() ,我翻译如下:

当 intern 方法被调用,如果池中已经拥有一个与该 String 的字符串值相等(即 equals()调用后为 true)的 String 对象时,那么池中的那个 String 对象会被返回。否则,池中会增加这个对象,并返回当前这个 String 对象。

其中有介绍一个字符串池的东西:字符串池(String pool),初始是空的,由类私有的控制。

查看 java.lang.String 的源代码,发现 Intern()方法是一个 native 方法,即本地实现的方法,而不是一个 java 方法,这让我们不能直观的看到字符串池的实现细节。不过能够理解字符串池其实是类似于线程池的缓冲器,可以起到节约内存的作用。如下代码可以验证

1
2
3
4
5
6
7
8
9
10
11
12
13
package biaobiaoqi.thinkingInJava;

public class Test {
    public static void main(String[] args){

        String strA1 = "ab";
        String strA2 = "c";
        String strB1 = "a";
        String strB2 = "bc" ;
        System.out.println((strA1+strA2).intern() == (strB1 + strB2).intern());

    }
}

输出结果为 true。

现代的 JVM 实现里,考虑到垃圾回收(Garbage Collection)的方便,将 heap 划分为三部分: young generation 、 tenured generation(old generation)和 permanent generation( permgen )

字符串池是为了解决字符串重复的问题,生命周期长,它存在于 permgen 中。

总结

编译 Java 源代码时,源文件中出现的双引号内的字符串都被收纳到常量池中,用 CONSTANT_utf8_info 项存储着。

JVM 中,相应的类被加载运行后,常量池对应的映射到 JVM 的运行时常量池中。其中每项 CONSTANT_utf8_info(也就试记录那些字符串的)都会在常量引用解析时,自动生成相应的 internal String,记录在字符串池中。

回过头来看看文章刚开始的那个问题。

1
String str = new String("abc");

这里确实是有两个 String 对象生成了。

new String("xxx") 创建的 String 对象会在 heap 中重新生成新的 String 对象,绕过字符串池的管辖。而如果使用String str = "xxx"则先查看字符串池 是否已经存在,存在则直接返回 PermGen 中的该 String 对象,否则生成新的 String 对象,并将它加入字符串池中。

尽量使用String str = "abc";,而不是String str = new String("abc");用 new 的方法肯定会开辟新的 heap 空间,而前者的方法,则会通过 string interning 优化。

参考资料

### String 数据类型概述 在不同编程语言中,`String` 的实现方式有所不同。对于 Java 而言,`String` 并不是一个内置数据类型而是 `java.lang.String` 类的一个实例[^3]。 #### 特性 - **不可变性**:一旦创建了一个 `String` 对象,则其内容无法被更改。任何改变都会导致新对象的生成。 - **线程安全**:由于字符串是不可变的对象,在多线程环境中可以放心使用而不必担心同步问题。 - **性能考虑**:频繁操作字符串可能会造成大量临时对象产生,影响程序执行效率;此时建议采用可变版本如 `StringBuilder` 或者 `StringBuffer` 来代替。 ```java // 创建一个新的字符串常量 String greeting = "Hello, world!"; System.out.println(greeting); // 连接两个字符串 String name = "Alice"; String message = "Welcome to the party, "; message += name; System.out.println(message); ``` #### 基本用法 可以通过多种方式来定义和初始化一个 `String` 变量: - 直接赋值给变量; - 使用构造函数传递字符序列作为参数; - 利用静态工厂方法如 `valueOf()` 将其他类型的值转换成字符串形式。 ```java // 方法一:直接指定字面量 String str1 = "This is a direct assignment."; // 方法二:通过new关键字调用带参构造器 String str2 = new String("Created via constructor."); // 方法三:利用静态方法进行类型转化 int number = 42; String str3 = String.valueOf(number); // 结果:"42" ``` #### 字符串比较 当涉及到判断两个字符串是否相等时需要注意区分逻辑上的相同(即内容一致)以及物理地址的一致性。前者应该借助于 `equals()` 方法而非运算符 `==` ,后者则正好相反。 ```java String s1 = "hello"; String s2 = "HELLO".toLowerCase(); if (s1.equals(s2)) { System.out.println("The strings are logically equal."); } else { System.out.println("The strings differ in content or case."); } if (s1 == s2.intern()) { // intern() 返回池中的规范表示形式 System.out.println("Both refer to same object after interning."); } ``` #### 高效处理大文本 如果应用程序需要频繁拼接或者修改较长的文字片段,那么应当优先选用 `StringBuilder` (单线程环境下更高效)或者是它的线程安全版 `StringBuffer` 。这些类允许动态调整内部缓冲区大小从而减少不必要的内存分配次数。 ```java StringBuilder sb = new StringBuilder(); for(int i=0; i<1000 ;i++){ sb.append(i).append(", "); } sb.setLength(sb.length()-2); // 移除最后一个逗号及其后的空格 System.out.println(sb.toString()); ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值