常量池(计数器/字面量/符号引用)
常量池:Class 文件的核心数据区
常量池是 Class 文件中内容最丰富、结构最复杂的区域。它存放了类、接口、方法、字段的所有符号引用,是连接字节码和运行时常量池的桥梁。
常量池的结构
基本结构
┌─────────────────────────────────────────────┐
│ constant_pool_count (u2) │
│ 值为:常量数量 + 1 │
│ (索引 1 ~ count-1 有效,0 保留) │
├─────────────────────────────────────────────┤
│ constant_pool[0] │ ← 不使用
│ constant_pool[1] │
│ constant_pool[2] │
│ ... │
│ constant_pool[constant_pool_count - 1] │
└─────────────────────────────────────────────┘常量池计数
bash
# 查看常量池计数
javap -verbose HelloWorld.class
# 输出:
# Constant pool:
# const #1 = Method ... ; // #2
# const #2 = class ; // #3
# ...
# { ... }
# constant pool size = 15 ← 常量池计数 = 15
# ← 实际常量 14 个(索引 1~14)常量池的 17 种常量类型
JVM 规范定义了 17 种常量类型(JDK 12 增加了 CONSTANT_Dynamic 和 CONSTANT_InvokeDynamic):
| 类型 | 标签 | 说明 |
|---|---|---|
CONSTANT_Utf8 | 1 | UTF-8 编码的字符串 |
CONSTANT_Integer | 3 | 整型字面量 |
CONSTANT_Float | 4 | 浮点字面量 |
CONSTANT_Long | 5 | 长整型字面量 |
CONSTANT_Double | 6 | 双精度字面量 |
CONSTANT_Class | 7 | 类或接口符号引用 |
CONSTANT_String | 8 | 字符串字面量引用 |
CONSTANT_Fieldref | 9 | 字段符号引用 |
CONSTANT_Methodref | 10 | 方法符号引用(非接口) |
CONSTANT_InterfaceMethodref | 11 | 接口方法符号引用 |
CONSTANT_NameAndType | 12 | 字段或方法的名称和类型 |
CONSTANT_MethodHandle | 15 | 方法句柄 |
CONSTANT_MethodType | 16 | 方法类型 |
CONSTANT_Dynamic | 17 | 动态计算常量(JDK 12) |
CONSTANT_InvokeDynamic | 18 | invokedynamic 指令 |
CONSTANT_Module | 19 | 模块引用(JDK 9) |
CONSTANT_Package | 20 | 包引用(JDK 9) |
常见常量类型详解
1. CONSTANT_Utf8(标签 = 1)
存储所有字符串内容,包括类名、方法名、字段名、描述符等:
CONSTANT_Utf8_info {
u1 tag; // 1
u2 length; // 字节长度
u1 bytes[length]; // UTF-8 编码的字节
}2. CONSTANT_Class(标签 = 7)
引用一个类或接口:
CONSTANT_Class_info {
u1 tag; // 7
u2 name_index; // 指向 CONSTANT_Utf8 的索引
}3. CONSTANT_String(标签 = 8)
引用字符串常量(但不是 String 对象本身):
CONSTANT_String_info {
u1 tag; // 8
u2 string_index; // 指向 CONSTANT_Utf8 的索引
}4. CONSTANT_NameAndType(标签 = 12)
描述字段或方法的名称和类型:
CONSTANT_NameAndType_info {
u1 tag; // 12
u2 name_index; // 名称(指向 Utf8)
u2 descriptor_index; // 描述符(指向 Utf8)
}5. 引用类常量(标签 = 9/10/11)
字段引用、方法引用、接口方法引用:
CONSTANT_Fieldref_info {
u1 tag; // 9
u2 class_index; // 指向 CONSTANT_Class
u2 name_and_type_index; // 指向 CONSTANT_NameAndType
}
CONSTANT_Methodref_info {
u1 tag; // 10
u2 class_index; // 指向 CONSTANT_Class
u2 name_and_type_index; // 指向 CONSTANT_NameAndType
}常量池的内部链接
常量池的「表驱动」结构
常量池中的各个常量通过索引相互引用:
常量池示例:
const #1 = class ; // #2
const #2 = Asciz HelloWorld;
↑
这是一个 CONSTANT_Utf8
const #3 = class ; // #4
const #4 = Asciz java/lang/Object;
完整链接:
const #1(Class)→ name_index = #2(Utf8 = "HelloWorld")
const #3(Class)→ name_index = #4(Utf8 = "java/lang/Object")字段引用的完整解析
java
public class User {
private String name;
private int age;
}对应的常量池条目:
#1 = Fieldref ; // #3.#12
#2 = class ; // #4
#3 = NameAndType ; // #5.#6
#4 = class ; // #7
#5 = Utf8 ; name
#6 = Utf8 ; Ljava/lang/String;
#7 = Utf8 ; com/example/User方法描述符(Descriptor)
描述符描述字段和方法的类型信息:
字段描述符
| 类型字符 | Java 类型 |
|---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
Z | boolean |
V | void(仅方法返回) |
L<classname>; | 对象引用,如 Ljava/lang/String; |
[ | 数组,如 [I(int 数组),[[Ljava/lang/Object; |
方法描述符
方法描述符的格式:(参数描述符*)返回描述符
java
// 方法描述符示例
void method()
// → ()V
int add(int a, int b)
// → (II)I
String getName(User user, List<String> names)
// → (Lcom/example/User;Ljava/util/List;)Ljava/lang/String;
boolean isEmpty()
// → ()Z查看描述符
bash
javap -verbose User.class
# 输出(部分):
# const #3 = NameAndType ; #9:#10
# const #4 = Asciz age:I
# const #5 = Asciz name:Ljava/lang/String;
# ...
# { ...
# private int age;
# descriptor: I
# └─ int 类型
# private java.lang.String name;
# descriptor: Ljava/lang/String;
# └─ String 对象引用
# }字面量 vs 符号引用
字面量(Literal)
字面量是源码中直接写出的值:
java
int a = 100; // 整数字面量 100
double d = 3.14; // 浮点字面量 3.14
String s = "hello"; // 字符串字面量 "hello"这些值存储在常量池中:CONSTANT_Integer、CONSTANT_Float、CONSTANT_String。
符号引用(Symbolic Reference)
符号引用是用符号形式描述的引用,不是直接内存地址:
java
public class User {
private String name;
public void setName(String name) { }
}对应的符号引用:
# 字段符号引用
class_index → User 类
name_index → "name"
descriptor_index → "Ljava/lang/String;"(String 对象引用)
# 方法符号引用
class_index → User 类
name_index → "setName"
descriptor_index → "(Ljava/lang/String;)V"(参数 String,返回 void)符号引用 vs 直接引用
编译时(Class 文件中):
使用符号引用(不知道实际内存地址)
运行时(类加载后):
JVM 解析符号引用 → 直接引用(内存地址)常量池的内存占用
常量池是 class 文件的一部分,会被加载到方法区/元空间中:
运行时常量池 vs Class 文件常量池:
┌─────────────────────┐ ┌─────────────────────┐
│ Class 文件常量池 │ │ 运行时常量池 │
│ (磁盘文件) │ │ (JVM 内存中) │
│ │ │ │
│ · 静态存储 │───▶│ · 动态添加(intern) │
│ · 编译时确定 │ │ · 运行时常量 │
└─────────────────────┘ └─────────────────────┘本节小结
常量池的核心要点:
| 维度 | 说明 |
|---|---|
| 位置 | Class 文件的核心区域,存放所有符号引用 |
| 计数 | constant_pool_count = 常量数量 + 1(索引 0 保留) |
| 17 种类型 | Utf8、Class、String、Fieldref、Methodref、NameAndType 等 |
| 描述符 | 字段类型(Z/C/B/I/J/D/F/L...)和方法类型(参数→返回) |
| 字面量 | 编译时常量(100、"hello") |
| 符号引用 | 类、方法、字段的符号形式(非内存地址) |
常量池是理解字节码指令和类加载机制的关键。
下一节,我们来看 访问标识/类索引/父类索引/接口索引。
