运行时常量池随类加载动态增长,不占满metaspace;它存储符号引用等元信息,不存对象,且不可被gc回收,仅随类卸载释放。

运行时常量池不是JVM启动时就填满的
很多人以为 java -XX:MaxMetaspaceSize=256m 启动后,方法区里的运行时常量池(Runtime Constant Pool)就立刻占掉一大块内存——其实不是。它随类加载动态增长,每个类的常量池在 ClassLoader.defineClass() 阶段才被解析并写入,而且只存符号引用、字面量、方法句柄等元信息,不存实际对象。
常见错误现象:OutOfMemoryError: Metaspace 经常被误判为“常量池爆了”,其实更可能是重复定义类(如热部署未清理旧 ClassLoader)或大量动态生成类(CGLIB、ASM),而非字符串常量塞太多。
- 字符串字面量(如
"hello")首次使用时才进入常量池,且 JDK 7+ 后存于堆中,不占方法区 -
Integer.valueOf(128)返回的新对象不会进常量池,只有-128 ~ 127范围缓存才复用 - 用
javap -v查看某个 class 的常量池内容,比猜更有依据
运行时常量池和字符串池(String Table)是两回事
环境配置里最常混淆的点:改 -XX:StringTableSize 只影响字符串驻留表(StringTable)的哈希桶数量,跟运行时常量池完全无关。后者大小由类结构决定,无法直接配置参数控制。
使用场景:当你在排查 intern() 调用变慢,或发现 StringTable 占用过高(jstat -gc 中 CCSC 值异常),别去调 MaxMetaspaceSize,该调的是 -XX:StringTableSize=65536(默认 60013,质数)。
立即学习“Java免费学习笔记(深入)”;
- JDK 8 默认
StringTableSize是 60013;JDK 11+ 改为可动态扩容,但初始值仍需合理设置 -
String.intern()在 JDK 7+ 后操作的是堆内字符串池,不是方法区里的运行时常量池 - 运行时常量池里存的是
CONSTANT_Utf8_info结构,不是 Java 对象,不能被 GC 回收
GC 对运行时常量池的影响很有限
运行时常量池属于方法区的一部分,而方法区在 JDK 8+ 是 Metaspace,底层用本地内存管理。这意味着:常量池里的符号引用不会因为堆 GC 就消失,也不会被 CMS 或 G1 的常规 GC 扫描清理。
真正能释放常量池空间的,只有类卸载(class unloading)——前提是该类的 ClassLoader 本身被回收,且无任何强引用指向该类或其实例。这在 Web 容器(如 Tomcat)热部署时极难满足。
- 开启
-XX:+TraceClassUnloading可观察哪些类被真正卸载,别光看Metaspace使用率下降就以为常量池清了 -
-XX:+UseCompressedClassPointers不影响常量池大小,但它压缩的是类元数据指针,间接减少 Metaspace 总用量 - 频繁
new String("abc").intern()不会填满运行时常量池,但可能撑爆StringTable(尤其小StringTableSize时哈希冲突严重)
验证运行时常量池状态得用对工具
靠 jstat -gc 看不到运行时常量池细节,它只显示 Metaspace 整体用量。想确认某个类的常量池是否加载、有没有重复项,必须结合静态分析和运行时探测。
性能影响:反复反射调用 Method.invoke() 会触发常量池中符号引用解析,如果类没预热,首次调用有微小延迟;但解析结果会被缓存,后续无额外开销。
- 用
javap -v MyClass.class | grep "Constant pool"查看编译期常量池结构 - 用
jcmd <pid> VM.native_memory summary scale=MB</pid>观察 Metaspace 实际内存分布,比jstat更准 -
-XX:+PrintGCDetails日志里出现Unloading class net.example.MyHandler才说明对应常量池空间被回收
真正难处理的不是常量池大小本身,而是类加载器生命周期和符号引用解析时机之间的耦合——这点在模块化(JPMS)或 OSGi 场景下尤其容易出问题,但 JVM 参数层面根本没法“一键修复”。










