JVM学习笔记-----StringTable

java 复制代码
public static void main(String[] args) {
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
}

常量池最初是存在字节码文件里,当它运行时就会被加载到运行时常量池里(这时a b ab都是常量池中的符号,还没有变成java中的字符串对象)。

等到具体执行到这行代码时

就会找到a符号,把它变成"a"字符串对象

后会准备好一块区域StringTable[],把"a"作为key去StringTable(长度固定不能扩容的hashtable)中去找有没有取值相同的key,没有就会把"a"放入串池。

String s2 = "b";String s3 = "ab";类似。

java 复制代码
String s4 = s1 + s2;

先创建一个StringBuilder对象,调用它的无参构造,aload_1拿到局部变量表中的"a",作为append的参数。b类似,最后调用toString()方法。astore 4就是将转换后的结果存入4号局部变量。

toString()方法:

将拼接好的值又创建了一个新的值为"ab"对象,存人s4。

java 复制代码
System.out.println( s3 == s4 ); 

是true还是false呢?

s3是串池中的,s4是在堆中的,俩个不一样的对象为false。

java 复制代码
String s5 = "a" + "b";

到常量池中找到值为"ab"的符号,就不会创建新的对象,延用并且存入局部变量表中值为5的位置

java 复制代码
System.out.println( s3 == s5 ); 

为true

这为javac在编译期间的优化,结果已经在编译期间确定为"ab"

而String s4 = s1 + s2;中s1和s2为变量,可能被修改,结果不能确定,所以必须在运行期间动态拼接。

jdk7以后

可以调用intern方法将这个字符串对象尝试放入串池

在 Java 7 及以后的版本中,intern() 方法做了优化。当调用 s.intern() 时,如果字符串常量池中不存在对应的字符串,会将堆中字符串对象的引用放入常量池,而不是重新创建一个对象。

所以这里 s 指向的堆中的 "ab" 字符串对象,和字符串常量池中的 "ab" 实际上是同一个对象(在 Java 7 及以后 ),因此 s == "ab" 的比较结果也为 true 。如果是在 Java 6 及以前版本,由于 intern() 方法会在常量池中创建新的字符串对象,那么 s 指向堆中的对象,和常量池中的 "ab" 是不同对象,s == "ab" 结果就会是 false

  • String x = "ab";:直接使用字符串字面值定义,"ab" 会被 JVM 放入字符串常量池x 指向常量池中的该字符串对象。
  • String s = new String("a") + new String("b");:通过 new 创建字符串对象,"a""b" 各自在堆内存生成对象,+ 操作实际由 StringBuilder 辅助完成,最终会在堆内存 生成拼接后的新字符串对象(内容为 "ab" ),s 指向堆里这个新对象。
  • System.out.println(s2 == x);
    s2s.intern() 返回的常量池引用,x 本身就指向常量池的 "ab",二者引用相同,所以结果为 true
  • System.out.println(s == x);
    s 指向堆内存 中拼接产生的 "ab" 对象,x 指向常量池"ab" 对象,二者内存地址不同,所以结果为 false

jdk6

在 JDK 6 中,字符串常量池位于永久代(PermGen),和堆内存是完全独立的内存区域。

  • new String("a") 创建的对象,会直接在堆内存生成。
  • String x = "ab" 这种字面值创建的字符串,会在永久代的常量池生成。
  • s.intern() 的逻辑是:
    • 先去永久代的常量池找是否有内容匹配的字符串。
    • 如果有,直接返回常量池中的字符串引用;
    • 如果没有,会在常量池新增一个字符串对象(把堆里的字符串内容拷贝到永久代 ),再返回常量池中新对象的引用。
java 复制代码
// 1. 字面值定义,"ab" 会直接在【永久代的常量池】生成对象,x 指向常量池的引用
String x = "ab";  

// 2. new String("a") + new String("b") 的过程:
//    - 先在堆里创建 "a"、"b" 两个对象;
//    - 拼接时通过 StringBuilder 生成新的字符串,最终在【堆】里创建内容为 "ab" 的对象,s 指向堆里的这个对象。
String s = new String("a") + new String("b");  

// 3. 调用 s.intern():
//    - JDK 6 中,常量池(永久代)里还没有 "ab"(因为 x 的 "ab" 是在常量池,但这里要注意:x 的 "ab" 是程序启动时就有的吗?不,x 是代码里定义的,会触发常量池生成 "ab"? 不,等一下,代码执行顺序是:
//      - 先执行 String x = "ab":此时常量池(永久代)会生成 "ab" 对象,x 指向常量池的引用。
//      - 再执行 s = new String("a") + new String("b"):堆里生成拼接后的 "ab" 对象。
//      - 然后执行 s.intern():去常量池找 "ab",发现已经存在(因为 x 已经触发常量池创建了 "ab"),所以 s2 直接返回常量池里 x 指向的引用。
String s2 = s.intern();  

// 4. 比较 s2 == x:
//    - s2 是常量池的引用,x 也是常量池的引用 → 地址相同 → 输出 true
System.out.println(s2 == x);  

// 5. 比较 s == x:
//    - s 指向【堆】里的 "ab" 对象,x 指向【永久代常量池】里的 "ab" 对象 → 地址不同 → 输出 false
System.out.println(s == x);  

总结

  1. 常量池特性
    • 常量池中的字符串仅是符号,第一次用到时才变为对象
    • 利用串池的机制,来避免重复创建字符串对象
  2. 字符串拼接原理
    • 字符串变量拼接的原理是 StringBuilder(1.8)
    • 字符串常量拼接的原理是编译期优化
  3. intern 方法
    • 作用:可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
    • JDK 1.8 行为:将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
    • JDK 1.6 行为:将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回

StringTable位置

JDK1.6,StringTable用的非常频繁,永久代垃圾回收要在老年代Full GC时才触发,导致永久代垃圾回收效率并不高,占用大量内存。

调优

1. StringTable 的 HashTable 原理

StringTable 采用哈希表(HashTable)数据结构来存储字符串。当向 StringTable 中添加字符串时,会根据字符串的哈希值来确定其在哈希表中的存储位置。如果多个字符串的哈希值经过计算后,映射到了哈希表的同一个位置,就会产生哈希冲突,此时会通过链表(在 Java 8 中,当链表长度达到一定阈值后会转换为红黑树)来解决冲突。

2. 默认 HashTable 大小及问题

在 Java 8 及以后版本中,StringTable 的默认大小是 60013。当存储的字符串数量不断增加,接近或超过这个默认大小时,哈希冲突的概率就会显著上升。大量的哈希冲突会导致在查找字符串时,需要遍历更长的链表(或树结构),从而增加查找时间,降低性能。

3. 调整 HashTable 大小的方式

可以通过 JVM 参数 -XX:StringTableSize 来调整 StringTable 的大小

如果某些字符串会被大量重复使用 (比如系统固定配置、枚举值、通用提示语 ),通过 intern 入池后,所有引用都会指向常量池同一份对象,能大幅减少堆内存占用 ,还能加速 == 比较(直接比引用 )。

若字符串是运行时动态拼接 / 生成 (如业务编码、缓存 key ),但会被多次使用,可通过 intern 入池。

若字符串体积特别大 (如几 MB 的文本 ),且仅用几次,入池反而可能浪费常量池空间(常量池回收难 )。这种场景不建议入池,让其在堆里按需创建、及时回收更合理。

相关推荐
知识分享小能手1 小时前
React学习教程,从入门到精通, React 属性(Props)语法知识点与案例详解(14)
前端·javascript·vue.js·学习·react.js·vue·react
汇能感知4 小时前
摄像头模块在运动相机中的特殊应用
经验分享·笔记·科技
阿巴Jun4 小时前
【数学】线性代数知识点总结
笔记·线性代数·矩阵
茯苓gao4 小时前
STM32G4 速度环开环,电流环闭环 IF模式建模
笔记·stm32·单片机·嵌入式硬件·学习
是誰萆微了承諾4 小时前
【golang学习笔记 gin 】1.2 redis 的使用
笔记·学习·golang
DKPT5 小时前
Java内存区域与内存溢出
java·开发语言·jvm·笔记·学习
aaaweiaaaaaa5 小时前
HTML和CSS学习
前端·css·学习·html
ST.J5 小时前
前端笔记2025
前端·javascript·css·vue.js·笔记
Suckerbin6 小时前
LAMPSecurity: CTF5靶场渗透
笔记·安全·web安全·网络安全
看海天一色听风起雨落6 小时前
Python学习之装饰器
开发语言·python·学习