java中讲的常量池,通常指的是运行时常量池,它是方法区的一部分,一个jvm实例只有一个运行常量池,各线程间共享该运行常量池。
java内存模型中将内存分为堆和栈,其中堆为线程间共享的内存数据区域,栈为线程间私有的内存区域。堆又包括方法区以及非方法区部分,栈包括本地方法栈、虚拟机栈等,如下图所示:
为什么需要常量池
jvm 在栈帧(frame) 中进行操作数和方法的动态链接(link),为了便于链接,jvm 使用常量池来保存跟踪当前类中引用的其他类及其成员变量和成员方法。
每个栈帧(frame)都包含一个运行常量池的引用,这个引用指向当前栈帧需要执行的方法,jvm使用这个引用来进行动态链接。
在 c/c++ 中,编译器将多个编译期编译的文件链接成一个可执行文件或者dll文件,在链接阶段,符号引用被解析为实际地址。java 中这种链接是在程序运行时动态进行的。
常量池探秘
每个 java 文件编译为 class 文件后,都将产生当前类独有的常量池,我们称之为静态常量池。class 文件中的常量池包含两部分:字面值(literal)和符号引用(Symbolic Reference)。其中字面值可以理解为 java 中定义的字符串常量、final 常量等;符号引用指的是一些字符串,这些字符串表示当前类引用的外部类、方法、变量等的引用地址的抽象表示形式,在类被jvm装载并第一次使用这些符号引用时,这些符号引用将会解析为直接引用。符号常量包含:
-
类和接口的全限定名
-
字段的名称和描述符
-
方法的名称和描述符
jvm在进行类装载时,将class文件中常量池部分的常量加载到方法区中,此时方法区中的保存常量的逻辑区域称之为运行时常量区。
使用javap -verbose
命令可以查看class字节码的详细信息,其中包含了编译期确定的静态常量池。
public class StringTest { public static void main(String[] args){ String s = new String("abc"); String s2 = s.intern(); System.out.println(s2 == s); String s3 = (s + s2); System.out.println(s3 == s3.intern()); } }
上述代码javap -verbose后得到(只拿出常量池部分):
major version: 52 Constant pool: #1 = Methodref #13.#26 // java/lang/Object."<init>":()V #2 = Class #27 // java/lang/String #3 = String #28 // abc #4 = Methodref #2.#29 // java/lang/String."<init>":(Ljava/lang/String;)V #5 = Methodref #2.#30 // java/lang/String.intern:()Ljava/lang/String; #6 = Fieldref #31.#32 // java/lang/System.out:Ljava/io/PrintStream; #7 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #8 = Class #35 // java/lang/StringBuilder #9 = Methodref #8.#26 // java/lang/StringBuilder."<init>":()V #10 = Methodref #8.#36 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #11 = Methodref #8.#37 // java/lang/StringBuilder.toString:()Ljava/lang/String; #12 = Class #38 // StringTest #13 = Class #39 // java/lang/Object #14 = Utf8 <init> #15 = Utf8 ()V #16 = Utf8 Code #17 = Utf8 LineNumberTable #18 = Utf8 main #19 = Utf8 ([Ljava/lang/String;)V #20 = Utf8 StackMapTable #21 = Class #40 //"[Ljava/lang/String;" #22 = Class #27 // java/lang/String #23 = Class #41 // java/io/PrintStream #24 = Utf8 SourceFile #25 = Utf8 StringTest.java #26 = NameAndType #14:#15 //"<init>":()V #27 = Utf8 java/lang/String #28 = Utf8 abc #29 = NameAndType #14:#42 //"<init>":(Ljava/lang/String;)V #30 = NameAndType #43:#44 // intern:()Ljava/lang/String; #31 = Class #45 // java/lang/System #32 = NameAndType #46:#47 // out:Ljava/io/PrintStream; #33 = Class #41 // java/io/PrintStream #34 = NameAndType #48:#49 // println:(Z)V #35 = Utf8 java/lang/StringBuilder #36 = NameAndType #50:#51 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #37 = NameAndType #52:#44 // toString:()Ljava/lang/String; #38 = Utf8 StringTest #39 = Utf8 java/lang/Object #40 = Utf8 [Ljava/lang/String; #41 = Utf8 java/io/PrintStream #42 = Utf8 (Ljava/lang/String;)V #43 = Utf8 intern #44 = Utf8 ()Ljava/lang/String; #45 = Utf8 java/lang/System #46 = Utf8 out #47 = Utf8 Ljava/io/PrintStream; #48 = Utf8 println #49 = Utf8 (Z)V #50 = Utf8 append #51 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #52 = Utf8 toString
我们可以看到,常量池共包含52个常量。#1
是一个类中方法的符号引用,它由#13
和#26
两个utf8编码的字符串构成;#3
是程序中定义的 String 类型的字面值"abc",它包含指向一个utf8编码字符串"abc"的索引 #28
。
方法的调用、成员变量的访问最终都是通过运行时常量池来查找具体地址的。
String 常量池
运行时常量池有一种 String 类型的常量,即通常我们所说的字符串字面值,所有的字符串字面值组成一个 String 常量表。String常量表并不是一成不变的,程序运行时可以动态添加字符串常量,使用String的intern()可以动态的添加String常量。但
jvm 确保两个在值上完全相等的字符串字面值(即其中包含的字符序列是相同的,使用equals()来判断)指向同一个 String 实例。
如:
String s1 ="abc"; String s2 ="abc"; System.out.println(s1 == s2); // true
上述代码中的字符串 s1 和 s2 将指向同一个 String 实例。实际上通过查看class文件,我们可以看到,在编译后,静态常量池中已经包含了一个 String 类型的字面值"abc",程序运行时只是从常量池中获取这个String字面值的引用地址,并赋值给变量 s1 和变量 s2。
Constant pool: #1 = Methodref #6.#19 // java/lang/Object."<init>":()V #2 = String #20 // abc ······ #20 = Utf8 abc public static void main(java.lang.String[]); ······ Code: stack=3, locals=3, args_size=1 0: ldc #2 // String abc 2: astore_1 3: ldc #2 // String abc
其中,ldc
表示将一个常量加载到操作数栈。
String 的 intern() 是一个native方法,返回的是一个String对象的标准表示。当调用该方法时,如果运行时常量池中已经存在与之相等(equal())的字符串,则直接返回常量池中的字符串引用,否则将此字符串添加到池中,并返回。
String s1 ="abc"; String s2 = new String("abc"); System.out.println(s1 == s2); //返回 false System.out.println(s1.equals(s2)); //返回 true System.out.println(s1 == s2.intern()); //返回 true
上述代码中,虽然 s1 和 s2 中的值是相同的,但是他们指向的并不是同一个对象,但 s2 的标准化表示和s1是同一个 String 对象,都是编译期确定的常量池中的"abc"。