首页/新闻资讯/正文详情

【JVM原理详解】15-运行时常量池

发布时间:2026/11/16 4:04:50 来源:云帆数科 栏目:资讯中心
【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调优实战

相关推荐

wxMEdit性能优化:处理32GB大文件时如何减少内存占用
wxMEdit性能优化:处理32GB大文件时如何减少内存占用

wxMEdit性能优化:处理32GB大文件时如何减少内存占用 【免费下载链接】wxMEdit wxMEdit, a Cross-platform Text/Hex Editor, an improved version of MadEdit 项目地址: https://gitcode.com/gh_mirrors/wx/wxMEdit wxMEdit是一款跨平台的文本/十六进制编辑器… · 2026/10/31 21:32:50

【管理科学】第五十六篇 企业管理层的权-责-利益分析及权利-人性-资源限制分析01
【管理科学】第五十六篇 企业管理层的权-责-利益分析及权利-人性-资源限制分析01

字段进行深度扩展,详细列出基础数学方程式、组合数学约束方程式、联动方程式以及传递函数方程式,并同步更新参数列表与关联知识。以下为完善后的表格: 编号 类型 行业 企业类型 企业性质 问题(权利-资源-资质-门槛-规则-知识) 数学分析及企业分析及权利-人性-资源限… · 2026/11/16 4:04:44

AI大模型应用开发实战:从Prompt工程到RAG与工程化部署
AI大模型应用开发实战:从Prompt工程到RAG与工程化部署

如果你是一名开发者,现在想进入AI大模型应用开发领域,最困惑的可能是:我到底该从哪里开始学?是直接啃论文,还是从Python基础学起?是研究Prompt Engineering,还是先搞懂RAG?市面上课程… · 2026/10/3 3:50:13

青原区初级电工证保姆级教程:学历不高怕报不上名?这篇全搞定
青原区初级电工证保姆级教程:学历不高怕报不上名?这篇全搞定

青原区初级电工证保姆级教程:学历不高怕报不上名?这篇全搞定 你是不是也觉得, 学历不高怕报不上名 ?其实,很多人以为报考特种作业证,比如【青原区初级电工证】,必须得有高学历才行,但事实远没有那么复杂。只要你满足基本的条件,哪怕学历不高,也能… · 2026/11/16 4:04:06

奉贤电工证培训点怎么考?外省证能转过来吗?全国通用不是问题
奉贤电工证培训点怎么考?外省证能转过来吗?全国通用不是问题

奉贤电工证培训点怎么考?外省证能转过来吗?全国通用不是问题 之前考的证在外省能不能转过来?这几乎是每个准备考证的人,尤其是从外地来奉贤的小伙伴,最关心的问题。别担心,现在奉贤电工证培训点的证书 全国通用… · 2026/11/16 4:04:06

宜昌电工证新办理流程内部消息全知道
宜昌电工证新办理流程内部消息全知道

宜昌电工证新办理流程内部消息全知道 你现在缺一个电工证,但又找不到门道,怕走弯路、怕花冤枉钱,这在特种作业证办理行业是常态。宜昌电工证新办理流程虽然有官方渠道,但不少小伙伴还是会因为信息不对称,耽误了找工作和上岗时间。下面这条内部消息,是根… · 2026/11/16 4:04:06

下岗后电工证怎么复试2026河南最新政策解读
下岗后电工证怎么复试2026河南最新政策解读

下岗后电工证怎么复试2026河南最新政策解读 工地太忙根本没时间复习考试,这可能是很多下岗后重新进入工地的电工朋友的真实写照。特别是2026年河南最新政策出台后,复审流程和考试内容都发生了变化,很多老电工朋友都感到压力山大。别急,这篇文章就… · 2026/11/16 4:04:06

电工证焊工证怎么验证 你的真实经历告诉你怎么过实操考试
电工证焊工证怎么验证 你的真实经历告诉你怎么过实操考试

电工证焊工证怎么验证 你的真实经历告诉你怎么过实操考试 实操考试心里没底怕挂科?这事儿我懂,去年我刚在洛阳涧西区参加完高压电工证的实操考试,那叫一个紧张。当时我身边好几个同行朋友也是第一次考,个个都像打了鸡血似的,生怕一个操作失误就被淘汰。… · 2026/11/16 4:04:06

最新电工证练题网址实操考试在哪考怎么过
最新电工证练题网址实操考试在哪考怎么过

最新电工证练题网址实操考试在哪考怎么过 你是不是也在担心电工证实操考试心里没底怕挂科?别急,这期内容直接讲透 最新电工证练题网址 ,让你知道 在哪考 、怎么准备、怎么稳过。 一、电工证实操考试到底考什么?怎么准备?… · 2026/11/16 4:03:06

长春初级电工证怎么考?通过率揭秘全在这里
长春初级电工证怎么考?通过率揭秘全在这里

长春初级电工证怎么考?通过率揭秘全在这里 之前考的证在外省能不能转过来?这几乎是每个想从事电工工作的人都会问的问题。特别是那些在外地打拼多年、手里握着一张“外地电工证”的人,到了长春想继续干这一行,第一反应就是“我的证能用吗?”。今天我们就… · 2026/11/16 3:21:55

国内电工证报考条件与考试时间
国内电工证报考条件与考试时间

低压电工证怎么考?学历不高也能报名,官方报名入口全在这 你是不是经常听说“电工证是入行门槛”,但又担心自己学历不够、经验不足,根本报不了名?别急,今天就带你从零了解【国内电工证】的报考流程、考试内容,还有怎么在 官方报名入口… · 2026/11/16 3:01:18

电工作业时如何断电?电子证书到底能不能全国通用?
电工作业时如何断电?电子证书到底能不能全国通用?

电工作业时如何断电?电子证书到底能不能全国通用? 实操考试心里没底怕挂科,特别是电工作业时如何断电这一操作,不仅关系到你能不能拿到证书,更关系到以后能不能顺利上岗。很多人觉得这不过就是“断个电”,但其实这里面的门道可不少,稍有不慎就可能出错… · 2026/11/15 0:00:50

电工证培训的通知在哪考?别再被中介骗了
电工证培训的通知在哪考?别再被中介骗了

电工证培训的通知在哪考?别再被中介骗了 你是不是还在为 电工证培训的通知在哪考 发愁?网上搜了一堆信息,结果要么是挂羊头卖狗肉的培训机构,要么就是打着“包过”旗号的骗子?别急,今天我来给你讲讲, 电工证培训的通知… · 2026/11/16 0:00:34

高级电工证企业认可吗保姆级教程:实操考试心里没底怕挂科?看完这些建议再考不迟
高级电工证企业认可吗保姆级教程:实操考试心里没底怕挂科?看完这些建议再考不迟

高级电工证企业认可吗保姆级教程:实操考试心里没底怕挂科?看完这些建议再考不迟 你是不是也这样,站在高压电工实操考试的设备前,手心冒汗,生怕一个操作失误就被判不合格?实操考试心里没底怕挂科,这是很多准备考高级电工证的人的真实写照。别担心,今天… · 2026/11/16 0:00:34

电工证去哪个部门考核值不值得考 信阳转岗人真实经历分享
电工证去哪个部门考核值不值得考 信阳转岗人真实经历分享

电工证去哪个部门考核值不值得考 信阳转岗人真实经历分享 怕考不过白交培训费?你不是一个人,信阳的李师傅去年刚转岗做电工,就因为没搞清楚电工证去哪个部门考核,差点交了8000块的培训费。别急,下面这份实操考试全攻略,告诉你电工证到底值不值得考… · 2026/11/16 0:00:34

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 1:51:20

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 4:02:19

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 3:02:05

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码