(图片来源于网络)
池化技术 (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.6中,字符串常量池在永久代,永久代和堆区是隔离的。JDK1.7开始,字符串常量池就在堆中。因此,在JDK1.7以及之后的版本,String.intern() 可以适用于只有有限值并且有限值会被重复利用的场景。
JDK1.6 JDK1.7&JDK1.7+ 实现 把首次遇到的字符串实例复制到常量池中,并返回此引用 把首次遇到的字符串实例的引用添加到常量池中,并返回此引用
如果相关字符串字面量都是首次被执行到,则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,其他类型都存在常量池,当然这些常量池是有缓存范围的。
Byte | Short | Integer | Long | Character | Boolean |
-128 | -128 | -128 | -128 | 0 | True |
127 | 127 | 127 | 127 | 127 | False |