JVM常量池了解

常量池

Class常量池(class constant pool)

定义:class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。

 

通过javap -v xxx.class 反编译字节码生成可读的JVM字节码指令文件

其中Constant pool指的就是class常量池的信息,常量池中主要存放的是两大类常量:字面量和符号引用

 

字面量

指的是常量概念,如文本字符串、字母、数字以及被声明为final的常量值等。

如:

String a="a";
int b=1;

 

符号引用

符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。

一般包括下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

如上面的两个常量a,b是字段的名称,就是一种符号引用,还有类的全限定名(常量池中的 Lcom/gx/demo/jvm/MyMath),caculate、main都是方法的名称,()V 等都是符号引用。

 

当然,常量池中的信息都是静态信息,只有真正的在运行时被加载到内存后,这些符号才会有对应的内存地址信息,这些常量池一旦被装入内存就变成了运行时常量池,对应的符号引用程序加载或者运行的时候会被转变为被加载到内存区域的代码的直接引用,即动态链接。(符号引用转变为了内存中地址的直接引用,主要是通过对象头里的Klass pointer类型指针去转换直接引用)

 

运行时常量池(runtime constant pool)

java文件被编译为class文件,即产生上面所述的class常量池。

运行时常量池如何产生?

回顾类加载过程:加载(load)>>链接(link)(验证>>准备>>解析)>>初始化(initialize)>>使用>>卸载

而当类被加载内存中的时候,JVM会将class常量池中的内容放到运行时常量池中,因此,几乎每一个类都存在于运行时常量池中。真正的将符号引用变成内存地址的直接引用是发生在解析的时候,解析的过程会去查询全局字符串池,也就是StringTable(字符串常量池的概念),以保证运行时常量池所引用的字符串与字符串常量池中所引用的是一致。

字符串常量池

设计思想

1.字符串对象的分配,和其它的对象分配一样,都需要耗费极大的时间与空间代价。且作为最基础的数据类型之一,大量频繁的创建字符串,极大程度地影响了程序的性能。

2.JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化

  • 为字符串开辟一个字符串常量池,类似于缓存区
  • 当创建一个字符串常量时,首先查询该字符串常量是否已存在于字符串常量池中
  • 当存在该字符串时,返回其引用实例;不存在时,则将该字符串实例化,并放入字符串常量池中

 

三种字符串操作

1.直接赋值

String str = "gx";

直接赋值创建的方式,只会存在于常量池中。str指向的是常量池中的引用

“gx”直接给到了值,当创建对象str时,JVM会先去常量池中通过equals(key)方法,寻找是否有相同的对象。

如果有,则直接返回该对象在常量池中的引用。

如果没有,则会先创建一个新对象于常量池中,再返回其引用。

 

2.new关键字

String str = new String("gx");

由于new对象,所以这种方式会在堆和字符串常量池中都拥有这个对象,没有就会创建,最后再返回堆内存中的对象引用。str指向的是内存中的对象引用

生成步骤:

a.“gx”直接给到了值,于直接赋值一样,先去池中寻找是否存在字符串“gx”。

b.如果不存在,先在字符串常量池中创建一个字符串对象;再去堆内存中创建一个字符串对象“gx”。

c.如果存在,直接去堆内存中创建一个字符串对象“gx”。

d.将内存中的引用返回给变量。

 

3.intern()方法

String str = new String("gx");
String str1 = str.intern();
System.out.println(str == str1); //false

String中的intern方法是一个 native 的方法,当调用 intern方法时,如果池已经包含一个等于此String对象的字符串 用equals(object)方法确定),则返回池中的字符串。否则将被加入池中并返回这个引用给变量。(jdk1.6版本需要将s1 复制到字符串常量池里)。


字符串常量池位置

Jdk1.6及之前: 有永久代, 运行时常量池在永久代,运行时常量池包含字符串常量池

Jdk1.7:有永久代,逐步开始抛弃方法区,将字符串常量池移至堆区.这里jdk文档并没有说运行时常量池是否也跟着移到堆区,也就是说运行时常量依然在方法区

Jdk1.8及之后: 无永久代,运行时常量池在元空间,字符串常量池里依然在堆里

 

设计原理

字符串常量池底层是hotspot的C++实现的,底层类似一个 HashTable, 保存的本质上是字符串对象的引用。

//示例1
String str = new String("gx");
String str1 = str.intern();
System.out.println(str == str1);//false
System.out.println("---------------------");
//示例2
String s1 = new String("a") + new String("bc");
String s2 = s1.intern();
System.out.println(s1 == s2);//true

先说示例2:

依据JDK的版本不同,所涉及创建的string对象多少也不同。

在JDK1.6及之前,因为有永久代的存在,常量池是存在于永久代中的,故此当调用intern()方法,即先去字符串常量池中寻找相等的字符串,如果存在则返回该字符串的引用,否则,会在永久代重新建立一个实例,将StringTable的一个表指向这个新创建的实例。

在JDK1.7及以后,由于字符串池不在存放于永久代了,intern() 有所变动,更方便地利用堆中的对象。字符

串存在时和 JDK 1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例。

因此这里 JDK1.6 创建了6个对象 常量池:“a” "bc" “abc”(abc常量池中没有新建了) 堆:“a” "bc" “abc”

JDK1.7后创建了5个对象 常量池:“a” "bc" (abc常量池中没有指向堆中的) 堆:“a” "bc" “abc”

(注意:是去字符串常量池中寻找)

所以s1.intern() 拿到的是堆中的“abc” 和 s1一致

 

示例1:

同理,str指向的是堆中的引用,str1是获取的字符串常量池中的引用,不一致

 

常见字符串比较问题

String ss1 = "gx";
String ss2 = "gx";
String ss3 = "g"+"x";
System.out.println(ss1 == ss2);//true
System.out.println(ss1 == ss3);//true
//基本都是字符串常量,在编译期间就被确定了。"g"+"x"都是常量,当由多个常量组合的时候,
//它也是个常量,在编译期间会被优化成“gx”
System.out.println("---------------------");
String ss4 = new String("gx");
String ss5 = "g" + new String("x");
System.out.println(ss1 == ss4);//false
System.out.println(ss1 == ss5);//false
System.out.println(ss4 == ss5);//false
//新建的字符串对象,而非常量,在编译期间无法确定,不是放在常量池中,有自己的内存地址。
//ss5也包含新对象,无法确定,是内存地址
System.out.println("---------------------");
String ss6 = "gx1";
String ss7 = "gx" + 1;
System.out.println(ss6 == ss7);//true
String ss8 = "gx1.1";
String ss9 = "gx" + 1.1;
System.out.println(ss8 == ss9);//true
String ss10 = "gxxx";
String ss11 = "gx" + "xx";
System.out.println(ss10 == ss11);//true
//JVM对于字符串常量的"+"号连接,将在程序编译期,JVM就将常量字符串的"+"连接优化为连接后的值
System.out.println("---------------------");
String ss12 = "gxxx";
String ss13 = "xx";
String ss14 = "gx" + ss13;
System.out.println(ss12 == ss14);//false
//字符串引用,由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的
//"gx" + ss13无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给b。所以结果为 false。
System.out.println("---------------------");
String ss15 = "gxxx";
final String ss16 = "xx";
String ss17 = "gx" + ss16;
System.out.println(ss15 == ss17);//true
//final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中

  

编译期间 常量间的 “+” 会被优化,但对象间的“+”等同于stringbuilder 即new string

 

八大基本类型和对象池

(1) int==========>Integer

(2) short========>Short

(3) long========>Long

(4) byte========>Byte

(5) char========>Character

(6) float========>Float

(7) double=======>Double

(8) boolean=======>Boolean

基本类型的包装类型基本都实现了常量池技术(对象池),除了Float和Double两种浮点数类型。

另外实现了的5种包装类型也只能在对应值小于127的时候才会使用对象池,即对象池不负责创建和管理大于127的对象。

Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2);//true
System.out.println("---------------------");
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);//false
System.out.println("---------------------");
Integer i5 = new Integer(127);
Integer i6 = new Integer(127);
System.out.println(i5 == i6);//false
System.out.println("---------------------");
Boolean b1 = false;
Boolean b2 = false;
System.out.println(b1 == b2);//true
System.out.println("---------------------");
Float f1 = 1.1f;
Float f2 = 1.1f;
System.out.println(f1 == f2);//false

  

我始终记住:青春是美丽的东西,而且对我来说,它永远是鼓舞的源泉。——(现代)巴金
原文地址:https://www.cnblogs.com/flyinglion/p/14929017.html