Java中的各种常量池

前言

“常量池”,这个光听概念其实应该是很好理解的,就是用来存放常量的地方。常量就是无法进行改变的那些量。

但是java中有许许多多的“常量池”,有的时候乍一听还反应不过来,今天就打算梳理一下这些常量池。

class常量池

随便写一个hello world的程序,然后编译,使用javap命令查看,你会发现字节码文件中就有一个class常量池:

image-20200812125353569

可以看到里面存放了各种字面量(如Hello World,应该在#18中)和各种符号引用(如#10

显然由于每一个类是一个字节码文件,所以这个class常量池是每个类独享的。

运行时常量池

上面说的class常量池,是字节码文件的常量池,本质上是存在与磁盘内的。我们都知道进程是无法直接使用磁盘内的东西的,所以必须要把这个磁盘中的class常量池加载到内存中才可以。而这个class常量池被加载到的地方,就叫做运行时常量池。

java中的运行时常量池位于方法区(方法区是共享空间哦),在JDK1.8之后方法区位于元空间,且元空间位于本地内存之中。

运行时常量池除了保存符号引用,还可能(因为没有强制要求,所以各家的JVM可以自己实现)有直接引用(你可以理解为指向内存的指针)。

运行时常量池除了可以通过由class文件的字节码文件常量池加载得到,还可以通过运行时动态的进行加入(String.intern())。

字符串常量池

这个应该是我们最先接触到,也是最多接触到的吧。String s = "a"这样的语句,当时老师说的是在字符串常量池中有一个a,然后接下来你再去声明别的String b = "a",会从字符串常量池中取出,所以字符串s和b的地址是一样的。

那么这个字符串常量池是在哪里呢?在JDK1.7之前是放在方法区(确切说应该是在方法区的运行时常量池)中的,而在JDK1.7之后被放到了堆中。

而且字符串常量池是全局唯一的,用下面的例子可以展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {

public static void main(String[] args) {
String a = "a";
// 返回true
System.out.println(a == new StringTest().a);
}

}


class StringTest {
public String a = "a";
}

也就是从JDK1.7之后,所有的字符串对象就到了堆中,我们用一个例子可以证明:

1
2
3
4
5
6
public static void main(String[] args) {
String str1 = new StringBuilder("111").append("22").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}

这段代码会返回true和false。

首先第一个能返回true是因为现在的string已经在堆中了,所以str1.intern返回的自然是这个字符串对象的地址了。

那第二个为什么会返回false呢?因为java这个字符串已经在字符串常量池存在了(系统放进去的),所以不符合首次出现。