字符串常量池
String 基本特性
1、String:字符串,使用一对 "" 引起表示
2、String 声明为 final 的,不可被继承
3、String 实现 Serializable 接口:表示字符串是支持序列化
4、String 实现 Comparable 接口:表示 String 可以比较大小
5、String 在 JDK 8 及以前,内部定义 final char[] value 用于存储字符串数据,JDK 9 时改为 byte[]
String 在 JDK 9 中存储结构变更
1、String 不用 char[] 存储,改成 byte[] 加上编码标记,节约空间
2、动机
(1)目前 String 类的实现将字符存储在一个 char 数组中,每个字符使用两个字节(16 位)
(2)从许多不同的应用中收集到的数据表明,字符串是堆使用的主要组成部分
(3)大多数字符串对象只包含 Latin-1 字符,这些字符只需要一个字节的存储空间,因此这些字符串对象的内部字符数组中有一半的空间没有被使用
3、说明
(1)建议将 String 类的内部表示方法从 UTF-16 字符数组,改为字节数组 + 编码标志域
(2)新 String 类将根据字符串的内容,以 ISO-8859-1 / Latin-1(每个字符一个字节)或 UTF-16(每个字符两个字节)方式存储字符编码
(3)编码标志将表明使用的是哪种编码
(4)与字符串相关的类,如 AbstractStringBuilder、StringBuilder、StringBuffer 将被更新以使用相同的表示方法,HotSpot VM 的内在字符串操作相同
(5)是一个实现上的变化,对现有的公共接口没有变化,没有计划增加任何新的公共 API 或其他接口
(6)证实内存占用的预期减少,GC 活动的大幅减少,以及在某些角落情况下的轻微性能倒退
String
1、不可变性:代表不可变的字符序列
(1)当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的 value 进行赋值
(2)当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值
(3)当调用 String 的 replace() 方法,修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值
(4)Java 语言规范:要求完全相同的字符串字面量,应该包含同样的 Unicode 字符序列(包含同一份码点序列的常量),并且必须是指向同一个 String 类实例
2、字符串常量池
(1)通过字面量的方式(区别于 new)给一个字符串赋值,堆中保存字符串常量,字符串常量池保存字符串地址
(2)不会存储相同字符串的引用
(3)一个固定大小的 HashTable,默认值大小长度是 1009
(4)若存储 String 非常多,就会造成 Hash 冲突严重,导致链表会很长,当调用 String.intern 时性能会大幅下降
3、使用 -XX:StringTablesize 可设置字符串常量池的长度
(1)在 JDK 6 中,大小固定,即 1009 长度,如果常量池中的字符串过多,就会导致效率下降很快,StringTablesize 设置没有要求
(2)在 JDK 7 中,长度默认值是 60013,StringTablesize 设置没有要求
(3)在 JDK 8 中,1009 是可以设置长度的最小值
String 内存分配
1、在 Java 语言中有 8 种基本数据类型、一种特殊类型 String
(1)为了使它们在运行过程中速度更快、更节省内存,提供一种常量池的概念
(2)常量池类似一个 Java 系统级别提供的缓存,8 种基本数据类型的常量池都是系统协调
2、String 类型的常量池较特殊,主要使用方法有两种
(1)直接使用 "" 声明的 String 对象存储在堆中,字符串常量池保存其引用
(2)如果不是使用 "" 声明的 String 对象,可以使用 String 提供的 intern() 方法
3、存储位置
(1)Java 6 及以前,字符串常量池存放在永久代
(2)Java 7 及以后,将字符串常量池的位置调整到 Java 堆内,所有字符串都保存在堆(Heap)中,调优应用时仅需要调整堆大小
4、调整原因
(1)在 JDK 7 中,内部字符串不再分配在 Java 堆的永久代中,而是分配在 Java 堆的主要部分:年轻代、老年代,与应用程序创建的其他对象一起,导致更多的数据驻留在主 Java 堆中,而更少的数据在永久代中,因此可能需要调整堆的大小
(2)加载许多类或大量使用 String.intern() 方法的大型应用程序将看到更明显的差异
字面量进入字符串常量池的时机
1、在类加载阶段, JVM 会在堆中创建对应这些 class 文件常量池中的字符串对象实例 ,并在字符串常量池中驻留其引用,具体在解析阶段执行,这些常量全局共享
2、JVM 规范里明确指定解析阶段可以懒加载
(1)JVM 规范里Class文件的常量池项的两种类型:CONSTANT_Utf8、CONSTANT_String
(2)CONSTANT_String 是 String 常量的类型,但它并不直接持有 String 常量的内容,而是只持有一个index,这个 index 所指定的另一个常量池项必须是一个 CONSTANT_Utf8 类型的常量,CONSTANT_Utf8 才真正持有字符串的内容
(3)在 HotSpot VM 的运行时常量池中,CONSTANT_Utf8 -> Symbol*(一个指针,指向一个 Symbol类型的 C++ 对象,内容是跟 Class 文件同样格式的 UTF-8 编码的字符串);CONSTANT_String -> java.lang.String(一个实际的 Java 对象的引用,C++ 类型是 oop)
(4)CONSTANT_Utf8 会在类加载的过程中就全部创建出来,而 CONSTANT_String 则是 lazy resolve,例如:在第一次引用该项的 ldc 指令才会 resolve,那么在尚未 resolve 时,HotSpot VM 把它的类型叫做 JVM_CONSTANT_UnresolvedString,内容跟 Class 文件里一样只是一个 index,等到 resolve 后这个项的常量类型就会变成最终的 JVM_CONSTANT_String,而内容则变成实际的 oop
(5)HotSpot VM 加载类时,字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池,即在 StringTable 中并没有相应的引用,在堆中也没有对应的对象产生
3、ldc 指令
(1)用于将 int、float 或 String 型常量值从常量池中推送至栈顶
(2)根据上面说的,在类加载阶段,这个 resolve 阶段(constant pool resolution)是 lazy 的,换句话说并没有真正的对象,字符串常量池里自然也没有,那么 ldc 指令还怎么把常量推送至栈顶?
(3)执行 ldc 指令就是触发 lazy resolution 动作的条件
(4)ldc 字节码的执行语义是:到当前类的运行时常量池(runtime constant pool,在 HotSpot VM 中是 ConstantPool + ConstantPoolCache)去查找该 index 对应的项,如果该项尚未 resolve 则 resolve之,并返回 resolve 后的内容
(5)在遇到 String 类型常量时,resolve 的过程如果发现 StringTable 已经有了内容匹配的java.lang.String 的引用,则直接返回这个引用,反之,如果 StringTable 里尚未有内容匹配的 String 实例的引用,则会在 Java 堆里创建一个对应内容的 String 对象,然后在 StringTable 记录下这个引用,并返回这个引用
(6)ldc 指令是否需要创建新的 String 实例,根据是在第一次执行 ldc 指令时,StringTable 是否已经记录了一个对应 String 的引用
创建 String 对象
1、两种方式
(1)字面量
(2)new(构造器)
2、字符串对象存在于堆,字符串常量池只会存放引用
通过字面量赋值
String str = "King";
1、查看字符串常量池是否存在引用指向 King 字符串
2、若存在,栈中 str 直接指向堆中 King
3、若不存在,在堆创建 King,在字符串常量池创建 King 的引用,栈中 str 指向 King
使用 new 创建对象
String str = new String("King");
1、堆中创建 String 对象,但未初始化
2、执行 ldc
(1)判断符号引用是否已经解析成了直接引用
(2)如果没有,则会进行解析,判断字符串常量池是否存在对 King 的引用
(3)如果存在,则将符号引用解析成字符串常量池的引用
(4)如果不存在,则在堆中创建一个 King 字符串对象,然后将符号引用解析成字符串常量池的引用
3、初始化 1 中的 String 对象为 King,str 指向它
4、此时,有两个 King 字符串对象
(1)str(栈)指向的 King(堆)
(2)字符串常量池的引用指向的 King(堆)
字符串拼接操作
1、常量与常量的拼接,结果在字符串常量池
(1)原理:都是常量时,前端编译器会进行代码优化
(2)常量:使用 final 修饰、使用 "" 直接参与拼接
(3)在实际开发中,尽量使用 final
2、只要其中有一个是变量,结果就在堆中的 String 对象
(1)变量拼接原理:使用 new 创建 StringBuilder,调用 append(),进行拼接
(2)变量:不使用 final 修饰、使用变量名、new 字符串
(3)每初始化一个 String,StringBuilder 就调用 append();直到最后一个 String 拼接完成
(4)最后 StringBuilder 调用 toString(),底层为 return new String(value, 0, count);,所以结果在堆中的 String 对象
public String(char[] value,
int offset,
int count)
3、常量池中不会存在指向相同字符串的引用
4、字符串拼接性能
(1)时间:StringBuilder.append() < StringBuffer.append() < String 使用 + 拼接
(2)在实际开发中,对于需要多次或大量拼接的操作,在不考虑线程安全问题时,尽可能使用 StringBuilder 进行 append 操作
(3)StringBuilder 空参构造器的初始化大小为 16,如果提前知道需要拼接 String 个数,应该直接使用带参构造器指定 capacity,以减少扩容的次数
intern()
public String intern()
1、手动检查字符串常量池,把有新字面值的字符串地址驻留到常量池里
2、当调用 intern 方法时
(1)如果字符串常量池中,已经包含一个与调用 intern() 的 String 对象相等的字符串的引用,如 equals(Object) 方法所确定的,则该引用指向的字符串会被返回
(2)否则,调用 intern() 的 String 对象的引用被添加到池中,并返回这个 String 对象的引用
(3)对于任何两个字符串 s 和 t,当且仅当 s.equals(t) 为 ture 时,s.intern() == t.intern() 为 true
(4)所有字面字符串、以字符串为值的常量表达式,都被实体化
3、一个 native 方法,调用底层 C 方法
public native String intern();
(1)如果不是用 "" 声明的 String 对象,可以使用 String 提供的 intern(),它会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入字符串常量池中
(2)如果在任意字符串上调用 String.intern(),则其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同
(3)intern() 确保字符串在内存里只有一份拷贝,可以节约内存空间,加快字符串操作任务的执行速度
4、尝试在字符串常量池中,创建调用该方法的字符串对象的引用
(1)使用 equals(),判断调用 intern() 的字符串,与字符串常量池中的引用指向的字符串,是否相等
(2)如果存在相等字符串的引用,则返回字符串常量池中引用的字符串
5、如果不存在相等字符串的引用,不同 JDK 版本存在不同实现
(1)JDK 6,复制调用 intern() 的字符串,并在字符串常量池创建该副本的引用,返回字符串(副本)
(2)JDK 7,在字符串常量池创建引用,指向调用 intern() 的字符串,返回该字符串
6、对于程序中大量使用存在的字符串时,使用 intern()方法能够节省内存空间
intern() 例1
String str5 = new String("1") + new String("1");
str5.intern();
String str6 = "11";
System.out.println(str5 == str6);
1、JDK 7
(1)双引号修饰的字面量 1 会在字符串常量池中创建字符串对象,有两个字面量 1,但是只会创建1次,另一个直接复用
(2)两个 new String 创建两个字符串对象 1
(3)字符串拼接通过 StringBuilder 创建出一个新的字符串对象 11,并将引用赋值给 str5
(4)str5 调用 intern 方法,检查到字符串常量池还没有字符串 11,则将字符串对象放入常量池,此时字符串常量池中的 11 就是 str5 指向的字符串对象
(5)双引号修饰的字面量 11 检查到字符串常量池中已经存在字符串 11,则直接使用字符串常量池中的对象,所以 str6 被赋值为字符串常量池中的对象引用
(6)输出结果为 true
(7)执行结束的内存结构
2、验证字符串的符号引用在运行阶段才被解析成直接引用
(1)假设字符串的符号引用在类加载的解析阶段,就解析成直接引用,那么这个例子的流程如下(JDK7及之后版本)
(2)解析阶段,双引号修饰的字面量 1 和 11 会在字符串常量池中创建字符串对象
(3)两个 new String 创建两个字符串对象 1
(4)字符串拼接通过 StringBuilder 创建出一个新的字符串对象 11,并将引用赋值给 str5
(5)str5 调用 intern 方法,检查到字符串常量池存在字符串11,则不做任何操作
(6)str6 被赋值为字符串常量池中的对象引用,此时 str6 和 str5 指向的是不同的字符串对象
(7)输出结果为 false
(8)实际上在 JDK 7 及之后版本的输出结果为 true
3、JDK 6
(1)JDK 6 存在永久代,字符串常量池指向的字符串对象在 JDK 6 中是在永久代创建的,JDK 7 才被移动到堆中
(2)所以当执行 str5.intern 时,发现永久代中没有字符串11,则会在永久代创建字符串对象 11,后续的 str6 也是指向永久代的字符串对象,所以,此时 str5 和 str6 指向的不同对象
intern() 例2
String str7 = new String("1") + new String("1");
String str8 = "11";
String str9 = str7.intern();
System.out.println(str7 == str8);
System.out.println(str8 == str9);
1、双引号修饰的字面量 1 会在字符串常量池中创建字符串对象,这边有两个字面量 1,但是只会创建一次,另一个直接复用
2、两个 new String 创建两个字符串对象 1
3、字符串拼接通过 StringBuilder 创建出一个新的字符串对象 11,并将引用赋值给 str7
4、双引号修饰的字面量 11 会在字符串常量池中创建字符串对象,并将引用赋值给 str8
5、str7 调用 intern(),检查到字符串常量池存在字符串 11,则不做任何操作,同时返回字符串常量池的引用,并赋值给 str9
6、输出结果为 false 和 true
7、执行结束的内存结构
G1 中的 String 去重操作
1、测量表明
(1)堆存活数据集合中 String 对象占 25%
(2)堆存活数据集合里面重复的 string 对象有 13.5%
(3)String 对象的平均长度是 45
2、G1 垃圾收集器中实现自动和持续的 String 重复数据删除,以避免浪费内存,减少内存占用
3、重复指的是在堆中的数据,而不是字符串常量池,因为字符串常量池不会产生重复
4、实现
(1)当垃圾收集器工作时,会访问堆上存活的对象,对每一个访问的对象都会检查,是否为候选的、要去重的 String 对象
(2)如果是,把这个对象的一个引用插入到队列中等待后续的处理,一个去重的线程在后台运行,处理这个队列,处理队列的一个元素意味着从队列删除这个元素,然后重新引用已有的 String 对象
(3)使用一个 HashTable 记录所有被 String 对象使用的、不重复的 char 数组,当去重时,会查找 HashTable,寻找堆上是否已经存在一个相同 char 数组
(4)如果存在,String 对象会被调整引用(3)数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉
(5)如果查找失败,char 数组会被插入到 HashTable,以后就可以共享这个数组
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战