在JVM的内存模型中,我们经常听到常量池的概念,我一开始时认为常量池就是一块内存区域,所有常量存放于其中,最近在学习JVM,看到好几种常量池的说法,把自己都搞糊涂了。花了一天时间,查询各种资料,所幸有所顿悟,决定记录下来,方便查阅。
注意本文以JVM8-HotSpot为基准。
1. 相关知识科普
(1)什么是常量
用final修饰的成员变量表示常量,值一旦给定就无法改变!final修饰的变量有三种:静态变量(类变量)、实例变量和局部变量,分别表示三种类型的常量。
(2)为什么使用常量池,即常量池的好处
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串字面量放到一个常量池中。
-
节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间;
-
节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等,new出来的不行。
(3)==的含义
-
基本数据类型之间使用==,比较的是他们的数值;
-
引用之间应用==,比较的是他们在内存中的存放地址。
2. class文件常量池(class constant pool)
public class App {
private String value = "Hello";
public int id = 110;
public final static int CONSTANT = 0x110;
public int getId() {
return id;
}
public void setId(int id) {
int tmp = id + 1;
this.id = tmp;
}
}
使用javac编译生成App.class,再使用javap -v App.class 查看编译后的内容,这里主要列出常量池的部分:
Constant pool:
#1 = Methodref #6.#29 // java/lang/Object."<init>":()V
#2 = String #30 // Hello
#3 = Fieldref #5.#31 // com/taylor/test/jvm/App.value:Ljava/lang/String;
#4 = Fieldref #5.#32 // com/taylor/test/jvm/App.id:I
#5 = Class #33 // com/taylor/test/jvm/App
#6 = Class #34 // java/lang/Object
#7 = Utf8 value
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 id
#10 = Utf8 I
#11 = Utf8 CONSTANT
#12 = Utf8 ConstantValue
#13 = Integer 272
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 LocalVariableTable
#19 = Utf8 this
#20 = Utf8 Lcom/taylor/test/jvm/App;
#21 = Utf8 getId
#22 = Utf8 ()I
#23 = Utf8 setId
#24 = Utf8 (I)V
#25 = Utf8 tmp
#26 = Utf8 MethodParameters
#27 = Utf8 SourceFile
#28 = Utf8 App.java
#29 = NameAndType #14:#15 // "<init>":()V
#30 = Utf8 Hello
#31 = NameAndType #7:#8 // value:Ljava/lang/String;
#32 = NameAndType #9:#10 // id:I
#33 = Utf8 com/taylor/test/jvm/App
#34 = Utf8 java/lang/Object
class文件常量池主要存放两大常量:字面量(Literal)和符号引用(Symbolic References)。
(1) 字面量:接近java语言层面的常量概念,主要包括:
-
文本字符串,也就是我们经常声明的:public String value = "Hello";中的"Hello"
#2 = String #29 // Hello #30 = Utf8 Hello
-
用final修饰的静态变量
#13 = Integer 272
强调一下,字面量指的是数据的值,即”Hello“和0x110(272)。通过上面对常量池的观察,这两个字面值确实存在于常量池中。而对于实例变量,即上面的public int id = 110,常量池只保留了他们的字段描述符I和字段的名称id,他们的字面量不会存在于常量池中。小朋友,你是不是有很多问号?110去哪里了?答案是,他们在对应的字节码方法里:
第11行对应的 bipush 110public com.taylor.test.jvm.App(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #2 // String Hello 7: putfield #3 // Field value:Ljava/lang/String; 10: aload_0 11: bipush 110 13: putfield #4 // Field id:I 16: return
(2) 符号引用:主要设涉及编译原理方面的概念,包括下面三类常量:
-
类和接口的全限定名,将类名中原来的"."替换为"/"得到的,主要用于在运行时解析得到类的直接引用
#5 = Class #27 // com/taylor/test/jvm/App #33 = Utf8 com/taylor/test/jvm/App
-
字段的名称和描述符(字段类型),字段也就是类或者接口中声明的变量,包括类变量和实例变量
-