JVM中的常量池

(图片来源于网络)

池化技术 (Pool) 是一种十分常见的编程技巧,通过提前预保存大量的资源,以供后续请求重复使用,在请求量数量级越大时越能明显优化应用性能,降低系统频繁的资源开销。

在JVM中也有相关的池化技术存在,例如:运行时常量池,字符串常量池,基本类型常量池等

JVM常量池概论

JVM的常量池有很多,主要有下述几种:

1:class文件常量池

2:运行时常量池

3:字符串常量池

4:八种基本类型的包装类常量池

在不同的JDK版本中,或者不同的虚拟机中,常量池的实现与分布都是不同的。就比如聊数据库中的事务,首先要定下来是哪种数据库,什么版本的,什么存储引擎,什么事务隔离级别。因此,不限制JDK版本+虚拟机版本,聊常量池是不成立的。本文的总结基于个人最常用的JDK11 + hotspot虚拟机展开,我们引用一个物理概念称其为“标况下”。

标况下常量池的物理分布主要在两个地方,方法区(元空间)和堆中。

方法区中的常量池

在JDK1.8开始hotspot已经将方法区从永久代的实现方式,调整成元空间,即从堆内存迁移到了操作系统的物理内存。默认的情况下,元空间可以只受操作系统本地内存的大小限制,而不需要去单独设置,也可以通过`-XX:MetaspaceSize` 搭配 `-XX:MaxMetaspaceSize`进行设置限制。

-XX:MetaspaceSize

实际设置的并不是JVM元空间的初始空间大小,而是GC触发的最小阈值大小,即达到该值就会触发垃圾收集进行类型卸载。同时GC会对该值进行调整,如果释放了大量的空间,就适当降低该值,如果释放了很少的空间,那么就会提高该元空间的值,但不管怎么提高或增加元空间的值,都不能超过MaxMetaspaceSize所设置的值。

-XX:MaxMetaspaceSize

表示元空间可以达到的最大值,默认是没有限制的(-1),大小完全取决于机器的内存,限制类的元数据使用的内存大小,以免出现虚拟内存切换以及本地内存分配失败。

运行时常量池

运行时常量池是方法区的一部分,即Runtime Constant Pool。对于javac编译的Class文件中有一项重要信息即常量池表(Constant Pool Table),用以存放编译期间生成的各种字面量与符号引用,这些信息(class文件常量池,使用#加数字标记每个“常量”)会在类加载后存放在运行时常量池中。运行时常量池除了有这些导入的class文件常量池的内容,还会保存符号引用对应的直接引用(实际内存地址),这些直接引用是JVM在类加载之后的链接(验证、准备、解析)阶段从符号引用翻译过来的。

同时运行时常量池具有动态特征,其中的内容并不是全部来源于编译后的class文件,在运行时也可以通过代码生成常量并放入运行时常量池中。比如调用String.intern()方法(运行时常量池中保存的“常量”依然是字面量和符号引用,对于String存放的是单纯的文本字符串,而不是String对象)。

Class文件常量池

class文件常量池存放编译期间生成的各种字面量与符号引用,class文件常量池是一个class文件对应一个常量池,而运行时常量池始终只有一个,因此多个class文件常量池中的相同字符串只会对应运行时常量池中的一个字符串。

字面量

字面量就是Java代码中的双引号字符串和常量(final)的实际的值。

public class Demo {
    private final String car = "奥迪";

    private final int count = 7;

    private int year = 2023;
}

上述示例的类,这里的"奥迪",7都是字面量。

主要组成:文本字符串、final修饰的成员变量的值、其他

符号引用

符号引用就是类和接口的全限定名,方法的名称和描述符,以及对其他类的方法的引用和字段的名称和描述符与局部变量

主要组成:类与接口的完全限定名、方法的名称与描述符与对其他类方法的引用、字段名称和描述符与局部变量

堆中的常量池

在JDK1.7中虽然还保留有永久代,但是彼时字符串常量池就从永久代外迁到了堆上,在JDK1.8以及以后的版本中彻底去除了永久代,改用元空间,此时的字符串常量池就彻底在堆上驻足了。

字符串常量池

字符串常量池,是JVM用来维护字符串实例的一个引用表。在HotSpot虚拟机中,它被实现为一个全局的StringTable,底层是一个c++的hashtable。将字符串的字面量作为key,实际堆中创建的String对象的引用作为value。

String的字面量被导入JVM的运行时常量池时,并不会马上试图在字符串常量池加入对应String的引用,而是等到程序实际运行时,要用到这个字面量对应的String对象时,才会去字符串常量池试图获取或者加入String对象的引用,因此字符串常量池是懒加载的。

由于字符串常量池本身是不会被GC的,因此为了防止这种持续增长模式导致的内存泄露,其中保存的引用指向的String对象是可以被回收的。基于不同的GC策略会进行不同的GC方式,一般会在触发了fullGC的时候,会通过可达性分析算法,将不可达的String对象的引用从StringTable中移除掉并销毁其指向的String对象。

字面量加入字符串常量池

String s = "hello world";

// true
System.out.println(s == "hello world");

Java虚拟机启动成功后,字符串"hello world"的字面量就会进入运行时常量池中,然后当程序运行到当前赋值语句的时候,JVM会根据运行时常量池中的这个字面量“hello world”去字符串常量池寻找其中是否有该字面量对应的String对象的引用(内存地址)。如果没找到会在堆中创建一个值为"hello world"的String对象,并将该对象的引用保存到字符串常量池,然后返回该引用。如果可以找到,说明之前已经有其他的执行语句通过相同的字面量“hello world”赋值创建了这个String对象,所以直接返回引用即可。因此此时两个引用指向的相同的,输出true。

new String()加入字符串常量池

String s = new String("hello world");

System.out.println(s == "hello world");

如果“hello world”是首次被执行,则会创建两个String对象。一个是JVM拿到字面量“hello world”去字符串常量池中获取String对象的引用,此时由于第一次执行,找不到引用,会在堆中创建一个String对象,然后把这个引用保存在字符串常量池中并返回。因为使用new String的方式,此时堆中会创建一个与“hello world”等值的String对象,然后返回。此时存在两个对象值都是“hello world”但是引用不同(指向不同的内存地址),因此此时两个引用指向的不相同,输出false。

如果“hello world”不是首次被执行,则会创建一个对象。JVM拿到字面量“hello world”去字符串常量池中获取String对象的引用,由于之前创建过,因此直接返回引用即可。因为使用new String的方式,此时堆中还是会创建一个与“hello world”等值的String对象,然后返回。此时存在新创建的对象值也是“hello world”,但是与字符串常量池中“hello world”字面量的引用不同(指向不同的内存地址),因此此时两个引用指向的不相同,也是输出false。

String.intern()加入字符串常量池

String s1 = new String("hello") + new String(" ") + new String("world");

s1.intern();

String s2 = "hello world";

System.out.println(s1 == s2);

intern()函数,将字符串放入到字符串常量池中,JDK版本有差异实现上有差异。

JDK1.6JDK1.7&JDK1.7+
实现把首次遇到的字符串实例复制到常量池中,并返回此引用把首次遇到的字符串实例的引用添加到常量池中,并返回此引用
由于JDK1.6中,字符串常量池在永久代,永久代和堆区是隔离的。JDK1.7开始,字符串常量池就在堆中。因此,在JDK1.7以及之后的版本,String.intern() 可以适用于只有有限值并且有限值会被重复利用的场景。

如果相关字符串字面量都是首次被执行到,则s3赋值语句会创建7个对象,两个“hello”,两个“ ”,以及两个“world”(其中一个“hello”,一个“ ”和一个“world”在字符串常量池中)和一个“hello world”,而这个“hello world”的引用是没有被保存在字符串常量池的。

基于JDK的版本,JDK1.6判断“hello world”在字符串常量池中不存在,会创建新的"hello world"对象并将其引用保存到字符串常量池,所以输出false。而JDK1.7开始,判断"hello world"在字符串常量池里不存在的话,会直接将s1的引用保存到字符串常量池中,所以输出true。

String s3 = new String("test intern");
s3.intern();

String s4 = "test intern";

System.out.println(s3 == s4);

针对这个场景,无论哪个JDK版本都会输出false。

布局详解

JDK1.6中的内存布局示例

JDK1.7以及以上内存布局示例

封装类常量池

Java的基本类型的封装类大部分也都实现了常量池,除了Float和Double,其他类型都存在常量池,当然这些常量池是有缓存范围的。

ByteShortIntegerLong Character Boolean 
-128-128-128-1280True
127127127127127False

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值