1. 字符串常量池的介绍
在 JAVA 语言中,有 8 种基本类型和一种比较特殊的类型 String。这些类型为了使它们在运行过程中速度更快,更节省内存,都提供了一种 常量池
的概念。常量池就类似一个JAVA系统级别提供的缓存。
其中,这 8 种基本类型的常量池都是系统协调的,而 String 类型的常量池比较特殊。它的主要使用方法有两种:
- 直接使用双引号声明出来的 String 对象会直接存储在常量池中
- 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern() 方法
那么 String#intern() 方法的作用是什么呢?
查看 API 文档:
/*** Returns a canonical representation for the string object....* When the intern method is invoked, if the pool already contains a* string equal to this {@code String} object as determined by* the {@link #equals(Object)} method, then the string from the pool is* returned. Otherwise, this {@code String} object is added to the* pool and a reference to this {@code String} object is returned....*/
public native String intern();
String#intern() 方法是一个 native 方法。
由注释知:如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回。即它的作用:可以在 运行期
将字符串引用放置到字符串常量池。
字符串常量池:在JVM中,为了减少相同的字符串的重复创建,为了达到节省内存的目的。会单独开辟一块内存,用于保存字符串常量,这个内存区域被叫做字符串常量池
常量池的位置:
- JDK1.7 之前,运行时常量池、字符串常量池是存放在方法区,此时方法区的实现是永久带
- JDK1.7 字符串常量池被单独从方法区移到堆中,运行时常量池还在永久带(方法区)
- JDK1.8,永久带更名为元空间(方法区的新的实现),但字符串常量池池还在堆中,运行时常量池在元空间(方法区)
常量池存放的内容:
- 编译期生成的各种字面值:文本字符串、final 修饰的常量值、基本类型的值
- 编译期生成的符号引用:类的完全限定名、字段/方法的名称等
2. Java 内存区域
在 Java 中,创建字符串对象有两种形式:
- 字面量的形式:
String str = “hello”
- 使用 new 的形式:
String s = new String(“world”)
那么这两种创建的字符串对象的方式有什么区别呢?
要想弄清楚它们的区别,首先得知道它们在内存的位置:
JVM 在执行 JAVA 程序的过程中,会把它所管理的内存划分为若干个不同的数据区域。其中,与字符串创建有关的是:方法区、堆区、栈区
- 方法区(线程公有):存储类信息、常量、静态变量
- 堆(线程公有):存放对象、数组
- 栈区(线程私有):存放基本数据类型、对象的引用
每当一个方法被执行时,就会在栈区创建一个 栈帧
;基本数据类型、对象引用就存在栈帧的 局部变量表
中。
当一个类被加载之后,类信息就存储在方法区中。在方法区中,有一块叫做 运行时常量池(Runtime Constant Pool)
,它是每个类私有的,每个 class 文件中的 常量池
被加载器加载之后就映射存放在这。
字符串常量池
是全局共享的。字符串调用 String#intern() 方法后,其引用就存放在 String Pool 中。
3. 两种创建方式在内存中的区别
字面量创建对象的方式:
- 当一个 .java 文件被编译成 .class 文件时,和其它所有常量一样,每一个字面量都通过一种特殊的方式被记录下来。
- 当一个 .class 文件被加载时(注意加载发生在初始化之前),JVM在 .class 文件中寻找字面量。
- 当找到一个时,JVM 会检查是否有相等的字符串在常量池中存放了堆中引用。
- 如果找不到,就会在堆中创建一个对象,然后将它的引用存放在池中的一个常量表中。
- 一旦一个字符串对象的引用在常量池中被创建,这个字符串在程序中的所有字面量引用都会被常量池中已经存在的那个引用代替
举个例子:
在一个类中,以 字面量
的方式将字符串 str 赋值为 “Hello”:
public class Test {
public static void main(String[] args) {
String str = "Hello";}
}
Test.java 文件编译后得到 .class 文件,.class 文件里面包含了类的信息,其中有一块叫做常量池(Constant Pool)的区域,(.class 文件中的常量池和内存中的常量池并不是同一个东西)。
.class 文件中的常量池主要存储:包括字面量。其中,字面量包括类中定义的常量。
当程序用到 Test 类时,Test.class 被解析到内存中的方法区。.class 文件中的常量池信息会被加载到运行时常量池,但 String 类不是。代码中 Hello
会在堆区中创建一个对象,同时会在字符串池(String Pool)存放一个它的引用,如下图所示:
【注意】:此时只是 Test 类刚刚被加载,主方法中的变零 str 并没有被创建,而“Hello”对象已经创建在于堆中。
当主线程开始创建 str 变量时,JVM 会去字符串常量池中查找是否有equals(“Hello”)的 String。如果有,就把在字符串池中“Hello”的引用复制给str;如果没有,就会在堆中 new 一个对象,同时把引用驻留在字符串常量池,再把引用赋给变量 str:
当用字面量赋值的方法创建字符串时,无论创建多少次,只要字符串的值相同,它们所指向的都是堆中的同一个对象
当使用 new 创建对象:
当利用new关键字去创建字符串时,前面加载的过程是一样的,只是在运行时无论字符串池中有没有与当前值相等的对象引用,都会在堆中新开辟一块内存,创建一个对象
举个例子:
public class Test {
public static void main(String[] args) {
String str = new String("Hello");}
}
这就是为什么:String str = new String(“Hello”);
会创建两个对象的原因。
【总结】:
- 字面量创建字符串会先在字符串池中找,看是否有相等的对象,没有的话就在堆中创建,把地址驻留在字符串池;有的话则直接用池中的引用,避免重复创建对象。
- new 关键字创建时,前面的操作和字面量创建一样,只不过最后在
运行时
会创建一个新对象,变量所引用的都是这个新对象的地址
4. String#intern() 方法
文章开头也说过,String#intern() 方法的作用:在 运行期
将字符串引用放置到字符串常量池。
下面是关于 String#intern() 方法的一道面试题。其实,理解了上述内容,这道面试题就不难了:
String s1 = new String("a") + new String("bc");
s1.intern();
String s2 = "abc";
System.out.println(s1 == s2); // true
执行完语句 String s1 = new String(“a”) + new String(“bc”);
后创建的对象有:
- 字符串常量池:“a”、“bc”
- 堆:“a”、“b”、“abc”、StringBuilder 对象
【注意】:字符串常量池中没有对象 “abc”
再执行语句s1.intern();
:
- 发现常量池中没有字符串对象 “abc”,就在字符串常量池中添加一个引用,并指向堆中的对象
- 如果修改成语句
String s = s1.intern()
,则 s1 == s,且都指向堆中对象的地址
【注意】:此时字符串常量池中已有字符串对象 “abc” 了哈
执行语句 String s2 = “abc”;
:
- JVM 发现字符串常量池中有字符串对象 “abc”,则将字符串常量池中的引用赋值给 s2。所以,变量 s2 也是指向堆中对象的地址。所以 s2 == s1
【思考】其实,读者可以试试,上面语句的调换顺序,看结果是否和你想的一样?
public class InternTest {
public static void main(String[] args) {
String s1 = new String("a") + new String("bc");String s2 = "abc";String s3 = s1.intern();System.out.println(s1 == s2); // falseSystem.out.println(s1 == s3); // falseSystem.out.println(s2 == s3); // true}
}