【JVM原理详解】15-运行时常量池
运行时常量池上一篇我们梳理了方法区的演进。方法区中有一个特殊的数据结构——运行时常量池Runtime Constant Pool它与class文件中的常量池、以及开发者常说的字符串常量池有千丝万缕的联系也常常被混淆。本篇将理清Class常量池、运行时常量池、字符串常量池三者的关系与区别深入讲解String.intern()的动态性和JDK版本差异帮助你彻底弄清这个面试高频考点。三种常量池的关系概念全景图┌──────────────────────────────────────────────────────────┐ │ 编译期 │ │ │ │ Class常量池 (Constant Pool Table) │ │ ┌────────────────────────────────────────────┐ │ │ │ #1 Methodref Object.hashCode │ │ │ │ #2 String hello │ │ │ │ #3 Class Demo │ │ │ │ #4 Utf8 Ljava/lang/String; │ │ │ │ ... │ │ │ └────────────────────────────────────────────┘ │ │ │ 类加载 │ │ ▼ │ ├──────────────────────────────────────────────────────────┤ │ 运行期 │ │ │ │ 运行时常量池 (Runtime Constant Pool) [方法区/元空间] │ │ ┌────────────────────────────────────────────┐ │ │ │ #1 → 直接引用 (Object.hashCode的入口地址) │ │ │ │ #2 → String对象引用 hello │ │ │ │ #3 → Klass指针 (Demo的Class对象) │ │ │ │ ... │ │ │ └────────────────────────────────────────────┘ │ │ │ │ 字符串常量池 (String Pool) [堆, JDK 7] │ │ ┌────────────────────────────────────────────┐ │ │ │ hello → String对象引用 │ │ │ │ world → String对象引用 │ │ │ │ java → String对象引用 │ │ │ └────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────┘Class常量池Class常量池Constant Pool Table是class文件的一部分存在于磁盘上的.class文件中。它在编译期生成存放编译期可知的字面量Literal和符号引用Symbolic Reference。字面量包括文本字符串如hello基本类型值如final int x 10中的10方法/字段的名称和描述符符号引用包括类和接口的全限定名字段的名称和描述符方法的名称和描述符用javap -v查看一个class文件的常量池Constant pool: #1 Methodref #6.#20 // java/lang/Object.init:()V #2 String #21 // hello #3 Fieldref #22.#23 // java/lang/System.out:Ljava/io/PrintStream; #4 Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 Class #26 // Demo #6 Class #27 // java/lang/Object ... #21 Utf8 hello ...关键点Class常量池中的内容都是符号不是真正的Java对象。#2 String #21表示一个字符串常量引用了#21的Utf8条目hello但这里没有实际的String对象。运行时常量池运行时常量池是Class常量池在运行时的内存表示。当class文件被加载到JVM后Class常量池的内容被加载到方法区JDK 8的元空间中形成运行时常量池。运行时常量池相比Class常量池的重要区别是动态性——它可以在运行时添加新内容最典型的就是String.intern()方法。此外运行时常量池中的符号引用在解析阶段会被替换为直接引用方法引用 → 方法的实际入口地址字段引用 → 字段的内存偏移量类引用 → Klass指针Class常量池 (符号): #1 Methodref Object.hashCode 运行时常量池 (解析后): #1 → Object.hashCode的直接入口地址 (0x7f8a2b3c0000)字符串常量池字符串常量池String Pool / String Table是一个专门存储字符串对象引用的数据结构。它是一个哈希表key是字符串的hash值value是指向String对象的引用。字符串常量池的位置随JDK版本变化JDK版本字符串常量池位置JDK 6及以前永久代PermGenJDK 7Java堆JDK 6: ┌────────────────────────────────┐ │ 永久代 │ │ ┌──────────────────────────┐ │ │ │ 运行时常量池 │ │ │ │ ┌──────────────────────┐ │ │ │ │ │ 字符串常量池 │ │ │ │ │ │ hello → String对象 │ │ │ │ │ └──────────────────────┘ │ │ │ └──────────────────────────┘ │ └────────────────────────────────┘ JDK 7: ┌────────────────────────────────┐ │ Java堆 │ │ ┌──────────────────────────┐ │ │ │ 字符串常量池 │ │ │ │ hello → String对象引用 │ │ │ └──────────────────────────┘ │ │ ┌──────────────────────────┐ │ │ │ 普通String对象 │ │ │ └──────────────────────────┘ │ └────────────────────────────────┘ ┌────────────────────────────────┐ │ 元空间 (方法区) │ │ ┌──────────────────────────┐ │ │ │ 运行时常量池 (无字符串池) │ │ │ └──────────────────────────┘ │ └────────────────────────────────┘JDK 7将字符串常量池移到堆中的原因永久代空间有限大量intern()容易OOM字符串常量池的GC与堆一起进行回收更高效为后续移除永久代JDK 8做准备三者关系总结对比维度Class常量池运行时常量池字符串常量池存在位置class文件磁盘方法区/元空间Java堆JDK 7生成时间编译期类加载时运行时动态内容符号引用字面量解析后的直接引用动态内容String对象引用动态性静态动态intern动态intern生命周期永久class文件随类加载/卸载随JVM生命周期一个常见误区字符串常量池不是运行时常量池的一部分JDK 7。它们是两个独立的数据结构只是有协作关系——class文件中的字符串字面量在类加载时会被加入到字符串常量池。String.intern() 的动态性intern()的工作原理String.intern()是一个native方法其行为是如果字符串常量池中已存在equals该字符串的String对象返回常量池中的引用如果不存在将当前String对象的引用加入常量池返回该引用String s1 new String(hello); String s2 s1.intern(); // 如果常量池中没有hello: // s2 s1.intern() → 常量池中存入s1的引用, s2 s1 // 如果常量池中已有hello (如通过字面量hello触发): // s2 常量池中的引用, s2 ! s1 (s1是堆中新对象)JDK 6 vs JDK 7 的intern()差异intern()在不同JDK版本中行为不同这是面试经典考点JDK 6字符串常量池在永久代。intern()时如果常量池没有会复制一份字符串到永久代返回永久代中的引用。JDK 7字符串常量池在堆中。intern()时如果常量池没有只存储当前String对象的引用不复制返回该引用。// 适用: JDK 8/11/17publicclassInternDemo{publicstaticvoidmain(String[]args){// 场景1: 仅intern, 无字面量Strings1newString(hello);// JDK 8: 常量池中存入s1的引用, s2 s1为true?// 实际上: new String(hello)中hello字面量已导致常量池有hello// 所以s1.intern()返回的是之前字面量产生的引用, s2 ! s1Strings2s1.intern();Strings3hello;System.out.println(s1s2);// false (s1是堆中新对象, s2/s3是常量池引用)System.out.println(s2s3);// true (都是常量池引用)// 场景2: 使用StringBuilder构造, 避免字面量预入池Strings4newStringBuilder(world).toString();// 此时world字面量已在常量池(因为StringBuilder的参数)// 改用: String s4 new StringBuilder(wor).append(ld).toString();// 这样wor和ld入池, 但world不在池中Strings5s4.intern();// JDK 7: 常量池存入s4的引用System.out.println(s4s5);// JDK 7: true// JDK 6: false (复制到永久代)}}经典面试题解析// 适用: JDK 8/11/17publicclassInternQuiz{publicstaticvoidmain(String[]args){// 第一组Strings1newString(1);// new String(1)做了两件事:// 1. 1字面量 → 字符串常量池创建1 (假设之前没有)// 2. new String() → 堆中创建新String对象, 内容为1// s1指向堆中的对象, 常量池中有1s1.intern();// 常量池已有1, intern返回常量池引用, 但没有赋给变量, 无影响Strings21;// s2 常量池中的1System.out.println(s1s2);// false (s1是堆对象, s2是常量池引用)// 第二组Strings3newString(2)newString(2);// new String(2) new String(2):// 2字面量入常量池(只一次)// 拼接生成的新String 22在堆中, 不在常量池// s3指向堆中的22Strings4s3.intern();// JDK 7: 常量池没有22, 存入s3的引用, s4 s3// JDK 6: 常量池没有22, 复制到永久代, s4 ! s3Strings522;// s5 常量池中的22// JDK 7: 常量池中的22就是s3的引用, s5 s3// JDK 6: 常量池中的22是永久代中的副本, s5 ! s3System.out.println(s3s4);// JDK 7: true, JDK 6: falseSystem.out.println(s3s5);// JDK 7: true, JDK 6: false}}这组测试的输出在JDK 6和JDK 7中不同JDK 6:false, false, falseJDK 7:false, true, true差异的根源就是JDK 7的intern()存储对象引用而非复制对象。字符串常量池的存储细节底层实现字符串常量池在HotSpot中由StringTable类实现它是一个哈希表默认大小1009JDK 6/ 60013JDK 7。StringTable (哈希表) ┌────┬────┬────┬────┬────┐ │ 0 │ 1 │ 2 │ 3 │... │ ├────┼────┼────┼────┼────┤ │null│ ●──┼──→ │null│ │ └────┘│ │ └────┴────┘ │ │ 链表节点 │ │ ┌──────────────┐ │ └──→ │ hello │ → String对象 │ │ hash99162322│ └─────→ │ world │ → String对象 │ hash11331880│ └──────────────┘JDK 7可以通过-XX:StringTableSize调整哈希表桶数量# 设置字符串常量池大小为1000003 (质数, 减少冲突)java-XX:StringTableSize1000003-jarapp.jar字符串入池的时机字符串进入常量池有几种途径字面量代码中hello这种形式类加载时入池intern()运行时主动入池常量表达式final String s he llo编译期常量折叠为hello入池// 适用: JDK 8/11/17publicclassStringPoolDemo{publicstaticvoidmain(String[]args){Stringahello;// 字面量, 入池Stringbhello;// 常量池已有, 复用System.out.println(ab);// trueStringchello;// 编译期常量折叠为helloSystem.out.println(ac);// trueStringdhe;Stringedllo;// 运行期拼接, 生成新对象System.out.println(ae);// falseStringf(dllo).intern();// intern后回到常量池引用System.out.println(af);// true// final的常量折叠finalStringghe;Stringhgllo;// final变量参与拼接, 编译期折叠System.out.println(ah);// true}}字符串拼接的字节码理解拼接行为需要看字节码Stringdhe;Stringedllo;字节码JDK 80: ldc #2 // String he 2: astore_1 // d he 3: new #3 // class StringBuilder 6: dup 7: invokespecial #4 // StringBuilder.init 10: aload_1 11: invokevirtual #5 // StringBuilder.append(String) 14: ldc #6 // String llo 16: invokevirtual #5 // StringBuilder.append(String) 19: invokevirtual #7 // StringBuilder.toString() 22: astore_2 // e 新String对象d llo在编译期无法确定d的值非final因此编译为StringBuilder.append生成新的String对象。而he llo在编译期就能折叠为hello直接用ldc加载常量池中的hello。JDK 9使用invokedynamic替代StringBuilder优化字符串拼接JEP 280但行为上仍是生成新对象。代码示例观察常量池intern()性能测试// 适用: JDK 8/11/17// 运行: java -XX:StringTableSize10000003 InternPerfimportjava.util.UUID;publicclassInternPerf{publicstaticvoidmain(String[]args){// 生成100万个随机字符串String[]strsnewString[1_000_000];for(inti0;istrs.length;i){strs[i]UUID.randomUUID().toString();}// 测试不intern的内存占用longstartSystem.currentTimeMillis();for(Strings:strs){s.hashCode();// 确保字符串被使用}System.out.println(不intern: (System.currentTimeMillis()-start)ms);// 测试intern的耗时startSystem.currentTimeMillis();for(Strings:strs){s.intern();// 每个字符串都入池}System.out.println(intern: (System.currentTimeMillis()-start)ms);// intern后内存变化可用jmap -histo观察}}intern()的内存优化应用在读取大量重复字符串的场景如日志处理、CSV解析intern()可以节省内存// 适用: JDK 8/11/17publicclassInternMemoryDemo{// 模拟读取大量重复状态码的场景publicvoidprocessLogs(String[]rawStatuses){// 假设rawStatuses中有大量重复的200/404/500// 不intern: 每个状态码都是独立的String对象// 100万条日志, 每条1个状态码 → 100万个String对象// intern: 重复的状态码共享同一个String对象// 100万条日志, 实际只有几种状态码 → 几个String对象for(inti0;irawStatuses.length;i){rawStatuses[i]rawStatuses[i].intern();}}}注意intern()适合重复率高的有限集合字符串。如果字符串大量不重复如UUIDintern反而增加常量池负担。实践要点不要滥用intern()intern()适合重复字符串多的场景如枚举值、状态码。对于大量不同的字符串intern会让StringTable膨胀降低查找性能甚至OOM。-XX:StringTableSize调优如果应用大量使用intern()适当增大StringTableSize如1000003减少哈希冲突。可以通过JMX观察StringTable的使用情况。JDK 6到JDK 7的intern行为差异升级JDK时依赖intern()返回值的比较可能行为变化。虽然不该用比较字符串内容应用equals但遗留代码可能受影响。字符串常量池的GCJDK 7字符串常量池在堆中可以被Full GC回收。如果常量池中的字符串不再被引用包括StringTable的引用可以被GC。但只要StringTable中有引用就不会回收。new String(xxx)创建了几个对象这是一个经典问题。new String(hello)最多创建2个对象——常量池中的hello如果之前不存在和堆中的新String对象。如果hello已在常量池则只创建1个堆中的。答案取决于常量池的初始状态。JDK 9的字符串紧凑存储JEP 254在JDK 9引入了Compact StringsString内部用byte[]而非char[]Latin-1字符只用1字节存储。这减少了50%的字符串内存占用但不改变字符串常量池的行为逻辑。避免String.intern()在并行类的陷阱如果两个不同的类加载器加载了同一个类它们的字符串常量池是共享的StringTable全局唯一。但类的其他元数据是隔离的。这在Tomcat等容器中可能导致意外行为。小结三种常量池各有定位Class常量池磁盘上的符号表编译期生成、运行时常量池方法区/元空间中的运行时表示支持解析和动态添加、字符串常量池堆中的String对象引用哈希表JDK 7从永久代移至堆。运行时常量池的动态性相比Class常量池它可以在运行时通过String.intern()等方法动态添加内容符号引用在解析阶段转为直接引用。String.intern()的JDK差异JDK 6会复制字符串到永久代JDK 7只存储对象引用——这导致new String(xx).intern() new String(xx)在两个版本中结果不同。字符串入池时机字面量类加载时、常量表达式折叠编译期、intern()运行时final变量参与拼接可触发编译期折叠。intern()的使用原则适合高重复字符串场景节省内存不适合大量不同字符串-XX:StringTableSize可调整哈希表大小优化性能。下一篇进入运行时数据区的最后一个区域——直接内存与NIO探讨堆外内存的管理机制。更多内容JVM调优实战

相关新闻

STM32C542输入捕获频率测量:从原理到误差优化的完整指南

STM32C542输入捕获频率测量:从原理到误差优化的完整指南

在实际嵌入式开发中,频率测量是一个常见需求,无论是用于检测传感器输出、通信信号分析还是电机转速监控,都需要准确捕获外部信号的周期或频率。STM32系列微控制器内置的高级定时器,配合输入捕获功能,可以高效地完成这一…

2026/7/25 22:45:17阅读更多 →
10分钟上手MultiScrollDemo:打造惊艳下拉视差效果的完整教程

10分钟上手MultiScrollDemo:打造惊艳下拉视差效果的完整教程

10分钟上手MultiScrollDemo:打造惊艳下拉视差效果的完整教程 【免费下载链接】MultiScrollDemo NestedScrollViewViewPagerRecyclerViewSmartRefreshLayout 打造酷炫下拉视差效果并解决各种滑动冲突 项目地址: https://gitcode.com/gh_mirrors/mu/MultiScrollDemo…

2026/7/25 22:45:17阅读更多 →
如何使用Aibeat进行场景驱动安全评测?新手入门完整指南

如何使用Aibeat进行场景驱动安全评测?新手入门完整指南

如何使用Aibeat进行场景驱动安全评测?新手入门完整指南 【免费下载链接】aibeat Break your AI before they do. 项目地址: https://gitcode.com/gh_mirrors/pr/aibeat Aibeat(GitHub加速计划/pr/aibeat)是面向真实AI Agent的场景驱动…

2026/7/25 22:45:17阅读更多 →
Manim实现阴影特效

Manim实现阴影特效

Manim实现阴影特效:从原理到实践 引言Manim(Mathematical Animation Engine)是由3Blue1Brown开发的强大动画引擎,广泛应用于数学可视化。阴影特效是提升视觉层次感和深度的关键技巧。本文将深入剖析Manim阴影实现的底层原理&#…

2026/7/25 23:55:27阅读更多 →
使用PySide/PyQt实现全国省市区的级联选择组件

使用PySide/PyQt实现全国省市区的级联选择组件

使用PySide/PyQt实现全国省市区的级联选择组件 引言在桌面应用开发中,地区选择是一个常见的功能需求。无论是用户注册、地址填写还是数据筛选,级联选择器都能提供直观、高效的交互体验。PySide/PyQt作为Python生态中成熟的GUI框架,提供了丰富…

2026/7/25 23:55:27阅读更多 →
Krea2高清图像生成工作流:4K/6K本地部署与API集成指南

Krea2高清图像生成工作流:4K/6K本地部署与API集成指南

这次我们来看 Krea2 高清成片链的最新升级,重点不是概念多复杂,而是这套工作流能不能在你的设备上稳定跑出 4K/6K 级别的超高清图像。如果你关心本地部署的显存占用、批量任务效率和接口调用灵活性,这篇文章可以直接收藏。Krea2 是一个基于 C…

2026/7/25 23:55:27阅读更多 →
从Linux到K8S:云原生运维实战入门路径与避坑指南

从Linux到K8S:云原生运维实战入门路径与避坑指南

这类教程最值得先看的不是它覆盖了多少知识点,而是能不能帮你把 Docker 和 Kubernetes (K8S) 这两项云原生运维的核心技术,从“知道名字”变成“能在自己环境里跑起来、管起来”。很多新手卡在第一步:环境装不上、命令看不懂、概念太抽象。这…

2026/7/25 23:55:27阅读更多 →
Jellium Desktop网络连接测试工具:评估服务器连接质量的完整指南

Jellium Desktop网络连接测试工具:评估服务器连接质量的完整指南

Jellium Desktop网络连接测试工具:评估服务器连接质量的完整指南 【免费下载链接】jellium-desktop An unofficial desktop client for Jellyfin 项目地址: https://gitcode.com/GitHub_Trending/je/jellium-desktop Jellium Desktop作为一款非官方的Jellyfi…

2026/7/25 23:55:27阅读更多 →
Linux内核开发实战:从模块编写到系统编译的完整指南

Linux内核开发实战:从模块编写到系统编译的完整指南

最近在整理操作系统开发相关的学习资料时,发现很多朋友对“从零开始构建一个操作系统”既充满向往,又感到无从下手。网上的资料要么过于理论化,要么过于零散,难以形成一条清晰的实践路径。本文将围绕 基于Linux内核的操作系统开发…

2026/7/25 23:53:27阅读更多 →
Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/25 1:01:14阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/25 1:01:14阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/25 1:01:14阅读更多 →
突破文档下载限制:kill-doc让你看到的都能保存

突破文档下载限制:kill-doc让你看到的都能保存

突破文档下载限制:kill-doc让你看到的都能保存 【免费下载链接】kill-doc 看到经常有小伙伴们需要下载一些免费文档,但是相关网站浏览体验不好各种广告,各种登录验证,需要很多步骤才能下载文档,该脚本就是为了解决您的…

2026/7/25 0:01:16阅读更多 →
C++ string类模拟实现:从深拷贝到内存管理的完整指南

C++ string类模拟实现:从深拷贝到内存管理的完整指南

1. 项目概述:为什么我们要“手撕”string类?在C的学习道路上,尤其是从C语言过渡到C的“初阶”阶段,string类绝对是一个绕不开的核心。标准库里的std::string用起来太方便了,、find、substr,几个操作符和函数…

2026/7/25 0:01:16阅读更多 →
三角洲寻宝鼠工具:高效文件搜索与资源管理实战指南

三角洲寻宝鼠工具:高效文件搜索与资源管理实战指南

1. 先搞清楚“三角洲寻宝鼠”到底是什么工具从名称来看,“三角洲寻宝鼠”更像是一个资源查找或文件检索类工具,而不是游戏或娱乐软件。这类工具的核心价值在于帮助用户快速定位特定资源,比如文档、图片、压缩包或特定格式的文件。如果你经常需…

2026/7/25 0:01:16阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/25 23:03:25阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/25 19:03:04阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/25 19:03:04阅读更多 →