1. Class常量池
Class常量池存在于每个Class文件中,它包含了该类在编译期生成的各种字面量和符号引用,这些信息构成了类的元数据。
- 字面量(Literals):这指的是直接量,如文本字符串、final变量定义的数值等,它们是不可改变的数据,只能作为右值出现。例如,String str = “Hello”;中的"Hello"就是一个字面量。
- 符号引用(Symbolic References):这包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。这些信息在编译时生成,用于描述程序中的各种引用。
- 静态常量池:Class文件中的常量池被称为静态常量池,因为它包含的是静态的编译期信息,不会在运行时改变。
2. 运行时常量池
运行时常量池是Class常量池的运行时表示,它属于方法区的一部分。当Class文件被加载到JVM时,Class文件中的常量池信息会被加载到运行时常量池中。
- 加载到方法区:程序运行时,Class文件中的常量池信息加载到方法区,此时符号引用才具有对应的内存地址。
- 动态链接:程序运行过程中,之前的符号引用会被转换为直接引用。这个过程称为动态链接,它确保了引用能够准确地定位到方法区中的目标内存地址。通过这种方式,例如,一个方法调用可以通过直接引用定位到方法区中的具体代码位置并执行。
3. 字符串常量池
字符串常量池是Java堆内存中的一部分,专门用于存储字符串对象。与Class常量池和运行时常量池相类似,字符串常量池的目的是为了优化内存使用和提高性能,但它专门服务于字符串实例。
- 存储机制:
当代码中出现字符串字面量时,JVM首先检查字符串常量池是否已经包含有相同Unicode的字符串对象。如果存在,JVM将返回引用到池中的现有字符串。如果不存在,JVM将创建一个新的字符串对象,放入池中,并返回其引用。
- 编译期与运行期:
在编译期间,字符串字面量被存储在Class文件的常量池中。当Class文件被JVM加载时,相应的字符串字面量会被加载到运行时常量池。如果这些字符串还不存在于堆内存的字符串常量池中,它们会被添加到字符串常量池中。这样,JVM中所有字符串字面量的唯一实例都会被存储在字符串常量池里。
- intern()方法:
Java提供了String.intern()方法,这个方法可以用来将字符串添加到字符串常量池中。如果池已经包含一个等于此String对象的字符串,则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用。
- Java 7的变化:
在Java 7之前,字符串常量池位于方法区的永久代中。从Java 7开始,字符串常量池被移动到了Java堆内,这意味着字符串常量池的大小不再受JVM方法区大小的限制,有助于更灵活地管理内存。
三种字符串操作
① 直接赋值字符串
String s = "zhuge"; //s指向常量池中的引用
这种方式创建的字符串对象,只会在常量池中。
因为有 "zhuge"字面量,创建对象 s的时候,JVM通过 equals(key),判断常量池中是否有相同的对象。true 返回对象在常量池中的引用;false 在常量池中创建一个新对象,再返回引用
② new String()
String s1 = new String("zhuge");//s1指向内存中的对象引用
"zhuge"字面量,相当于 String s = “zhuge”;
这种创建方式会在字符串常量池和堆中都会创建对象,但是只使用堆内存中的引用
既然只使用到堆内存的引用,为什么字符串常量池中还要创建这个对象。作为缓存使用,可能后面会用到。
③ intern()方法
String s1 = new String("zhuge");
String s2 = s1.intern();
System.out.println(s1 == s2); //false
字符串常量池设计原理
字符串常量池底层是hotspot C++实现的,底层类似一个 HashTable,保存的本质上是字符串对象的引用。
结合经典案例分析
String s1 = new String("he") + new String("llo");
String s2 = s1.intern();
System.out.println(s1 == s2);
// 在 JDK 1.6 下输出是 false,创建了 6 个对象
// 在 JDK 1.7 及以上的版本输出是 true,创建了 5 个对象
为什么输出结果不一样?
主要还是字符串池从永久代中脱离、移入堆区的原因, intern()方法也随之发生变化:
① 在 JDK 1.6 中,调用 s1.intern(),JVM通过 equals()方法判断常量池没有 hello字符串,就会复制一份到常量池中。常量池底层是一个 HashTable,key 放的是对象在常量池中的地址,value放的对象值。故 s2指向的是常量池中的key
② 在 **JDK 1.7(及以上版本)**中,字符串常量池在堆中,intern() 做了一些修改,更方便地利用堆中的对象。调用 s1.intern(),JVM通过 equals() 方法判断常量池没有 hello字符串,不再需要重新创建实例,可以直接指向堆上的实例。
基础类型的包装类的缓存常量池
Byte Short Integer Long Character Boolean 的包装类实现了常量池技术,另外两种浮点数类型的包装类则没有实现。
上面5种整型的包装类也只是在对应值小于等于127时才可使用对象池,因为一般这种比较小的数用到的概率相对较大。
public class Test {
public static void main(String[] args) {
//5种整形的包装类Byte,Short,Integer,Long,Character的对象,
//在值小于127时可以使用对象池
Integer i1 = 127; //这种调用底层实际是执行的Integer.valueOf(127),里面用到了IntegerCache对象池
Integer i2 = 127;
System.out.println(i1 == i2);//输出true
//值大于127时,不会从对象池中取对象
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);//输出false
//用new关键词新生成对象不会使用对象池
Integer i5 = new Integer(127);
Integer i6 = new Integer(127);
System.out.println(i5 == i6);//输出false
//Boolean类也实现了对象池技术
Boolean bool1 = true;
Boolean bool2 = true;
System.out.println(bool1 == bool2);//输出true
//浮点类型的包装类没有实现对象池技术
Double d1 = 1.0;
Double d2 = 1.0;
System.out.println(d1 == d2);//输出false
}
}