深入理解字符串常量池

java中的常量池

jvm中常量池主要分为Class文件常量池、运行时常量池、字符串常量池以及基本类型包装类对象常量池。

1.Class文件常量池

class文件是一组以字节为单位的二进制数据流,在java代码的编译期间,我们编写的java文件就被编译成.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。class文件中存在常量池(非运行时常量池),其在编译阶段就已经确定,jvm规范对class文件结构有着严格的规范,必须符合此规范的class文件才能被jvm认可和加载。

简而言之,就是class文件中除了包含类的版本、字段、方法、接口等信息。还有一项信息就是常量池(constant pool table),它用于存放编译器生成的**各种字面量(Literal)符号引用(Symbolic References)。**字面量比较接近java语言层面常量的概念,比如文本字符串,被声明final的常量值。符号引用是编译原理方面的概念,包括了如下三种类型的常量:1.类和接口的全限定名2.字段的名称和描述符3.方法的名称和描述符

2.运行时常量池

运行时常量池是方法区的一部分。当java文件被编译为class文件之后,就会生成上面所说的class常量池。

❓运行时常量池又是什么时候产生的?

jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接过程又分为验证、准备、解析三个阶段,这个过程也就是我们所说的类加载过程。当一个类完成类加载过程被加载到内存之后,jvm就会将class文件常量池的内容存放到运行时常量池中,由此可见运行时常量池是每个类都有一个。在上面也说了class常量池中存放的是字面量和符号引用,也就是说他们存放的并不是对象的实例,而是对象的符号引用值。而经过类加载过程中的解析(resolve)一步之后,就把符号引用替换为直接引用。这也就是为什么解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析的过程还会去查询全局字符串常量池以保证运行时常量池中所引用的字符串与全局字符串池所引用是一致的。

总的来说:运行时常量池的作用是存储java class文件常量池中的符号信息,运行时常量池中保存着一些class文件中描述的符号引用,同时在类的解析过程还会将这些符号引用替换成直接引用(直接指向实例对象的指针,内存地址)。

3.全局字符串池(字符串常量池)

  • 在 JDK6.0 及之前版本,字符串常量池是放在 Perm Gen 区(也就是方法区)中,此时常量池中存储的是对象。
  • 在 JDK7.0 版本,字符串常量池被移到了堆中了。此时常量池存储的就是引用了。在 JDK8.0 中,永久代(方法区)被元空间取代了。

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

为了更好的理解字符串常量池,下面逐渐深入理解。

深入理解字符串常量池

(1)java中创建字符串对象的俩种方式

1
2
3
String s1 = "hello";
String s2 = new String("hello");
System.out.println(s0 == s1);

第一种方式声明的字面量hello是在编译期就已经确定的,它会直接进入class文件常量池中,当运行期间在全局字符串常量池中会保存它的一个引用。实际上最终还是要在堆上创建一个hello对象。

第二种方式使用了new String(),也就是调用了String类的构造函数,new指令是创建一个类的实例并完成加载初始化的,因此这个字符串是在运行期才能确定的,创建字符串对象是在堆内存上的。

因此此时调用System.out.println(s0 == s1);返回的肯定是flase,因此==符号比较的是两边元素的地址,s1和s0都存在于堆上,但是地址肯定不相同。

(2)new String()到底创建了几个对象

1
String s1 = new String("abc")

首先这个代码中有个new关键字,这个关键字是在程序运行的时候根据已经加载的系统类String在堆内存中去实例化一个字符串对象,然后在这个string的构造器中传入“abc”,因为字符串里面的成员变量是一个final修饰的,所以它是一个字符串常量,所以jvm会首先拿这个字面量“abc”去字符串常量池中试图找到对应它的一个String引用,如果拿不到,就会在堆内存中创建一个“abc”的一个String对象,并且把引用保存到字符串常量池中,后面再有字面量“abc”一个定义,因为字符串常量池中已经存在字面量“abc”的引用,所以只需从字符串常量池中获得对应的引用即可,所以不需要去创建对象。

  • 如果“abc”这个字符串常量不存在,则创建俩个对象,分别是“abc”这个字符串常量,以及new String这个实例对象
  • 如果“abc”这个字符串常量存在,则只会创建一个对象,就是new String这个实例对象

(3)String str =new String(“a”) + new String(“b”) 会创建几个对象 ?

1
2
3
4
5
public class StringNewTest {
public static void main(String[] args) {
String str = new String("a") + new String("b");
}
}

字节码:

image-20231124165202926

对象1: new StringBuilder()
对象2: new String(“a”)
对象3: 常量池中的"a"
对象4: new String(“b”)
对象5: 常量池中的"b"

深入剖析: StringBuilder 的 toString() 方法中有 new String(value, 0, count) ,
对象6 :new String(“ab”)

强调一下:
StringBuilder 的 toString() 的调用,在字符串常量池中,没有生成"ab"。
如果前后文中还有代码,并且已经常量在常量池存在时,相同的常量 将不再创建,因为在常量池只能存在一份相同的对象。

结论是至多是6个对象。

(4)intern方法

1
2
3
4
5
6
7
8
9
10
11
12
String s1 = new String("abc");
String s2 = s1.intern();
String s3 = "abc";
System.out.println(s1 == s3);
System.out.println(s2 == s3);


String s4 = new String("3") + new String("3");
String s5 = s4.intern();
String s6 = "33";
System.out.println(s4 == s6);
System.out.println(s5 == s6);

结果(jdk1.8):

image-20231124165757742

String s1 = new String("abc"); 运行时创建了两个对象,一个是在堆中的”abc“对象,一个是在堆中创建的”abc”对象,并在常量池中保存“abc”对象的引用地址。

String s2 = s1.intern(); 在常量池中寻找与 s1 变量内容相同的对象引用,发现已经存在内容相同对象“abc”的引用,返回该对象引用地址,赋值给 s2。

String s3 = "abc"; 首先在常量池中寻找是否有相同内容的对象引用,发现有,返回对象"abc"的引用地址,赋值给 s3。

String s4 = new String("3") + new String("3");运行时创建了四个对象,一个是在堆中的“33”对象,一个是在堆中创建的”3”对象,并在常量池中保存“3”对象的引用地址。中间还有2个匿名的 new String(“3”) 这里我们不去讨论它们。

String s5 = s4.intern();在常量池中寻找与 ”33“对象内容相同的对象引用,没有发现“33”对象引用,将 s4 对应的”33“对象的地址保存到常量池中,并返回给 s5。

String s6 = "33";首先在常量池中寻找是否有相同内容的对象引用,发现有,返回对象"33"的引用地址,赋值给 s6。

System.out.println(s4 == s6);从上面可以分析出,s4 变量和 s6 变量地址指向的是相同的对象,所以返回 true。❗❗这里涉及到字符串相加:字符串相加的时候,都是静态字符串的结果会添加到字符串池,如果其中含有变量,则不会进入字符串池中。

img

❗注意:jdk1.6之前结果不同,因为jdk1.6字符串常量池是在方法区中(堆外)而1,7之后字符串常量值在堆中。所以1.6字符串常量池存放对象,1.7之后存放对象的引用。

总结:intern() 方法用于在运行时将字符串添加到内部的字符串池中,并返回字符串池中的引用。当调用 intern() 方法时,如果字符串池中已经存在相同内容的字符串,则返回字符串池中的引用;否则,将该字符串添加到字符串池中,并返回对字符串池中的新引用。