Class常量池
什么是Class
常量池?
- 我们写的每一个
Java
类被编译后,就会形成一份Class
文件;Class
文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table
),用于存放编译期生成的各种字面量与符号引用 - 每一个Class文件中都有一个Class常量池
什么是字面量和符号引用?
- 字面量包括:
- 文本字符串
- 声明为
final
的常量 - 八种基本类型的值
- …
- 符号引用包括:
- 类和方法的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
.class
文件都包含哪些内容?
- 创建Test.java
public class Test{
public static void main(String[] args){
System.out.println("AAA");
}
}
- 将Test.java编译为Test.class
javac Test.java
- 反编译Test.class字节码文件
命令:javap -v Test.class
结果如下:
******************************类的描述信息*****************************************
Classfile /Users/gaotengfei/Desktop/Test.class
Last modified 2022-5-11; size 405 bytes
MD5 checksum f1726f37e72972ae216e51eef3a0d281
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
******************************常量池************************************************
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // AAA
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // Test
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Test.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 AAA
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 Test
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
******************************虚拟机中执行编译的方法*****************************************
{
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
//main方法JVM指令
public static void main(java.lang.String[]);
//main方法描述
descriptor: ([Ljava/lang/String;)V
//main方法访问修饰符
flags: ACC_PUBLIC, ACC_STATIC
=========================解释器读取JVM指令解释并执行==========================
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String AAA
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
=========================解释器读取JVM指令解释并执行==========================
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "Test.java"
运行时常量池
什么是运行时常量池?
- 运行时常量池存在于内存中,是方法区的一部分。它是
Class
常量池被加载到内存之后的版本。 - 运行时常量池除了保存
Class
文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。 - 运行时常量池相对于
Class
文件常量池的另一个重要特征是具备动态性,Java语言并不要求常量一定只在编译期才能产生,也就是说,并非预置入Class
文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量池放入池中。它的字面量是可以动态添加的(String
类的intern()
方法),符号引用可以被解析为直接引用。 JVM
在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,JVM
就会将Class
常量池中的内容放到运行时常量池中,因此,每个类都有一个运行时常量池。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是StringTable
,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。
字符串常量池
字符串常量池在Java内存区域的哪个位置?
- 在
JDK6.0
及之前的版本,字符串常量池是放在Perm Gen
区(也就是方法区)中; - 在
JDK7.0
版本,字符串常量被移到了堆中。
字符串常量池是什么?
- 在
HotSpot VM
里实现的String Pool
功能的是一个String Table
类,它是一个Hash
表,底层是HashSet
,默认值大小长度是1009;这个String Table在每个HotSpot VM
的实例只有一份,被所有的类共享。字符串常量是由一个个字符组成,放在了StringTable
上。 - 在
JDK6.0
中,StringTable
的长度是固定的,长度就是1009,因此如果放入String Pool
中的String
非常多,就会造成hash
冲突,导致链表过长,当调用String.intern()
时就需要到链表上一个一个找,从而导致性能大幅度下降。 - 在
JDK7.0
中,StringTable的长度可以通过参数指定:-XX:StringTableSize=66666
字符串常量池里放的是什么?
在之前版本中,里放的都是字符串常量
在中,由于发生了改变,因此中也可以存放放置在堆内的字符串对象的引用。
⚠️字符串常量池中的字符串只存在一份,且被所有线程共享
⚠️全局字符串池里的内容是在类加载完成,经过验证、准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到中;中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。
常量池内存位置演化
- 在
JDK1.7
之前运行时常量池逻辑包含字符串常量池,存放在方法区,此时HotSpot VM
对方法区的实现方式为永久代。
在JDK1.7
字符串常量池和静态变量被从方法区拿到了堆中,运行时常量池剩下的还在方法区,也就是HotSpot的永久代中。
在JDK1.8HotSpot
废除永久代的概念,用元空间(Metaspace
)代替,这时候字符串常量池还在堆中,运行时常量池还在方法区,只不过方法区从永久代变成了元空间(Metaspace
)。
4.关于永久代
- ⚠️在
JDK8
以前,许多Java程序员都习惯在HotSpot
虚拟机上开发、部署程序,很多人更愿意把方法区称呼为“永久代”,或将两者混为一谈。本质上这两者并不是等价的,因为仅仅是当时的HotSpot
虚拟机设计团队把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得HotSpot
的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。对于其他虚拟机,例如BEA、JROCkit、IBM
等是不存在永久代的概念的。 - 永久代这种设计导致Java应用更容易遇到内存溢出的问题(永久代有
- XX:MaxPermSize
的上限,即使不设置也有默认大小,而J9
和JRockit
只要没有触碰到进程可用内存的上限,例如32位系统中的4GB限制,就不会有问题),而且有极少数方法(例如String::intern()
)会因永久代的原因而导致不同虚拟机有不同的表现。 - 在
JDK6
的时候HotSpot
开发团队就有放弃永久代、逐步改为采用本地内存(Native Memory
)来实现方法区的计划。