Tavio's blog Tavio's blog
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Tavio Zhang

努力学习的小码喽
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JVM整体架构
  • JVM String Table
    • 一、String的核心特性:不可变性
      • 1.1 底层存储的不可变设计
      • 1.2 字符串修改的本质
      • 1.3 不可变性的优势
    • 二、JVM中String的存储模型
      • 2.1 存储位置的版本演变
      • 2.2 核心存储结构解析
      • 2.3 结构间的关联逻辑
    • 三、字符串的生命周期:从编译到运行
      • 3.1 编译阶段:记录字面量
      • 3.2 类加载阶段:加载常量到运行时常量池
      • 3.3 运行时解析阶段:创建实例并关联常量池
    • 四、String的创建方式与常量池交互
      • 4.1 字面量赋值:String s = "abc"
      • 4.2 new String("abc"):强制创建新对象
      • 4.3 字符串拼接:new String("a") + new String("b")
      • 4.4 非字面量创建:new String(char[])
      • 4.5 intern():手动入池与引用获取
    • 五、String常量池的性能优化实践
      • 5.1 避免频繁字符串拼接
      • 5.2 合理使用intern()
      • 5.3 空字符串检查优化
      • 5.4 调整常量池参数
    • 六、总结
  • JVM GC
  • JVM 对象的创建与调用
  • 《JVM》笔记
Tavio
2022-02-17
目录

JVM String Table

在Java中,String是使用最频繁的引用类型之一,其设计与JVM的字符串常量池(String Table)密切相关。字符串常量池作为JVM对字符串的核心优化机制,通过复用相同字符串对象大幅减少内存占用,同时保障了字符串操作的高效性。本文将从String的本质特性出发,深入剖析字符串常量池的存储机制、运行流程及实践中的优化策略。

# 一、String的核心特性:不可变性

String的不可变性是JVM设计字符串常量池的基础,其核心表现与实现逻辑如下:

# 1.1 底层存储的不可变设计

  • JDK版本差异:
    JDK 8及之前,String底层通过private final char[] value存储字符(每个字符占2字节);
    JDK 9及之后,改为private final byte[] value(默认使用Latin-1编码,单字节存储普通字符,节省50%内存)。
  • 不可变的保障:
    value数组被final修饰,确保引用不可变;且String类未提供任何修改数组元素的方法(如setCharAt),外部无法直接修改底层数据。

# 1.2 字符串修改的本质

任何对String的"修改"操作(如substring、replace、concat)都会创建新的String实例,原对象保持不变。例如:

String s = "abc";
s = s.concat("d"); // 原"abc"对象未变,创建新对象"abcd"并赋值给s
1
2

# 1.3 不可变性的优势

  • 线程安全:多线程并发访问时,无需额外同步机制,避免数据冲突;
  • 常量池复用:相同字符串可共享同一实例,减少内存消耗;
  • 哈希缓存:hashCode()计算后会缓存到hash字段,避免重复计算(适合作为HashMap的键)。

# 二、JVM中String的存储模型

JVM对字符串的存储涉及运行时常量池和字符串常量池(String Table) 两个核心结构,二者分工明确又紧密协作。

# 2.1 存储位置的版本演变

  • JDK 6及之前:字符串常量池位于永久代(PermGen),运行时常量池作为方法区的一部分也在永久代;
  • JDK 7及之后:字符串常量池迁移至堆内存,运行时常量池仍属于方法区(JDK 8后方法区以元空间Metaspace实现,存储类元信息)。

# 2.2 核心存储结构解析

结构 位置 作用 特性
运行时常量池 元空间(方法区) 存储类的常量信息(包括字符串字面量、数字常量、符号引用等) 每个类独有,随类加载创建
字符串常量池(String Table) 堆内存 全局哈希表,存储字符串对象的引用,实现字符串实例的全局复用 所有类共享,懒加载创建
堆内存(普通对象) 堆内存 存储通过new创建的字符串对象(未入池或未被常量池引用的实例) 无复用,随GC回收

# 2.3 结构间的关联逻辑

  1. 类加载时,.class文件中的编译时常量池(含字符串字面量)被加载到元空间的运行时常量池;
  2. 运行时首次使用字符串字面量时,JVM会通过运行时常量池的字面量去字符串常量池查找对应实例;
  3. 若找到则复用,否则在堆中创建实例并将引用存入字符串常量池,同时更新运行时常量池的字面量为实例引用。

# 三、字符串的生命周期:从编译到运行

字符串从代码编写到实际使用,经历编译→类加载→运行时解析三个阶段,每个阶段的JVM操作如下:

# 3.1 编译阶段:记录字面量

当代码中出现String s = "abc"时:

  • 编译器在.class文件的编译时常量池中记录字面量"abc"(仅文本信息,无对象实例);
  • 生成字节码指令ldc "abc",表示运行时需从当前类的运行时常量池加载该字符串。

# 3.2 类加载阶段:加载常量到运行时常量池

JVM加载类时:

  • 将.class文件的编译时常量池加载到元空间,生成当前类的运行时常量池;
  • 字面量"abc"被存入运行时常量池(仍为文本记录,未关联对象实例)。

# 3.3 运行时解析阶段:创建实例并关联常量池

当JVM执行String s = "abc"时(首次使用该字面量):

  1. 检查运行时常量池,发现"abc"仍是文本记录(未关联对象);
  2. 去字符串常量池查找是否有"abc"的引用:
    • 若存在,直接获取引用(如0x666);
    • 若不存在,在堆中创建"abc"对象(地址0x666),并将引用存入字符串常量池;
  3. 更新运行时常量池的"abc"文本记录为引用0x666(完成符号引用→直接引用的解析);
  4. 将引用0x666赋值给栈中变量s。

阶段交互图示:

┌─────────────────┐    编译    ┌─────────────────┐
│ 源代码          │ ─────────> │ .class文件      │
│ String s = "abc"│            │ 编译时常量池    │
│                 │            │ ("abc"字面量) │
└─────────────────┘            └────────┬────────┘
                                         │ 类加载
                                         ▼
┌─────────────────┐    解析后关联    ┌─────────────────┐
│ 虚拟机栈        │ <───────────── │ 元空间(方法区) │
│ 变量s(引用0x666)│               │ 运行时常量池    │
└─────────────────┘               │ ("abc"→0x666)  │
                                         │ 引用
                                         ▼
                              ┌─────────────────┐
                              │ 堆内存          │
                              │ 字符串常量池    │
                              │ (存储0x666引用)│
                              │ "abc"对象(0x666)│
                              └─────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 四、String的创建方式与常量池交互

不同创建方式会导致字符串与常量池的交互逻辑不同,直接影响对象复用和内存占用。

# 4.1 字面量赋值:String s = "abc"

  • 流程:优先从字符串常量池查找,不存在则创建对象并入池,复用已有引用;
  • 特点:可能复用常量池中的实例,减少对象创建;
  • 示例:
  String s1 = "abc"; 
  String s2 = "abc"; 
  s1 == s2; // true(复用同一实例,地址相同)
1
2
3

# 4.2 new String("abc"):强制创建新对象

  • 流程:
    1. 检查字符串常量池,若"abc"不存在则创建并入池(地址0x666);
    2. 在堆中新创建一个String对象(地址0x777),复制0x666的数据;
    3. 变量引用0x777(与常量池实例无关);
  • 特点:至少创建1个对象(若常量池已有则仅1个,否则2个);
  • 示例:
  String s1 = new String("abc"); 
  String s2 = "abc"; 
  s1 == s2; // false(s1指向堆新对象,s2指向常量池引用)
1
2
3

# 4.3 字符串拼接:new String("a") + new String("b")

  • 流程:
    1. 编译时会被优化为new StringBuilder().append("a").append("b").toString();
    2. toString()方法会创建新String对象"ab"(地址0x888),但不会自动入池;
  • 特点:创建多个临时对象("a"、"b"、StringBuilder、"ab");
  • 示例:
  String s1 = new String("a") + new String("b"); 
  String s2 = "ab"; 
  s1 == s2; // false(s1的"ab"未入池)
1
2
3

# 4.4 非字面量创建:new String(char[])

  • 流程:直接在堆中创建字符串对象,与字符串常量池无交互(不会自动入池);
  • 示例:
  String s1 = new String(new char[]{'a','b','c'}); 
  String s2 = "abc"; 
  s1 == s2; // false(s1未入池,s2指向池内引用)
1
2
3

# 4.5 intern():手动入池与引用获取

intern()方法将当前字符串对象的引用存入字符串常量池,并返回常量池中的引用(JDK 7+特性)。

  • 场景1:常量池已有目标字符串
  String s1 = new String("abc"); // 常量池已有"abc"(0x666),s1指向新对象(0x777)
  String s2 = s1.intern(); // 返回常量池引用0x666
  s1 == s2; // false(0x777 ≠ 0x666)
  s2 == "abc"; // true(均为0x666)
1
2
3
4
  • 场景2:常量池无目标字符串
  String s3 = new String(new char[]{'a','b','c'}); // s3指向0x888(未入池)
  String s4 = s3.intern(); // 将0x888存入常量池,返回0x888
  s3 == s4; // true(均为0x888)
  s3 == "abc"; // true("abc"从常量池取0x888)
1
2
3
4
  • JDK版本差异:
    JDK 6中intern()会复制字符串到永久代的常量池,返回新引用;JDK 7+直接存储堆中对象的引用,避免复制,更高效。

# 五、String常量池的性能优化实践

合理利用字符串常量池可显著减少内存占用和GC压力,关键优化策略如下:

# 5.1 避免频繁字符串拼接

  • 问题:+拼接会编译为StringBuilder操作,频繁拼接(如循环中)会创建大量临时对象;
  • 优化:直接使用StringBuilder并指定初始容量(默认16,扩容会复制数组):
  // 推荐
  StringBuilder sb = new StringBuilder(1024); // 预设足够容量
  for (int i = 0; i < 1000; i++) {
      sb.append(i);
  }
  String result = sb.toString();
1
2
3
4
5
6

# 5.2 合理使用intern()

  • 适用场景:高频重复字符串(如日志关键词、字典词),通过intern()入池复用;
  • 注意:低频字符串入池会浪费常量池空间(常量池对象生命周期长,不易回收)。

# 5.3 空字符串检查优化

  • 用str.isEmpty()代替"".equals(str),避免创建临时空字符串对象;
  • 用Objects.equals(str, "")处理str为null的场景,更安全高效。

# 5.4 调整常量池参数

通过JVM参数-XX:StringTableSize调整字符串常量池的哈希表大小(默认值:JDK 11+为65536),减少哈希冲突,提升查找效率(适用于字符串数量极多的场景)。

# 六、总结

字符串常量池是JVM对String类型的核心优化,其本质是全局哈希表,通过复用相同字符串实例减少内存消耗。

编辑 (opens new window)
#JVM StringTable
上次更新: 2026/01/21, 19:29:14
JVM整体架构
JVM GC

← JVM整体架构 JVM GC→

最近更新
01
订单超时取消
01-21
02
双 Token 登录
01-21
03
长短链接跳转
01-21
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式