Java字符串常量池-学习总结

有关字符串如何在内存中创建和共享的。涉及字符串字面量、字符串常量池、class文件内容等。

需要知道的几种常量池类型

class文件中的常量池

在class文件中存在一个常量池,主要存放编译期间生成的字面量和符号引用。其中的字面量就包括我们在java代码中用双引号括起来的字符串。我们可以通过jdk自带的javap工具查看class文件中的常量池和编译后的字节码。以下为java源码与javap -v部分输出:
java源码:

1
2
3
4
5
public class Main {
public static void main(String... args) {
System.out.println("hello");
}
}

javap -v的部分输出:

1
2
3
4
5
6
7
8
9
C:\...>javap -v Main
...(系统信息等)
Constant Pool:
...(其他字面量和符号引用)
#3 = String #23 // hello
...
#23 = Utf8 hello
...
...(字节码、行号、本地变量表等)

这两项分别表示class文件常量池中的CONSTANT_StringCONSTANT_Utf8类型的常量。其中,CONSTANT_String表示String常量的类型,但它并没有持有String常量的内容,而是指向另一个常量类型为CONSTANT_Utf8的常量,在那里才真正存储着String常量的内容(注意这里仅仅保存着内容,而不是字符串实例)。具体的class文件中的常量池格式可以查看JVM规范-4.4. The Constant Pool

运行时常量池

直接查看JVM规范-2.5.5. Run-Time Constant Pool

2.5.5. Run-Time Constant Pool

A run-time constant pool is a per-class or per-interface run-time representation of the constant_pool table in a class file (§4.4). It contains several kinds of constants, ranging from numeric literals known at compile-time to method and field references that must be resolved at run-time. The run-time constant pool serves a function similar to that of a symbol table for a conventional programming language, although it contains a wider range of data than a typical symbol table.

Each run-time constant pool is allocated from the Java Virtual Machine’s method area (§2.5.4). The run-time constant pool for a class or interface is constructed when the class or interface is created (§5.3) by the Java Virtual Machine.

The following exceptional condition is associated with the construction of the run-time constant pool for a class or interface:

When creating a class or interface, if the construction of the run-time constant pool requires more memory than can be made available in the method area of the Java Virtual Machine, the Java Virtual Machine throws an OutOfMemoryError.

大意是,JVM在加载类和接口的时候都会在JVM方法区中申请一块内存作为运行时常量池,运行时常量池的内容是class文件中常量池内容的体现。此外还提到,在加载类或接口时,所申请的运行时常量池的内存大小大于方法区所能申请的大小时,则会抛出OutOfMemoryError
在这里可以简单的理解为class文件常量池中的字面量和符号引用等内容会在类加载时进入对应的运行时常量池中。

字符串常量池

字符串常量池是JVM所维护的一个字符串实例的引用表,在HotSpot VM中,它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。

有关String的intern方法

查看String.intern()的Java Doc:

java.lang.String
@NotNull
public String intern()
External annotations available:
@org.jetbrains.annotations.NotNull
Returns a canonical representation for the string object.
A pool of strings, initially empty, is maintained privately by the class String.
When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.
It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.
All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java™ Language Specification.
Returns:
a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.

在String.intern()的Java Doc中有提到Sting类会私有维护一个字符串池(即字符串常量池),当调用一个String实例的intern方法时,会先检查字符串池中是否存在相同内容的字符串实例的引用,如果有则返回该引用(原先在字符串池中的),否则,会将String实例的引用添加进字符串池,接着返回该引用。然后还提到,任何字符串字面量或字符串的常量表达式都是interned的,即它们所指向的String对象都会在字符串常量池中留有一份引用。

Java语言规范-3.10.5. String Literals有一段关于字符串字面量的描述:

Moreover, a string literal always refers to the same instance of class String. This is because string literals - or, more generally, strings that are the values of constant expressions (§15.28) - are “interned” so as to share unique instances, using the method String.intern.

即字符串字面量或内容相同的字符串常量表达式,例如"hello""hel" + "lo"都指向同一个字符串对象实例。

字符串的创建

通过一个简单的案例来了解字符串的创建过程:

1
2
3
4
5
6
public class Main {
public static void main(String... args) {
String s1 = "hello";
System.out.println(s1);
}
}

  1. 首先在编译期间,"hello"作为字符串的字面量,会被保存在class文件的常量池中,具体为一个CONSTANT_String类型的常量指向另一个CONSTANT_Utf8类型的常量,然后CONSTANT_Utf8类型的常量中保存着hello这几个字符的Unicode编码。
  2. 接着在Main.class文件被加载时,会在方法区中申请一块内存作为Main类的运行时常量池,并将Main.class文件中常量池的内容放进运行时常量池中。注意此时,还没有创建”hello”字符串的任何实例。
  3. 接着到执行String s1 = "hello";时,表示”hello”字面量的CONSTANT_String类型的常量会被推到栈顶,此时会发现此常量尚未进入resolve阶段,因此会先执行resolve。在resolve阶段,首先会先查看字符串常量池中是否有相同内容的字符串实例的引用,如果有,则直接使用该引用,否则会在堆内存中创建一个新的字符串实例,其内容与CONSTANT_String类型的常量所指向CONSTANT_Utf8类型的常量中的内容一致,接着将这个引用添加到字符串常量池中,最后返回该引用。
  4. 最后将引用赋给s1,打印并return。

有几点需要注意的:

  1. 字符串的字面量指向一个字符串对象的实例,这个字符串对象实例的引用保存在字符串常量池中。
  2. 相同的字符串的字面量表示同一个字符串对象实例。
  3. 字符串字面量所表示的字符串对象实例并非一定在类加载阶段实例化。而是在resolve阶段,而resolve阶段的执行是可以延迟的。可以理解为当执行到字面量所在的代码时才会进入resolve阶段(对应的字节码指令为ldc)。

关于 String s = new String(“hello”)

根据这行代码执行时的上下文环境不同,具体执行的内容也不同。若在执行此代码之前,”hello”字符串已经在字符串常量池中(即,之前已出现过”hello”字面量或者通过String.intern方法将”hello”字符串加入字符串常量池中了),则只会在堆中创建一个新的字符串实例。否则,会先在堆中创建”hello”实例,添加该实例的引用到字符串常量池中,接着根据该实例再创建一个新的内容一致的字符串对象赋值给变量s。

参考:

Java Language and Virtual Machine Specifications
请别再拿“String s = new String(“xyz”);创建了多少个String实例”来面试了吧
Java 中new String(“字面量”) 中 “字面量” 是何时进入字符串常量池的? - 木女孩的回答 - 知乎
Java中,这些字符串什么时候进入常量池的? - RednaxelaFX的回答 - 知乎
Strings, Literally - by Corey McGlone