深入解析Java字符串:常量池、内存管理与StringBuilder、StringBuffer操作类指南
文章目录
- 前言
- 本次话题:String类。
- 一、String内部表示
- 1、字符数组存储
- 2、如何减少内存使用呢?
- 3、字符串拼接问题
- 结论
- 二、String的内存管理
- 1、 堆内存与方法区(元空间):
- 三、StringBuilder和StringBuffer的区别
- 1、如何选择?
- 2、StringBuffer线程安全的实现
- 总结
前言
在时光的长河中,技术如同一只悄然进化的鳄鱼,沉稳而强大,在看似平静的水面下孕育着无限可能。
鳄鱼杆 ,一名悄悄进化 No 变异的博主。开启了本次话题文章:
本次话题:String类。
在Java中,字符串的存储是一个非常有趣且复杂的话题。它涉及到字符串常量池、内存管理等多个方面。
本篇文章主要通过两个方面来分析Java中字符串:String。
- String内部表示
- String的内存管理
同时,提供一些实践代码来展示String类的构成。相信我们一定可以在其中找到一些问题的答案。
Let’s GO!
一、String内部表示
在Java中,String 类的内部表示方式对于理解其性能特性和行为至关重要。
1、字符数组存储
- 字段定义:
- 从Java 9开始,String 类使用名为 value 的私有 byte[] 数组来存储字符数据,而不是之前的 char[]。
- 这是引入了一种称为“压缩字符串”的优化技术,旨在减少内存使用。
- 这就引出了一个问题,那它是如何减少内存使用呢?
2、如何减少内存使用呢?
- 在Java 8及之前,String类使用一个char[]数组来存储字符串内容。这就导致每个字符占用2个字节(即UTF-16编码)。
- 自Java 9起,String类改用了byte[]数组来存储字符数据,增加了字段:coder;
- 用于指示当前字符串使用的编码类型。
/**
* The identifier of the encoding used to encode the bytes in
* value. The supported values in this implementation are LATIN1 UTF16
*
* 使用的编码标识符,用于编码 value 中的字节。
* 在此实现中支持的值是 LATIN1 和 UTF16。
*
* This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*/
private final byte coder;
JDK 17中代码片段
其中,支持的值是 LATIN1 和 UTF16的解释:
- 对于LATIN1编码,coder值为0;
- 通常指的是ISO-8859-1编码,足够日常使用。(1字节)
- 对于UTF16编码,coder值为1。
- 通常指可表示几乎所有书写系统的字符编码方式。(2字节)
/** The value is used for character storage. */
private final char value[];
-----------------JDK8和JDK17中String类部分代码对比----------------
/**
* The value is used for character storage.
* 该值用于字符存储。
*
* This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*
* 此字段被虚拟机信任,并且如果字符串实例是常量,则此字段可能会进行常量折叠。
* 构造后覆写此字段将会导致问题。
*
* Additionally, it is marked with {@link Stable} to trust the contents
* of the array. No other facility in JDK provides this functionality (yet).
* @Stable is safe here, because value is never null.
*
* 此外,它还被标记为 Stable 以信任数组的内容。
* JDK 中目前没有其他设施提供这种功能。
* 在这里 @Stable 是安全的,因为值永远不会是 null。
*
*/
@Stable
private final byte[] value;
- 以上是JDK17版本中,String类的部分代码,有一点值得关注:
- constant folding
- 指的是编译器优化技术,它会在编译期计算常量表达式的值,而不是在运行时计算.
- 用于提高程序的性能。
- 不可变性
- 上面代码提到的**@Stable**:
- @ Stable 注解表明了被注解的字段在使用过程中不会发生改变。(即其内容是稳定的)
- 同时也因为value数组被 final 修饰。
String 对象是不可变的,这意味着一旦创建了一个 String 对象,就不能更改它的值。
- 这里说的是值。有的时候,我们可以更改变量的引用,相当于间接更改值。
- 可以通过一段代码来演示String的不可变性。
例如:
public static void main(String[] args) {
String temp = "change after";
System.out.println("temp对象的hashCode:" + temp.hashCode());
// 原始对象
String str = "Not change";
System.out.println("str对象的hashCode:" + str.hashCode());
// 对象引用修改
str = temp;
System.out.println("引用修改后str的hashCode:" + str.hashCode());
// 拼接修改
str = temp + "second change";
System.out.println("方法修改后str的hashCode:" + str.hashCode());
}
执行以上代码会得到如下结果:
- 无论通过哪种方式,都无法修改原有对象的内容。(反射除外,说反射的纯粹是来捣乱的)
3、字符串拼接问题
其中字符串拼接代码值得我们关注,如下代码:(JDK8版本)
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
//编译器优化:编译期拼接ab
String s4 = "a" + "b";
// 相当于new StringBuilder().append(s1).append(s2).toString();
String s4 = s1 + s2;
}
如果我们使用JDK9以及以后的版本呢?就会得到下面的情况:
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
//编译器会使用 StringConcatFactory.makeConcatWithConstants 方法来处理此字符串拼接
String s4 = s1 + s2;
}
这一点可以通过:javap className.class,反编译后的字节码文件得出:
其中,makeConcatWithConstants():
- 这个方法特别适合于那些包含常量字符串与一个或多个变量的拼接情况。
- 允许编译器在编译期确定不变的部分,并在运行时仅对需要计算的部分进行处理,从而减少了运行时的开销,提高了程序的执行效率。
在JDK9之后,字符串拼接依照一下规则:
- 存在于字符串常量池时,直接引用。
- 不存在于字符串常量池时,创建新的String对象。
- 不论是,字符串常量和字符串变量,还是字符串变量与变量拼接。都依照以上规则。
我们可以实验一下,代码如下:
// public static void main(String[] args) {
// String temp = "change after";
// System.out.println("temp对象的hashCode:" + temp.hashCode());
原始
// String str = "Not change";
// System.out.println("str对象的hashCode:" + str.hashCode());
引用修改
// str = temp;
// System.out.println("引用修改后str的hashCode:" + str.hashCode());
拼接修改
// str = "second change"; // 相当于new StringBuilder().append("second change").toString();
// System.out.println("方法修改后str的hashCode:" + str.hashCode());
//
// }
public static void main(String[] args) {
String str1 = "a";
String str2 = "b";
String temp = "c";
String str3 = "ab";//原始字符串,存于字符串常量池中
System.out.println("str3的hashCode:" + str3.hashCode());
String str4 = str1 + str2;//变量与变量拼接 ab
System.out.println("str4的hashCode:" + str4.hashCode());
String str5 = "a" + str2;//常量与变量拼接 ab
System.out.println("str5的hashCode:" + str5.hashCode());
System.out.println("--------------一但,字符串不存在与字符串常量池中时-----------------");
String str6 = "ac" + str2;//常量与变量拼接 acb
System.out.println("str6的hashCode:" + str6.hashCode());
String str7 = "a" + temp + "z";//常量与变量拼接 acz
System.out.println("str7的hashCode:" + str7.hashCode());
System.out.println("--------值得关注的是,JDK9以后,会将创建的对象放入字符串常量池一份----------");
String str8 = "a" + temp + "b";//常量与变量拼接 acb
System.out.println("str8的hashCode:" + str8.hashCode());
}
结果,如下图所示:
结论
- String 类的设计考虑了多方面的因素,包括性能、内存使用和安全性。
- 通过使用 字节数组 进行内部表示,并结合不可变性、压缩字符串等特性。
- 了解这些细节有助于我们更好地利用 String 类的功能,并编写出更加高效的代码。
二、String的内存管理
- 内存方面绕不开的便是字符串常量池。下面介绍字符串常量池的位置,以及内存空间术语介绍:
1、 堆内存与方法区(元空间):
- 虽然字符串常量池(Java 7之前),是在永久代(PermGen)中实现的,但在JDK 17中,字符串常量池(String Pool)位于堆(Heap)内存区域中的“元空间”(Metaspace)之外。,以更好地进行垃圾回收。
- 元空间(方法区的实现):
- 自Java 8开始引入的一个内存区域,用于替代永久代(Permanent Generation, PermGen)。
- 垃圾回收:
- 由于字符串常量池位于堆内存中,因此其中的对象也可以被垃圾回收器回收,但这通常只发生在极端情况下,例如系统面临严重的内存压力时。
三、StringBuilder和StringBuffer的区别
特点:
- 两者都不像 String 类是不可变的,可以改变它们所包含的内容而不必创建新的对象。
- 在频繁修改字符串内容的情况下,推荐使用!
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer();
System.out.println("修改前stringBuffer的hashCode:" + stringBuffer.hashCode());
、
stringBuffer.append("b");
System.out.println("修改后stringBuffer的hashCode:" + stringBuffer.hashCode());
System.out.println("------------------------------------------------------");
StringBuilder stringBuilder = new StringBuilder();
System.out.println("修改前stringBuilder的hashCode:" + stringBuilder.hashCode());
stringBuilder.append("b");
System.out.println("修改后stringBuilder的hashCode:" + stringBuilder.hashCode());
}
这样我们就会到结果,如下图:
- 由这个测试结果,可知无论是StringBuilder还是StringBuffer都是操作同一个对象,不会创建新的对象。
当然我们还可以这样做:
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("change after");
StringBuilder stringBuilder = new StringBuilder();
System.out.println("修改前stringBuilder的hashCode:" + stringBuilder.hashCode());
stringBuilder.append(stringBuffer);
System.out.println("修改后stringBuilder的hashCode:" + stringBuilder.hashCode());
}
这样也是行得通的!我们可以从二者的父类:AbstractStringBuilder 中得到答案。
1、如何选择?
- 单线程下操作,即确定对字符串的操作是在一个线程内完成的,那么推荐使用 StringBuilder,因为它提供了更好的性能。
- 涉及到多线程环境,即多个线程同时访问或修改同一个 StringBuffer 实例,则应选择 StringBuffer 来确保 线程安全。
2、StringBuffer线程安全的实现
- 通过对它的公开方法使用 synchronized 关键字来确保任意时刻只有一个线程能够执行这些方法。如图所示:
总结
- 综上所述,理解Java中字符串的存储机制有助于编写更加高效和资源友好的代码。
- 尤其是在处理大量字符串操作的应用程序中,合理利用字符串常量池和选择合适的字符串操作方式显得尤为重要。
各位再见!这里是 鳄鱼杆,钓……鳄鱼的杆儿!
期待下次再会,愿每一次垂钓之旅都能满载而归。