Java中字符串池化的基本机制是什么?

24

我对于为什么可以不用调用new String()来创建字符串感到好奇,因为API将其定义为class java.lang.StringObject

那么我们如何使用String s="hi"而不是String s=new String("hi")呢?

这个帖子阐明了==运算符的使用和缺少new,并指出这是由于JVMString字面量放入常量池中或从中提取,因此Strings是不可变的。

当看到这样的陈述时:

String s="hi"

第一次会发生什么?

  1. JVM是否像这样替换它:String s=new String("hi"),其中创建了一个对象并将"hi"添加到字符串字面值池中,因此随后的调用(例如String s1="hi")都从池中获取?

  2. 底层机制是这样操作的吗?如果是,则

  3. String s=new String("Test");
    String s1="Test";
    

    与...相同

    String s="Test";
    String s1="Test";
    

    在内存利用和效率方面有什么考虑吗?

  4. 此外,我们是否可以访问字符串池以检查其中存在多少个String文字,占用了多少空间等信息?


2
“JVM会像这样替换它吗?” - 我认为是编译器替换了它,而不是JVM。 - user330315
是的,但是否会发生类似我提到的替换或优化的等效类型取决于具体情况。 - Sainath S.R
顺便问一下,你看过String的intern()方法的注释了吗?http://docs.oracle.com/javase/7/docs/api/ - klarki
1
你的第三个问题在这里得到了回答:https://dev59.com/u2Ik5IYBdhLWcg3wr_9o - blgt
s = "hi"更改为s = new String("hi")是什么意思?我不明白这样做除了添加一个新层之外解决了什么问题,现在你需要s = new String(new String("hi")),最终你需要一个无限期的术语new String(new String(...。如果你指的是rhs中的"hi"不是字符串,那么你应该使用不同的语法。 - Bakuriu
查看JVM类文件规范,所有内容都在那里。显然,首先生成.class文件的是编译器。 ;) - TC1
7个回答

15

Java编译器对字符串字面量有特别的支持。假设没有这种支持,那么在源代码中创建字符串将会非常麻烦,你需要写类似这样的东西:

// Suppose that we would not have string literals like "hi"
String s = new String(new char[]{ 'h', 'i' });

回答你的问题:

  1. 大体上说是这样的,如果你真的想知道细节,你需要研究JVM的源代码,可以在OpenJDK找到,但要注意它非常庞大且复杂。

  2. 不,这两个并不等价。在第一种情况下,你明确地创建了一个新的String对象:

    String s=new String("Test");
    

    它将包含由字面值"Test"表示的String对象的副本。请注意,在Java中写new String("some literal")绝不是一个好主意 - 字符串是不可变的,永远不需要复制字符串字面值。

  3. 我不知道有什么方法可以检查字符串池中的内容。


我理解新的 'String("Hi");' 会产生一个副本,但是考虑到这两个语句一起使用时,它们不是更或多或少相同吗?因为无论如何都需要创建一个副本,我猜想 String s=new... 是否会在字面上和本地上都产生一个副本而不是指向它,如果是这样,我就理解了它们之间的区别,+1 个 "hi" 的副本。 - Sainath S.R
1
不,第一种情况下你有两个内容相同的String对象“Test”,而在第二种情况下,你只有一个String对象(并且ss1都指向同一个对象)。 - Jesper
2
return string.intern() == string 应该检查字符串是否被内部化。这是一种不太正规的方法,但应该能返回正确的答案。大多数情况下都可以。它还具有将 String 内部化的副作用。 - Boris the Spider

11
  1. String s="hi" 第一次发生了什么?

JVM 不会像这样替换它:String s=new String("hi"),其中创建一个对象并将 "hi" 添加到字符串字面值池中,因此诸如 String s1="hi" 的后续调用将从该池中获取。

实际上发生的是-在编译时解析字符串常量并在类被加载/初始化或惰性地添加到字符串常量池(String constants pool)中,因此它们可以供JVM内部的类使用。

请注意,即使在字符串常量池中具有值为"hi"的字符串,new String("hi")将在堆上创建另一个字符串并返回其引用。

  1. 是什么?
 String s=new String("Test"); 
 String s1="Test"; 
与...相同
 String s="Test"; 
 String s1="Test"; 

在内存利用和效率方面有什么区别吗?

不会,第一种情况下创建了2个“Test”字符串。其中一个将被添加到字符串常量池中(假设它还没有存在于那里),另一个将在堆上创建。第二个可以被垃圾回收。在第二种情况下,字符串常量池中只有一个字符串字面值,并且有2个引用指向它(s和s1)。

  1. 此外,是否有任何方法可以从程序或任何监视工具中访问字符串池,例如检查其中存在多少个字符串字面值,占用的空间等?

我认为我们无法查看字符串常量池的内容。我们只能根据自己的假设进行推断并确认其行为。


那么,为了明确起见,像String s = new String("Test");这样的调用将把它添加到文字池中(假设还没有在其中),并创建一个值为“Test”的堆上的String对象? - Sainath S.R
@DroidIcs - 没错。所以下次你执行 String someVar="Test"时,来自字符串常量池的值将被返回。 - TheLostMind
2
请注意,将字面量添加到池中是编译器(javac)执行的编译时操作,而调用字符串构造函数是由JVM在运行时执行的。因此,实际上它是相同的机制,只是在调用new String("test")时,您将来自文字池的引用作为构造函数参数传递,而不是直接分配它。另外,在http://docs.oracle.com/javase/8/docs/api/java/lang/String.html#intern--上有一个#intern方法,它将删除重复的实例并用来自池的引用替换它们。 - Tibor Blenessy
2
@saberduck,我稍微不同意第一句话。编译器将字符串添加到类的常量池中,稍后JVM将所有类字符串添加到其字符串池中(可能在类加载时一次性添加)。 - maaartinus
@maaartinus - 我完全同意你的观点.. :) - TheLostMind
显示剩余2条评论

7
以下是稍微简化的内容,因此不要试图引用确切的细节,但一般原则适用。
每个编译好的Java类包含数据块,指示在该类文件中声明了多少个字符串,每个字符串的长度以及属于这些字符串的字符。当类被加载时,类加载器将创建一个适当大小的String[]来容纳在该类中定义的所有字符串;对于每个字符串,它将生成一个适当大小的char[],从类文件中读取相应数量的字符到char[]中,创建一个封装那些字符的String,并将引用存储到类的String[]中。
编译某个类(例如Foo)时,编译器知道它首先遇到了哪个字符串字面量,第二个、第三个、第五个等等。如果代码说myString = "George";并且George是第六个字符串字面量,则会出现“load string literal #6”指令;当即时编译器为该指令生成代码时,它将生成一条指令,以获取与该类关联的第六个字符串引用。

6

虽然这与主题不是密切相关,但当你对 Java 编译器会做什么有疑问时,可以使用

javap -c CompiledClassName

打印实际正在进行的内容。(来自CompiledClassName.class所在的目录中的CompiledClassName)

除了Jesper的答案外,还有更多机制在起作用,例如当您从文字或final变量连接字符串时,它仍将使用intern池:

String s0 = "te" + "st";
String s1 = "test";
final String s2 = "te";
String s3 = s2 + "st";
System.out.println(s0==s1); //true
System.out.println(s3==s1); //true

但是,当您使用非最终变量进行连接时,它将不使用池:
String s0 = "te";
String s1 = s0 + "st";
String s2 = "test";
System.out.println(s1 == s2); //false

实际上,这并没有为 Jesper 的答案增添任何内容。 - TheLostMind
1
只是想为这个话题做出贡献,重复Jesper说过的话没有意义。 - klarki

5
  1. 有点类似,但并非完全相同。在常量池解析期间创建和整合字符串常量。这发生在第一次执行加载字符串字面量的LDC字节码时。在第一次执行后,JVM将JVM_CONSTANT_UnresolvedString常量池标记替换为JVM_CONSTANT_String标记,以便下一次LDC使用现有字符串而不是创建新字符串。

  2. 不是。第一次使用"Test"会创建一个新的字符串对象。然后new String("Test")会创建第二个对象。

  3. 可以,使用HotSpot服务性能代理。这里是一个示例


0

我相信创建字符串的基本机制是使用 StringBuilder,在最后组装 String 对象。至少我可以确定,如果你有一个想要更改的字符串,例如:

String str = "my String";
// and then do
System.out.println(str + "new content");

这个操作会从旧对象创建一个StringBuilder,并用它构建一个新的对象来替换旧对象。这就是为什么使用StringBuilder比使用普通字符串更加内存高效,因为你可以向其中添加内容。

还有一种方法可以访问已经创建的String池,那就是使用String.intern()方法。它告诉Java对于相同的字符串使用相同的内存空间,并给你一个指向该内存位置的引用。这也允许你使用==运算符来比较字符串,更加内存高效。


-2

字符串池是存储在堆中的字符串池,例如:

String s="Test";
String s1="Test";    

它们都存储在堆中并引用单个“Test”,因此s1 = s,

String s=new String("Test");

是一个对象,也存储在堆中,但与 s1=s 不同。请参见此处


3
Java中的字符串必须使用双引号。因为你使用单引号,所以你的代码在Java中无效。 - Stephen Ostermiller
下次我会记住它。 - user3912994

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接