Java 面试宝典:你是如何理解常量池的?

744 阅读8分钟

大家好,我是大明哥,一个专注「死磕 Java」系列创作的硬核程序员。 本文已收录到我的技术网站:skjava.com。有全网最优质的系列文章、Java 全栈技术文档以及大厂完整面经


回答

常量池是 JVM 的一部分,主要用于存储类和接口中的编译时常量以及运行时被解析的引用。它有如下两种:

  • class 文件常量池

class 文件常量池是指在 Java 代码编译成字节码文件(.class文件)时,JVM 会将所有的字面量(如文本字符串、定义为final的常量值)和符号引用(如类和接口的全路径名、字段的名称和描述符、方法的名称和描述符等)存储在.class文件的常量池中。

  • 运行时常量池

当类和接口被加载到 JVM 时,class 文件常量池中的数据会被加载到运行时常量池中。运行时常量池位于 JVM 的方法区。在这个过程中,JVM会对这些常量和符号引用进行解析,将它们转换成直接引用。除此之外,运行时常量池还可以在 Java 运行时动态地将新的常量放入池中,比如 String 的 intern()

扩展

JVM 中其实有四种常量池:

  • Class文件常量池
  • 运行时常量池
  • 字符串常量池
  • 封装类常量池

Class文件常量池

Class文件常量池也是编译时常量池,它是Java类文件结构的一部分。当 Java 源代码文件编译成字节码文件(.class文件)时,Class文件常量池就生成的。在编译源代码到字节码的过程中,编译器会收集该 Java 文件中所有的字面量(如整数、浮点数、字符串等)和符号引用(如类和接口的全限定名、字段和方法的名称及描述符等)并将它们放入Class文件的常量池中。

Class 文件常量池主要包括字面量和符号引用。为了更加直观地看清楚 Class 文件常量池的内容,我们编写一个简单的类:

public class ClassConstantTest {
    private final String name = "大明哥";
    private final int age = 18;
    private String sk = "skjava.com";
    private int salary = 10000000;


    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getSk() {
        return sk;
    }

    public int getSalary() {
        return salary;
    }
}

先通过 javac 命令编译该 Java 文件,然后在通过 javac -v 查看 class 文件信息:

Classfile /Users/chenssy/Documents/workSpace/project-space/skjava-demo/src/main/java/com/skjava/demo/skjavademo/java/jichu/ClassConstantTest.class
  Last modified 2024-2-26; size 719 bytes
  MD5 checksum db91b97f7827c1dcdbd6aab863188316
  Compiled from "ClassConstantTest.java"
public class com.skjava.demo.skjavademo.java.jichu.ClassConstantTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #10.#31        // java/lang/Object."<init>":()V
   #2 = String             #32            // 大明哥
   #3 = Fieldref           #9.#33         // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest.name:Ljava/lang/String;
   #4 = Fieldref           #9.#34         // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest.age:I
   #5 = String             #35            // skjava.com
   #6 = Fieldref           #9.#36         // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest.sk:Ljava/lang/String;
   #7 = Integer            10000000
   #8 = Fieldref           #9.#37         // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest.salary:I
   #9 = Class              #38            // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest
  #10 = Class              #39            // java/lang/Object
  #11 = Utf8               name
  #12 = Utf8               Ljava/lang/String;
  #13 = Utf8               ConstantValue
  #14 = Utf8               age
  #15 = Utf8               I
  #16 = Integer            18
  #17 = Utf8               sk
  #18 = Utf8               salary
  #19 = Utf8               <init>
  #20 = Utf8               ()V
  #21 = Utf8               Code
  #22 = Utf8               LineNumberTable
  #23 = Utf8               getName
  #24 = Utf8               ()Ljava/lang/String;
  #25 = Utf8               getAge
  #26 = Utf8               ()I
  #27 = Utf8               getSk
  #28 = Utf8               getSalary
  #29 = Utf8               SourceFile
  #30 = Utf8               ClassConstantTest.java
  #31 = NameAndType        #19:#20        // "<init>":()V
  #32 = Utf8               大明哥
  #33 = NameAndType        #11:#12        // name:Ljava/lang/String;
  #34 = NameAndType        #14:#15        // age:I
  #35 = Utf8               skjava.com
  #36 = NameAndType        #17:#12        // sk:Ljava/lang/String;
  #37 = NameAndType        #18:#15        // salary:I
  #38 = Utf8               com/skjava/demo/skjavademo/java/jichu/ClassConstantTest
  #39 = Utf8               java/lang/Object
{
  public com.skjava.demo.skjavademo.java.jichu.ClassConstantTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String 大明哥
         7: putfield      #3                  // Field name:Ljava/lang/String;
        10: aload_0
        11: bipush        18
        13: putfield      #4                  // Field age:I
        16: aload_0
        17: ldc           #5                  // String skjava.com
        19: putfield      #6                  // Field sk:Ljava/lang/String;
        22: aload_0
        23: ldc           #7                  // int 10000000
        25: putfield      #8                  // Field salary:I
        28: return
      LineNumberTable:
        line 3: 0
        line 4: 4
        line 5: 10
        line 6: 16
        line 7: 22

  public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #2                  // String 大明哥
         2: areturn
      LineNumberTable:
        line 11: 0

  public int getAge();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: bipush        18
         2: ireturn
      LineNumberTable:
        line 15: 0

  public java.lang.String getSk();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #6                  // Field sk:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 19: 0

  public int getSalary();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #8                  // Field salary:I
         4: ireturn
      LineNumberTable:
        line 23: 0
}

这了的 Constant pool 就是常量池了

字面量

什么是字面量?字面量是在源代码中直接表示值的符号或组合,它可以理解为实际的值,通过源代码直接给出的值。Java 支持八种类型的字面量:intlongfloatdoublebooleancharStringnull

  • 文本字符串

代码中用双引号(" ")包裹的字符串部分的值,如下:

#2 = String             #32            // 大明哥
#32 = Utf8               大明哥

#5 = String             #35            // skjava.com
#35 = Utf8               skjava.com
  • 用 final 修饰的成员变量

如上面的:private final int age = 18;,对应常量池:

#16 = Integer            18

这里有两点需要注意:

  1. 保存在常量池中的是值,也就是上面的"大明哥""18"
  2. 对于非 final 的成员,它们的字面量都不会在常量池中定义。

符号引用

符号引用主要包括:类和接口的全限定名、字段的名称和描述符、方法中的名称和描述符。

  • 类和接口的全限定名
#9 = Class              #38            // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest
#38 = Utf8               com/skjava/demo/skjavademo/java/jichu/ClassConstantTest
  • 字段的名称和描述符
#3 = Fieldref           #9.#33         // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest.name:Ljava/lang/String;
#4 = Fieldref           #9.#34         // com/skjava/demo/skjavademo/java/jichu/ClassConstantTest.age:I
  • 方法中的名称和描述符

即参数类型和返回值

#23 = Utf8               getName
#24 = Utf8               ()Ljava/lang/String;
#25 = Utf8               getAge
#26 = Utf8               ()I

运行时常量池

运行时常量池是方法区的一部分。在Java程序运行期间,每个已加载的类都会有一个对应的运行时常量池,用于存储类中的常量、静态变量以及符号引用。它的主要作用是提供了一种在程序运行时动态访问常量的机制。

它与 Class 文件常量池不同,Class 文件常量池是静态的,我们也可以称之为静态常量池,它是在编译期间生成的,并且存储在类文件中。而运行时常量池是在类加载过程中创建创建的,并存储在 JVM 的方法区中的。

运行时常量池包含了在类加载过程中从类文件中加载的常量信息,包括字面常量和符号引用。

  • 对于字面常量,它们的值会被直接存储在运行时常量池中。
  • 对于符号引用,JVM会将符号引用转换为实际的内存地址或方法指针,并存储在运行时常量池中。

除了这些,在程序运行时,我们还可以通过程序动态生成一些常量假如其中,例如:String#intern()。关于 ntern() 请阅读这篇文章:String 的intern() 原理是怎样的?

字符串常量池

字符串常量池也称之为全局字符串常量池,整个 JVM 就一个。它的主要目的是用于存储字符串常量。

由于字符串的不可变性,使得字符串常量池的实现成为了可能。它会确保相同的字符串常量在内存中只存在一份,即相同的字符串常量只会在常量池中存储一次。

关于字符串常量池推荐看这几篇文章:

String str = new String("abc");创建了几个对象,为什么? String 的intern() 原理是怎样的? String a = "ab"; String b = "a" + "b"; a == b 吗?

关于字符串常量池,大明哥这里补充几点:

  1. 运行时常量池在方法区,JDK 6 ,字符串常量池在永久代,而到JDK 7后,字符串常量池被移到了Java 堆去了。
  2. 字符串常量池是 JVM 所维护的一个字符串实例的引用表,在 HotSpot VM中,它是一个叫做 StringTable 的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现就是一个 Hashtable

封装类常量池

在 Java 中,大部分的基本类型的封装类都实现了常量池。包括ByteShortIntegerLongCharacterBoolean,但是浮点类型FloatDouble是没有常量池的,个人认为是因为浮点数的精度问题导致他们没有必要使用常量池。

  • ByteShortIntegerLong:会存储 -128127 之间的所有常量值。
  • Character:存储 0127之间的所有常量值,包括所有ASCII字符。
  • Boolean:会缓存 truefalse 这两个常量值。

关于封装类常量池推荐看这篇文章:Integer a1 = 100 Integer a2 = 100 , a1 == a2? 原因是什么?