目录
在Java虚拟机(JVM)的世界中,常量池是一个备受瞩目的话题。它不仅是内存管理的关键组成部分,还直接影响着程序的性能和资源利用率。在本文中,我们将探讨Java中几种主要常量池的实现方式,以及它们在不同版本JDK中的变化和优化。
Class文件常量池
当Java源文件经过编译后,会生成对应的Class文件。
在Class文件中,开头的4个字节用于存储魔数(Magic Number),用于验证文件是否为JVM可接受的格式;紧接着的4个字节用于存储版本号,其中前2个字节表示次版本号,后2个字节表示主版本号;常量池紧随其后,用于存放常量。在Java虚拟机加载Class文件时,这些信息会被存储在方法区。
常量池主要包括两种类型的常量:字面量,类似于Java语言层面的常量,例如文本字符串和声明为final的常量值;符号引用,属于编译原理的概念。通过javap命令生成更可读的JVM字节码指令文件:
字面量
在Java中,字面量指的是字符串常量或数值常量,它们只能作为右值出现,也就是等号右边的值。例如,在表达式 int a=1 中,1 就是字面量。
符号引用
符号引用是编译原理中的概念,相对于直接引用而言。主要包括三类常量:类和接口的全限定名,字段名称和描述符,方法名称和描述符。举例来说,在之前提到的表达式中,a 就是一个字段名称,也是一种符号引用。
运行时常量池
运行时常量池也是方法区的一部分。当Class文件被加载到内存后,Java虚拟机会将其中的内容转移到运行时常量池中,对应的符号引用会被转变为内存中代码的直接引用,这就是动态链接。
由于Java语言并不要求常量只能在编译期产生,因此不是只有预置入Class文件中常量池的内容才能进入方法区的运行时常量池,在运行期间,也可以将新的常量放入池中。
字符串常量池
设计思想
在我们的工作中,String类是一种极其常用的对象类型,因此为了提高性能和节省内存,JVM维护了一个特殊的内存空间,即字符串常量池。这类似于缓存区,在创建字符串常量时,首先会检查字符串常量池中是否存在该字符串,如果存在则返回引用实例,如果不存在,则会在常量池中创建一个新对象,再返回引用。
但是,值得注意的是,字符串池的实现前提是String对象是不可变的。这样可以确保多个引用同时指向字符串池中的同一个对象。如果字符串是可变的,一个引用的操作改变了对象的值,会对其他引用产生影响,这是不合理的。
字符串创建
在创建字符串对象时,有几种不同的方式。
字面量方式
使用字面量方式创建的字符串对象只会存在于常量池中。在创建对象时,JVM会首先在常量池中通过equals(key)方法判断是否存在相同的对象。如果存在,则直接返回该对象在常量池中的引用;如果不存在,则会在常量池中创建一个新对象,再返回引用。
new String()
另一种方式是使用new String()关键字,这会在堆中创建一个对象,同时也会存在于字符串常量池中。
JVM首先会在字符串常量池中查找是否存在相同的字符串对象,如果有,则不在池中再去创建,而是直接在堆中创建一个对象,然后返回堆中对象的引用。如果没有,则首先在字符串常量池中创建一个字符串对象,然后再在堆中创建一个相同的对象,最后返回堆中对象的引用。
intern()
另外,还有一个方法是使用intern()方法。这是一个本地方法,调用intern()方法时,如果字符串在字符串常量池中存在对应的字面量,则返回该字面量的地址;如果不存在,则创建一个对应的字面量,并返回其地址。
字符串常量池位置变迁
-
Jdk1.6及以前运行时常量池在永久代,其中包含字符串常量池;
-
Jdk1.7字符串常量池从永久代里的运行时常量池分离到堆中;
-
Jdk1.8及之后运行时常量池在元空间,字符串常量池里仍然在堆里;。
总结
字符串常量池的存在避免了频繁创建相同内容的字符串,从而节省了内存,并减少了创建相同字符串耗费的时间,提升了性能。同时字符串常量池牺牲了在常量池中遍历对象所需的时间,不过这种时间成本相对较低。
对象池
对于Java中基本类型的包装类,大部分都实现了常量池技术(严格来说应该称为对象池,在堆上),包括Byte、Short、Integer、Long、Character、Boolean,而另外两种浮点数类型的包装类则没有实现。
此外,Byte、Short、Integer、Long、Character这五种整型的包装类只有在对应值小于等于127时才可以使用对象池,也就是说,对象池不负责创建和管理大于127的这些类的对象。这是因为一般来说,这些较小的数值被使用的概率相对较大。如果超出了对应范围,仍然会去创建新的对象。
欢迎关注微信订阅号:技术勘察馆