常量池详解

一、概述

JVM的常量池主要有以下几种:

  • class文件常量池
  • 运行时常量池
  • 字符串常量池
  • 基本类型包装类常量池

它们相互之间关系大致如下图所示:

  1. 每个class的字节码文件中都有一个常量池,里面是编译后该class会用到的字面量符号引用,这就是class文件常量池。JVM加载class,会将其类信息,包括class文件常量池置于方法区中。
  2. class类信息及其class文件常量池是字节码的二进制流,它代表的是一个类的静态存储结构,JVM加载类时,需要将其转换为方法区中的java.lang.Class类的对象实例;同时,会将class文件常量池中的内容导入运行时常量池
  3. 运行时常量池中的常量对应的内容只是字面量,比如一个"字符串",它还不是String对象;当Java程序在运行时执行到这个"字符串"字面量时,会去字符串常量池里找该字面量的对象引用是否存在,存在则直接返回该引用,不存在则在Java堆里创建该字面量对应的String对象,并将其引用置于字符串常量池中,然后返回该引用。
  4. Java的基本数据类型中,除了两个浮点数类型,其他的基本数据类型都在各自内部实现了基本类型包装类常量池,但都在[-128~127]这个范围内。

二、class文件常量池

java的源代码.java文件在编译之后会生成.class文件,class文件需要严格遵循JVM规范才能被JVM正常加载,它是一个二进制字节流文件,里面包含了class文件常量池的内容。

三、运行时常量池

  1. 将读入的字节流从静态存储结构转换为方法区中的运行时的数据结构。
  2. 在Java堆中生成该class对应的类对象,代表该class原信息。这个类对象的类型是java.lang.Class,它与普通对象不同的地方在于,普通对象一般都是在new之后创建的,而类对象是在类加载的时候创建的,且是单例。

上述过程的第二步,就包含了将class文件常量池内容导入运行时常量池。class文件常量池是一个class文件对应一个常量池,而运行时常量池只有一个,多个class文件常量池中的相同字符串只会对应运行时常量池中的一个字符串。

运行时常量池除了导入class文件常量池的内容,还会保存符号引用对应的直接引用(实际内存地址)。这些直接引用是JVM在类加载之后的链接(验证、准备、解析)阶段从符号引用翻译过来的。

运行时常量池中保存的“常量”依然是字面量符号引用。比如字符串,这里放的仍然是单纯的文本字符串,而不是String对象。

四、字符串常量池

4.1 字面量赋值创建String对象

我们以下面这个简单的例子来说明使用字面量赋值方法来创建一个String对象的大致流程:

String s = "黄河之水天上来";

当Java虚拟机启动成功后,上面的字符串"黄河之水天上来"的字面量已经进入运行时常量池;

然后主线程开始运行,第一次执行到这条语句时,JVM会根据运行时常量池中的这个字面量去字符串常量池寻找其中是否有该字面量对应的String对象的引用。注意是引用。

如果没找到,就会去Java堆创建一个值为"黄河之水天上来"的String对象,并将该对象的引用保存到字符串常量池,然后返回该引用;如果找到了,说明之前已经有其他语句通过相同的字面量赋值创建了该String对象,直接返回引用即可。

4.2 字符串常量池

字符串常量池,是JVM用来维护字符串实例的一个引用表。在HotSpot虚拟机中,它被实现为一个全局的StringTable,底层是一个c++的hashtable。它将字符串的字面量作为key,实际堆中创建的String对象的引用作为value

字符串常量池在逻辑上属于方法区,但JDK1.7开始,就被挪到了堆区

String的字面量被导入JVM的运行时常量池时,并不会马上试图在字符串常量池加入对应String的引用,而是等到程序实际运行时,要用到这个字面量对应的String对象时,才会去字符串常量池试图获取或者加入String对象的引用。因此它是懒加载的。

4.3 字符串操作

  • 直接赋值字符串

    String s = "abcde";  // s指向常量池中的引用
    

    因为有"abcde"这个字面量,创建对象s的时候,JVM会先去常量池中通过 equals(key) 方法,判断是否有相同的对象。如果有,则直接返回该对象在常量池中的引用;如果没有,则会在常量池中创建一个新对象,再返回引用。

  • new String()

    String s1 = new String("abcde");  // s1指向内存中的对象引用
    

    先检查字面量,再在内存中新建字符串对象

  • intern方法

    String s1 = new String("zhuge");   
    String s2 = s1.intern();
    
    System.out.println(s1 == s2);  //false
    

    最初为空的字符串池 由String类私有维护。
    调用intern方法时,如果池已经包含 通过equals(Object)方法确定 的字符串,则返回池中的字符串否则,将此String对象添加到池中,并返回对此String对象的引用

4.4 字符串常量池是否会被GC

字符串常量池本身不会被GC,但其中保存的引用所指向的String对象们是可以被回收的。否则字符串常量池总是"只进不出",那么很可能会导致内存泄露。

在HotSpot的字符串常量池实现StringTable中,提供了相应的接口用于支持GC,不同的GC策略会在适当的时候调用它们。一般在Full GC的时候,会调用StringTable的对应接口做可达性分析,将不可达的String对象的引用从StringTable中移除掉并销毁其指向的String对象。

4.5 字符串常量池的位置变化

  1. JDK1.7前,运行时常量池+字符串常量池是存放在方法区中。
  2. JDK1.7中,字符串常量池从方法区移到堆中,运行时常量池保留在方法区中。
  3. JDK1.8中,方法区改为元空间,此时字符串常量池保留在堆中,运行时常量池保留在元空间中,JVM实现从JVM内存变成了直接内存
  • 直接内存属于本地系统的IO操作,具有更高的性能,而JVM的堆内存这种,如果有IO操作,也是先复制到直接内存,然后再去进行本地IO操作。经过了一系列的中间流程,性能就会差一些。非直接内存操作:本地IO操作——>直接内存操作——>非直接内存操作——>直接内存操作——>本地IO操作,而直接内存操作:本地IO操作——>直接内存操作——>本地IO操作

  • 永久代有一个无法调整更改的JVM固定大小上限,回收不完全时,会出现OutOfMemoryError问题;而直接内存(元空间)是受到本地机器内存的限制,不会有这种问题。

五、封装类常量池

除了字符串常量池,Java的基本类型的封装类大部分也都实现了常量池。包括Byte,Short,Integer,Long,Character,Boolean,注意,浮点数据类型Float,Double是没有常量池的。

封装类的常量池是在各自内部类中实现的,比如IntegerCache(Integer的内部类),自然也位于堆区。

要注意的是,这些常量池是有范围的:

  • Byte,Short,Integer,Long : [-128~127]
  • Character : [0~127]
  • Boolean : [True, False]

hhhhh