JVM(六)JVM常量池

1.常量池类型

Java中的常量池分为三种:

  • 类文件常量池(静态常量池)(The Constant Pool)
  • 运行时常量池 (The Run-Time Constant Pool)
  • String 常量池

在JDK1.7前,运行时常量池逻辑包含字符串常量池,存放在方法区,此时HotSpot对方法区的实现为永久代。
在JDK1.7中,字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆中,运行时常量区还在方法区中。
在JDK1.8中,HotSpot移除了永久代用元空间(MetaSpace)代替,这时候字符串常量池还在堆中,运行时常量池还在方法区,只是方法区用元空间实现。


1.字符串常量池 -----存在于堆中

字符串常量池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中。(string pool中存的是引用值,而不是具体实例对象,具体的实例对象时在堆中开辟的一块空间存放的。)在HotSpot Vm里实现的string pool功能的String Table类,它是一个哈希表,里面存的是驻留字符串(也就是我们用双引号括起来的)引用(而不是实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就被赋予了"驻留字符串"的身份。这个StringTable在每个HotSpot Vm的实例只有一份,被所有类共享。


2.Class常量池(静态常量池)----存在于class文件中

javap -verbose Test.class 可以查看class文件中的常量池。

class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。字面量就是我们所说的常量概念,如文本字符串,被声明为final的常量值等。符号引用时一组符号来描述所引用的目标。符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。一般包括下面三类常量:

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

常量池的每一项常量都是一个表,一共有11中各不相同的表结构数据,每个表开始的第一位都是一个字节的标志位(取值1-12),代表当前这个常量属于哪种常量类型。每种不同类型具有不同的结构。

class文件常量池和运行时常量池关系

当java文件被编译成class文件之后,也就是会生成class常量池。

方法区class文件信息包含内容

可以看到方法区里的class文件信息包括:魔数(用来确定一个文件能否被JVM接受)、版本号、常量池、类、父类、和接口数组、字段、方法等信息。

下面用一张图来表示常量池里存储的内容:


class常量池中存储的内容

3.运行时常量池 -----存在于元空间中

jvm在执行某个类的时候,必须经过加载连接初始化,而连接又分为验证、准备、解析三个阶段。而当类加载到内存后,jvm就会将class常量池中的内容放到运行时常量池中。由此可知,运行时常量吃也是每个类都有一个。class常量池中存的是字面量和符号引用,也就是说它存的并不是实例对象,而是对象的符号引用值,而经过解析之后,也就是把符号引用替换为直接引用,解析过程去查询字符串常量池,也就是我们上面所说的String Table,以保证运行时常量池所引用的字符串与字符串常量池中所引用的是一致的。

class文件常量池和运行时常量池的关系以及区别
class文件常量池存储的是当class文件被虚拟机加载进来后存放在方法区的一些字面量和符号引用。

运行时常量池是当class文件被加载完成后,java虚拟机会将class文件常量池的内容转移到运行时常量池里。在此过程中符号引用有一部分是会被转为直接引用。比如说类静态方法或私有方法,实例构造方法等。这是因为这些方法不能被重写其他版本。而其他一些方法是在方法被第一次调用的时候才会将符号引用转变为直接引用。

运行时常量池相对于class常量池另外一个重要特性是具备动态性。运行期间也可能将新的常量放入池中。这种特性被开发人员利用比较多的是String类的intern()方法。(JDK1.7之前)


4.8种基本类型的包装类和常量池

java中基本类型的包装类,大部分都实现了常量池技术
即Byte,Short,Integer,Long,Character,Boolean,两种浮点型包装类并没有实现常量池技术。
除Boolean外,5中包装类默认创建了数值【-128,127】的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。


//Integer 缓存代码 :
public static Integer valueOf(int i) {
        assert IntegerCache.high >= 127;
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
 Integer i1 = 40;
  Integer i2 = 40;
  System.out.println(i1==i2);//输出TRUE

 Integer i1 = 400;
  Integer i2 = 400;
  System.out.println(i1==i2);//输出FALSE
例子
Integer  i1 = 40;
Integer  i2 = new Integer(40);
System.out.println(i1 == i2); //输出false

Integer i1 == 40;Java在编译的时候会直接将代码封装成Integer.valueOf(40);从而使用常量池技术。
Integer i2 == new Integer(40);这种情况下会创建新的对象。


  Integer i1 = 40;
  Integer i2 = 40;
  Integer i3 = 0;
  Integer i4 = new Integer(40);
  Integer i5 = new Integer(40);
  Integer i6 = new Integer(0);
  
  System.out.println("i1=i2   " + (i1 == i2));
  System.out.println("i1=i2+i3   " + (i1 == i2 + i3));
  System.out.println("i1=i4   " + (i1 == i4));
  System.out.println("i4=i5   " + (i4 == i5));
  System.out.println("i4=i5+i6   " + (i4 == i5 + i6));   
  System.out.println("40=i5+i6   " + (40 == i5 + i6));   


i1=i2   true
i1=i2+i3   true
i1=i4   false
i4=i5   false
i4=i5+i6   true
40=i5+i6   true

语句i4 == i5 + i6,因为+操作符不适用于Integer对象,首先会对i5 和 i6进行拆箱操作。

String类和常量池
String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1 == str2);//false

这两种方式是有差别的,第一种方式是在常量池中获取对象,没有就加入。第二种方式时直接在堆内存空间创建一个新的对象。

连接表达式
1.只有使用双引号包含的文本创建的String对象间 使用 “+”, 产生的新对象才会被加入到字符串池中。
2.对于所有使用new方式创建(包括null)的 “+” 连接表达式,它所产生的新对象都不会被加入字符串池中。

String str1 = "str";
String str2 = "ing";

String str3 = "str" + "ing";
String str4 = str1 + str2;
System.out.println(str3 == str4);//false

String str5 = "string";
System.out.println(str3 == str5); //true
public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
     String s = A + B;  // 将两个常量用+连接对s进行初始化 
     String t = "abcd";   
    System.out.println(s == t);//true
 }

A和B都是常量,值是固定的,它在类编译阶段就已经被替换了。也就是说String s = A + B 等同于String s = "ab"+"cd";


public static final String A; // 常量A
public static final String B;    // 常量B
static {   
     A = "ab";   
     B = "cd";   
 }   
 public static void main(String[] args) {   
    // 将两个常量用+连接对s进行初始化   
     String s = A + B;   
     String t = "abcd";   
     System.out.println(s == t);//false

A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,它们何时被赋予什么样的值,都无法确定。因此A和B在被赋值之前,性质是一个变量。那么s在编译器不能被确定。只能在运行时被创建。

String s1 = new String("xyz")创建了几个对象。
创建了2个对象。
类加载时,"xyz"引用会被放入字符串常量池,堆中会产生一个常量池引用的“xyz”实例对象。
在代码被运行时,new String("xyz")会在堆中产生一个s1实例对象。

public class Test {
 public static void main(String[] args) {   
      String hello = "Hello", lo = "lo";
      System.out.println((hello == "Hello") + " ");
      System.out.println((Other.hello == hello) + " ");
      System.out.println((other.Other.hello == hello) + " ");
      System.out.println((hello == ("Hel"+"lo")) + " ");
      System.out.println((hello == ("Hel"+lo)) + " ");
      System.out.println(hello == ("Hel"+lo).intern());
 }   
}
class Other { static String hello = "Hello"; }
package other;
public class Other { public static String hello = "Hello"; }

true true true true false true```
在同包同类下,引用自同一String对象.
在同包不同类下,引用自同一String对象.
在不同包不同类下,依然引用自同一String对象.
在编译成.class时能够识别为同一字符串的,自动优化成常量,引用自同一String对象.
在运行时创建的字符串具有独立的内存地址,所以不引用自同一String对象.

常量池的好处

常量池为了避免频繁的创建和销毁影响系统性能,实现了对象共享。
例如字符串常量池,在编译阶段就把所有字符串放到一个常量池中,节省内存空间,节省运行时间。

双等号 == 的含义

基本数据类型之间用双等号,比较的是他们的数值。
符合数据类型之间用双等号,比较的是他们在内存中存放的地址。


总结

字符串常量池:每个JVM中只有一份,存放字符串常量的引用值。(StringTable)。

class常量池(静态常量池):在编译时产生,每个class都有。存放是常量的字面量和符号引用。

运行时常量池:类加载完成之后,除了转存class文件常量池里的内容外,还将部分符号引用转变为直接引用。

你可能感兴趣的:(JVM(六)JVM常量池)