JVM-08-JVM常量池

154 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第12天,点击查看活动详情

JVM常量池

上一节我们介绍完了所有垃圾回收相关的知识,本节将介绍一个非常容易搞混的概念,JVM常量池。JVM里有很多的常量池,比如Class常量池,运行时常量池,字符串常量池等等,本节将对它们进行分析,搞清楚概念以及弄明白它们之间的关系。

Class常量池与运行时常量池

Class常量池可以理解为是Class文件的仓库。

前文提到过Class文件除了包含类的版本,字段,方法等等之外,还有一项就是Class常量池,它用于存放在编译期间生成的各种字面量和符号饮用。以这一段代码为例:

int a = 1;
int b = 2;
Student s = new Student();
s.learn();

字面量是指由字母、数字等构成的字符串或者数值常量。而且字面量只可以以右值的方式出现。如上面这一段代码,1和2就是字面量。

符号引用是值类和接口的全限定名,字段的名称和描述符或者方法的名称和描述符。比如上面这一段代码a、b、s、Student、learn这些都是符号引用。

无论是字面量还是符号引用,它们都是静态的,只有在运行时被加载到内存后才有对应的内存地址信息。也就是说Class常量池被装载到内存后就变成了运行时常量池。

变成运行时常量池后就会将符号引用替换成直接引用,这就是动态链接阶段。比如通过改变对象头中的类型指针,将learn()这个符号变量会转变为learn()这个方法具体代码在内存中的地址。

字符串常量池

String对象和其它所有对象一样,生成会耗费高昂的时间与空间代价,但是这种数据类型频繁地被使用,为了提升性能减少开销,JVM开辟了字符串常量池。

字符串常量池类似于一个缓冲池,在创建字符串常量时,首先会查询字符串常量池中是否有该字符串,如果存在的话则返回引用实例,否则将该字符串放入池中。

字符串一共有三种赋值方法

第一种,直接赋值

String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);

这种方式创建的字符串对象只会在字符串常量池中,创建s1时会去常量池通过equal字面量abc查看是否有相同对象,有的话则返回引用,没有则建立对象再返回。因此这段代码中s1和s2都是指向的字符串常量池中的统一对象,因此返回true。

第二种,new String()

String s1 = new String("abc");

这种方式其实会创建两个对象,一个在堆中创建一个字符串对象“abc”,另一个会查看字符串常量池中有没有字面量为"abc"的对象,如果没有则创建。当然了无论如何最后都是返回堆中的引用。

第三种,intern()

String s1 = new String("abc");
String s2 = s1.intern();
String s3 = "abc";
System.out.println(s1 == s2);
System.out.println(s2 == s3);

这种方法是如果常量池中包含有一个等于此String对象字面量的对象,则返回池里的对象,因此这里s2和s3指向的都是同一个对象。但是要注意如果池中没有,那么返回的引用将指向s1。不过在JDK1.6前,如果字符串常量池中没有,会复制该字面量在池中创建对象并返回池中的引用。

在JDK1.6前,运行时常量池在永久代中,运行时常量池包含字符串常量池。

JDK1.7,字符串常量池分离到堆中,逐步去永久代。

JDK1.8+,运行时常量池在元空间中,字符串常量池仍然在堆中。

下面是一道经典的题目,这段代码到底生成了几个对象

String s1 = new String("abc") + new String("def");
String s2 = s1.intern();
System.out.println(s1 == s2);

这个比较明显的是两个new的对象,两个字面量在池中的对象,一个s1对象。这里就五个了,关键就在于这个intern会不会生成对象。

在JDK1.6前它会在常量池中生成"abcdef"对象,并且将该对象引用返回给s2,那么就是生成了6个对象,并且最后结果为false。

但是在JDK1.7之后它不会再去常量池中生成对象,而是直接返回s1的引用,因此生成了5个对象,并且最后的结果为true。

基本类型包装类常量池

Java的基本类型包装类大部分都实现了常量池。

Byte,Short,Integer,Long,Character这五种整形包装类在对应值小于127时才可以使用常量池,Boolean类型也有常量池,但是两种浮点类没有常量池。

Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2);//true

//new关键词不会使用常量池
Integer i1n = new Integer(127);
Integer i2n = new Integer(127);
System.out.println(i1n == i2n);//false

Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);//false

总结

本节分析了JVM内常量池的情况,包括Class常量池和运行时常量池的区别,详细介绍了字符串常量池的工作原理及作用。合理运用字符串常量池和基本类型包装类常量池能够大大提高项目的效率,并且节约大量的内存空间。

感谢观看!