想要搞清楚「运行时常量池」,就必须先弄清楚「常量池」是什么?!
「常量池」就在 Class 文件中,通过javap -v xxx
就可以看到 Class 文件反编译后的内容,其中有一项Constant pool
就是常量池,它用于存放编译期生成的各种字面量与符号引用
字面量比较接近于 Java 语言层面的常量池概念,如:文本字符串、被声明为 final 的常量值等
常量池的项目类型中只有五类字面量:整型字面量、浮点型字面量、长整型字面量、双精度浮点型字面量、字符串类型字面量
关于符号引用和直接引用的详细内容可见 类加载的过程 的解析部分!!
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如:字符串常量池,在编译阶段就把所有的字符串放到一个常量池中
==
比equals()
快。对于两个引用变量,只用==
判断引用是否相等,也就可以判断实际值是否相等// 源码
public class Test {
public final int a = 1; // 常量
public static int b = 2; // 静态变量
public static int c = 200000; // 静态变量
public final static int d = 3; // 静态常量
public String s = "abc"; // 字符串
}
// 常量池
Constant pool:
#1 = Methodref #9.#30 // java/lang/Object."<init>":()V
#2 = Fieldref #8.#31 // com/lfool/myself/Test.a:I
#3 = String #32 // abc
#4 = Fieldref #8.#33 // com/lfool/myself/Test.s:Ljava/lang/String;
#5 = Fieldref #8.#34 // com/lfool/myself/Test.b:I
#6 = Integer 200000
#7 = Fieldref #8.#35 // com/lfool/myself/Test.c:I
#8 = Class #36 // com/lfool/myself/Test
#9 = Class #37 // java/lang/Object
#10 = Utf8 a
#11 = Utf8 I
#12 = Utf8 ConstantValue
#13 = Integer 1
#14 = Utf8 b
#15 = Utf8 c
#16 = Utf8 d
#17 = Integer 3
#18 = Utf8 s
#19 = Utf8 Ljava/lang/String;
#20 = Utf8 <init>
#21 = Utf8 ()V
#22 = Utf8 Code
#23 = Utf8 LineNumberTable
#24 = Utf8 LocalVariableTable
#25 = Utf8 this
#26 = Utf8 Lcom/lfool/myself/Test;
#27 = Utf8 <clinit>
#28 = Utf8 SourceFile
#29 = Utf8 Test.java
#30 = NameAndType #20:#21 // "<init>":()V
#31 = NameAndType #10:#11 // a:I
#32 = Utf8 abc
#33 = NameAndType #18:#19 // s:Ljava/lang/String;
#34 = NameAndType #14:#11 // b:I
#35 = NameAndType #15:#11 // c:I
#36 = Utf8 com/lfool/myself/Test
#37 = Utf8 java/lang/Object
主要看一下常量池中有哪些我们定义的字面量,可以找到有:200000, 1, 3, abc
根据上面说的「文本字符串、被声明为 final 的常量值」肯定在常量池中,所以1, 3, abc
毫无疑问,那为什么有200000
但没有2
呢???
这就要先回到为静态变量b, c
赋值的地方!!我们知道静态变量是在类加载的初始化阶段赋实际值,详情可见 类加载的过程-初始化
对应的反编译代码如下:
xxxxxxxxxx
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_2
1: putstatic #5 // Field b:I
4: ldc #6 // int 200000
6: putstatic #7 // Field c:I
9: return
LineNumberTable:
line 14: 0
line 15: 4
可以看到,为静态变量b
赋值的字节码指令为:iconst_2
;而为静态变量c
赋值的字节码指令为:ldc #6
,其中#6
是字面量200000
的符号引用
这显然和字节码指令有关,与之有关的字节码有:iconst, bipush, sipush, ldc
。当 int 类型
iconst
指令 bipush
指令sipush
指令ldc
指令iconst 指令
当 int 取值 -1 ~ 5 时,Java 虚拟机使用iconst
指令将常量压入栈中
总结:int 取值 0 ~ 5 时 Java 虚拟机采用iconst_0, iconst_1, iconst_2, iconst_3, iconst_4, iconst_5
指令将常量压入栈中,取值 -1 时采用iconst_m1
指令将常量压入栈中
xxxxxxxxxx
// 源码
public class Test {
public static int a = -1;
public static int b = 5;
}
// 反编译
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_m1
1: putstatic #2 // Field a:I
4: iconst_5
5: putstatic #3 // Field b:I
8: return
LineNumberTable:
line 13: 0
line 14: 4
bipush 指令
当 int 取值 -128 ~ 127 时,Java 虚拟机使用bipush xxx
指令将常量压入栈中,其中xxx
为常量值
xxxxxxxxxx
// 源码
public class Test {
public static int a = -128;
public static int b = 127;
}
// 反编译
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush -128
2: putstatic #2 // Field a:I
5: bipush 127
7: putstatic #3 // Field b:I
10: return
LineNumberTable:
line 13: 0
line 14: 5
sipush 指令
当 int 取值 -32768 ~ 32767 时,Java 虚拟机使用sipush xxx
指令将常量压入栈中,其中xxx
为常量值
xxxxxxxxxx
// 源码
public class Test {
public static int a = -32768;
public static int b = 32767;
}
// 反编译
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: sipush -32768
3: putstatic #2 // Field a:I
6: sipush 32767
9: putstatic #3 // Field b:I
12: return
LineNumberTable:
line 13: 0
line 14: 6
ldc 指令
当 int 取值 -2147483648 ~ 2147483647 时,Java 虚拟机使用ldc #n
指令将常量压入栈中,其中#n
为符号引用
注意:这条指令和前三条指令都不同,该指令的参数是通过符号引用从常量池中获得
xxxxxxxxxx
// 源码
public class Test {
public static int a = Integer.MIN_VALUE;
public static int b = Integer.MAX_VALUE;
}
// 反编译
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #3 // int -2147483648
2: putstatic #4 // Field a:I
5: ldc #5 // int 2147483647
7: putstatic #6 // Field b:I
10: return
LineNumberTable:
line 13: 0
line 14: 5
当 Class 文件被加载到内存后,「常量池」被存放的地方就叫「运行时常量池」,它在方法区中!!
当 Class 文件加载到内存中的时候,有些符号引用被转化为直接引用 (如:静态常量,静态方法等);而有些符号引用只有在每一次运行期间才会转化成直接引用
所以除了保存 Class 文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中!!
「常量池」也被称为「静态常量池」,和「运行时常量池」呼应上了!!所以运行时常量池另外一个重要的特性时具备动态性
Java 语言并不要求常量一定只能编译期才能产生,也就是说,并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量区,运行期间也可以将新的常量放入池中
这种特性被开发人员利用得比较多的便是String
类的intern()
方法,详情可见 字符串常量池