Skip to content

方法区内部结构(常量池/运行时常量池)

方法区内部结构一览

方法区(元空间)内部包含多个组成部分,其中运行时常量池是最核心的部分之一。

┌──────────────────────────────────────────────────┐
│                  方法区 / 元空间                       │
│                                                      │
│  ┌────────────────────────────────────────────┐  │
│  │  类信息                                       │  │
│  │  - 类的修饰符、全限定名、父类名                │  │
│  │  - 实现的接口列表                            │  │
│  │  - 字段表(修饰符、类型、名称)               │  │
│  │  - 方法表(修饰符、返回类型、字节码)         │  │
│  └────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────┐  │
│  │  运行时常量池(Runtime Constant Pool)          │  │
│  │  - 字面量(字符串、数值)                     │  │
│  │  - 符号引用(类、字段、方法的符号引用)       │  │
│  │  - 方法句柄(MethodHandle)                  │  │
│  │  - 方法类型(MethodType)                   │  │
│  └────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────┐  │
│  │  代码缓存(Code Cache)                        │  │
│  │  - JIT 编译后的本地机器码                    │  │
│  │  - 即时编译器管理的数据结构                   │  │
│  └────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────┐  │
│  │  其他信息:域引用、方法表、内部结构等           │  │
│  └────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────┘

常量池的两个阶段

常量池经历了两个阶段:Class 文件常量池运行时常量池

Class 文件常量池

编译阶段,.class 文件中包含一个静态的常量池。它是一个表结构,包含字面量和符号引用:

bash
# 用 javap 查看 Class 文件常量池
javap -verbose MyClass.class | grep -A 50 "Constant pool"
java
Constant pool:
   #1 = Methodref          #4.#13        // java/lang/Object."<init>":()V
   #2 = String             #14            // hello
   #3 = Integer            100
   #4 = Class              #15            // java/lang/Object
   #5 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = Methodref          #18.#19        // java/io/PrintStream.println:(Ljava/lang/String;)V

运行时常量池

Class 文件常量池被加载到内存后,变成运行时常量池(存放在方法区/元空间中)。运行时常量池是方法区的一部分,它在类加载后存储真正的直接引用

java
public class RuntimeConstantPool {
    public static void main(String[] args) {
        // "hello" 这个字符串,在运行时常量池中有对应的 entry
        // 符号引用 #2 已经被解析成直接的内存地址
        String s = "hello";

        // Integer 100 的包装类型
        Integer i = 100;  // 自动装箱,使用运行时常量池中的常量
    }
}

运行时常量池的动态性

运行时常量池与 Class 文件常量池最大的区别是具有动态性——运行时常量池可以在运行时添加新的常量。

String.intern():向常量池添加字符串

String.intern() 是向运行时常量池添加字符串的主要方式:

java
public class InternDemo {
    public static void main(String[] args) {
        // intern() 的作用:
        // 1. 如果字符串常量池中已有该字符串,返回常量池中的引用
        // 2. 如果没有,把该字符串加入常量池,返回常量池中的引用

        String s1 = new String("hello");
        String s2 = s1.intern();

        // s1 是堆中 new 出来的对象
        // s2 是常量池中的字符串引用
        System.out.println(s1 == s2);  // false(s1 在堆,s2 在常量池)

        String s3 = "hello";  // 字面量在编译时就进入常量池
        String s4 = "hello";
        System.out.println(s3 == s4);  // true(都是常量池中的同一个引用)

        // intern() 的使用场景:节省内存
        String[] arr = new String[1000000];
        for (int i = 0; i < 1000000; i++) {
            // 每个 new String() 都在堆上分配对象
            // intern() 可以让多个引用共享常量池中的字符串
            arr[i] = new String("hello").intern();
        }
    }
}

JDK 6 vs JDK 8 中 intern() 的区别

这是面试经典题,JDK 6 和 JDK 8 对 intern() 的实现有重大差异:

java
public class InternDifference {
    public static void main(String[] args) {
        String s1 = new StringBuilder("ja").append("va").toString();
        // JDK 6:intern() 把字符串复制到 PermGen 的常量池
        //         s1 和 s2 是不同的对象
        // JDK 7+:intern() 只在常量池中记录字符串的引用
        //         s1 和 s2 是相同的对象(堆中的同一个对象)
        String s2 = s1.intern();
        System.out.println(s1 == s2);  // JDK 6: false, JDK 7+: true
    }
}

核心原因:JDK 7 把字符串常量池移到了堆中。

常量池的 GC 回收

方法区的 GC 主要回收两部分:类的卸载常量池的垃圾回收

常量池中的常量回收

运行时常量池中的常量,如果没有任何地方引用,也会被 GC 回收:

java
public class ConstantGC {
    public static void main(String[] args) {
        // 大量创建字符串,这些字符串可能被 GC 回收
        for (int i = 0; i < 100000; i++) {
            String s = "generated_" + i;
            // 如果这些生成的字符串没有被任何地方长期持有
            // 它们可能会被常量池 GC 回收
        }
    }
}

本节小结

方法区内部结构的核心要点:

组成部分说明
类信息类的元数据、字段表、方法表
运行时常量池字面量、符号引用、动态添加的常量
代码缓存JIT 编译后的机器码
运行时常量池的动态性可以通过 String.intern() 动态添加

运行时常量池是理解字符串池化、intern() 机制、符号引用解析的关键。

下一节,我们来看 StringTable 位置调整与验证

基于 VitePress 构建