当前位置: 首页 > news >正文

JVM学习笔记-----StringTable

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";类似。

String s4 = s1 + s2;

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

toString()方法:

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

System.out.println( s3 == s4 ); 

是true还是false呢?

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

String s5 = "a" + "b";

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

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);
    s2 是 s.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() 的逻辑是:
    • 先去永久代的常量池找是否有内容匹配的字符串。
    • 如果有,直接返回常量池中的字符串引用;
    • 如果没有,会在常量池新增一个字符串对象(把堆里的字符串内容拷贝到永久代 ),再返回常量池中新对象的引用。
// 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 的文本 ),且仅用几次,入池反而可能浪费常量池空间(常量池回收难 )。这种场景不建议入池,让其在堆里按需创建、及时回收更合理。

http://www.xdnf.cn/news/1317331.html

相关文章:

  • Docker Compose 安装 Neo4j 的详细步骤
  • PostgreSQL导入mimic4
  • go基础学习笔记
  • k8s集群搭建一主多从的jenkins集群
  • Win11 文件资源管理器预览窗格显示 XAML 文件内容教程
  • C++ vector的使用
  • 10 SQL进阶-SQL优化(8.15)
  • 说一下事件委托
  • Java 大视界 -- Java 大数据分布式计算在基因测序数据分析与精准医疗中的应用(400)
  • 【UEFI系列】ACPI
  • 跨越南北的养老对话:为培养“银发中国”人才注入新动能
  • JavaScript 性能优化实战:从评估到落地的全链路指南
  • Spark03-RDD02-常用的Action算子
  • 在鸿蒙中实现深色/浅色模式切换:从原理到可运行 Demo
  • E2B是一个开源基础设施,允许您在云中安全隔离的沙盒中运行AI生成的代码和e2b.dev网站
  • Diamond基础2:开发流程之LedDemo
  • c_str()函数的详细解析
  • 简单的 VSCode 设置
  • (nice!!!)(LeetCode 每日一题) 837. 新 21 点 (动态规划、数学)
  • bash shell 入门
  • 云智智慧停充一体云-allnew全新体验-路内停车源码+路外停车源码+充电桩源码解决方案
  • Rust:DLL 输出对象的生命周期管理
  • API生命周期10阶段
  • 原子操作及基于原子操作的shared_ptr实现
  • Baumer高防护相机如何通过YoloV8深度学习模型实现工作设备状态的检测识别(C#代码UI界面版)
  • 【C++】Windows 下 TCP接口超详介绍,如何实现一个TCP服务端和客户端
  • Windows 10共享打印机操作指南
  • 业务员手机报价软件免费领取——仙盟创梦IDE
  • 精美UI的单页网盘资源分享搜索页面 短剧搜索 自适应页面
  • 飞算JavaAI赋能高吞吐服务器模拟:从0到百万级QPS的“流量洪峰”征服之旅