Java编译器对字符串拼接优化,以及对内占用内存做了那些优化
hi 大家好,我是 DHL。就职于美团、快手、小米。公众号:ByteCode,分享有用的原创文章,涉及鸿蒙、Android、Java、Kotlin、性能优化、大厂面经
字符串是高频场景的面试题,所以这篇文章我会详细介绍一下 Java 9 之前和 Java 9 之后,Java 对字符串拼接、内存的占用、内存泄露都有做了那些优化,主要包含以下内容,文章比较长,可以点赞、收藏慢慢看。
- 字符串常量的合并
- 使用
StringBuilder
的隐式优化 final
变量的优化- Java 9 的
invokedynamic
指令来优化字符串拼接的性能 - Java 9 对字符串占用内存做了那些优化
- 内存优化
- 内存泄露
字符串常量的合并
编译器会在编译期间对字符串常量做合并处理。如果在代码中有多个相同的字符串常量,编译器通常会把它们视为同一个对象。
例如:
String hello = "Hello, " + "World!";
编译器会将它们合并为一个字符串常量:
String hello = "Hello, World!";
使用 StringBuilder
的隐式优化
对于简单的字符串拼接,如使用 +
操作符连接一系列字符串,Java 编译器会在背后隐式地使用 StringBuilder
来代替多个 String
对象的拼接。
例如:
String str = "Hello, " + "World" + "!";
编译期间,编译器会将上面的代码转换成:
String str = new StringBuilder()
.append("Hello, ")
.append("World")
.append("!")
.toString();
d
final
变量的优化
如果是编译时可以确定的 final
字符串变量拼接,编译器会进行优化,直接计算出结果,而不是运行时拼接。
示例代码:
final String a = "Hello, ";
final String b = "World";
String c = a + b;
编译器处理后:
String c = "Hello, World";
Java 9 的 invokedynamic
从 Java 9 开始,Java 编译器使用了 invokedynamic
指令来优化字符串拼接的性能。这允许运行时动态选择最佳的字符串拼接策略,而不是在编译时固定使用 StringBuilder
。
例如,以下代码:
String s = "Hello, " + name + "!";
编译器将使用 invokedynamic
指令来将这段代码转换成字节码。而不是使用 StringBuilder
来拼接字符串。
在运行时,当第一次执行这条 invokedynamic
指令时,将调用 StringConcatFactory
的 Bootstrap
方法。这个方法会选择并生成一个 CallSite
方法句柄,用于完成字符串的拼接操作。这样做的好处是,根据程序的运行情况,选择不同的实现,比如直接连接字符串常量、使用 StringBuilder
、使用 StringBuffer
等等。
这种优化更加智能,因为它能够在 JVM 运行时,更好地利用运行时信息来提高字符串拼接的性能,这通常比编译时静态决定使用那种方法更加高效。
Java 9 对字符串占用内存做了那些优化
内存优化
在 Java 9 之前,String
类使用 char
数组(char[]
)存储字符串,每个字符占用两个字节。但是对于只包含拉丁字符等可以用一个字节表示的字符串,这样的存储方式会导致内存浪费。
为了优化内存使用,从 JDK 9 开始,String
类的实现被改变了。现在 String
使用一个字节数组(byte[]
)加上一个编码标记来存储字符串。这个编码标记(coder
)指示字符串使用的是 LATIN1(ISO-8859-1)
还是 UTF16
编码。
- 如果字符串只包含
LATIN1
字符,那么每个字符只占用一个字节。 - 如果字符串包含至少一个需要 UTF16 编码的字符,那么所有字符都使用 UTF16 编码,每个字符占用两个字节。
以下是 JDK 9 中 String
类的源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 字符串内容使用 byte 数组存储
private final byte[] value;
// coder 字段指示使用的编码:0 代表 LATIN1,1 代表 UTF16
private final byte coder;
// ...省略其他代码...
// 返回字符串的长度
@HotSpotIntrinsicCandidate
public int length() {
return value.length >> coder; // 如果 coder 为 1(UTF16),长度是 value.length 的一半
}
// ...省略其他代码...
}
在这个实现中,length()
方法通过右移操作 >>
来计算字符串长度。
- 如果
coder
是0
(表示LATIN1
编码),则不会进行移位操作,直接返回value.length
。 - 如果
coder
是1
(表示UTF16
编码),则value.length
会右移一位,因为UTF16
编码中每个字符占用两个字节。
这种改进显著减少了对于拉丁字符等只需要一个字节表示的字符串的内存使用。然而,对于包含非拉丁字符的字符串,String
类仍然使用两个字节来存储每个字符。
内存泄露
在 JDK 7 update 6 之前,String
类的 substring
方法会尝试共享原始字符串的 char
数组,只是通过改变偏移量和计数来表示新的子串。这种实现可以减少内存的使用,但也可能导致意外的内存泄漏,因为原始字符串的整个字符数组会被保留在内存中,只要子串中的任何一个实例存在,都会导致泄露。
从 JDK 7 update 6 开始,substring
方法的行为发生了变化,它创建了一个新的 char
数组来存储子串,避免了上述的内存泄漏问题。
在 Java 9 及以后版本中,由于 String
使用 byte
数组存储,substring
方法的实现也会创建一个新的 byte
数组。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// ...省略其他代码...
public String substring(int beginIndex, int endIndex) {
int length = endIndex - beginIndex;
// 检查索引范围
if (beginIndex < 0 || endIndex > length || beginIndex > endIndex) {
throw new IndexOutOfBoundsException();
}
// 如果子串长度为 0,返回空字符串
if (beginIndex == 0 && endIndex == length) {
return this;
}
// 创建新的字符串实例
return isLatin1() ? new String(Arrays.copyOfRange(value, beginIndex, endIndex), LATIN1)
: new String(Arrays.copyOfRange(value, beginIndex << 1, endIndex << 1), UTF16);
}
// ...省略其他代码...
}
在这个实现中,substring
方法会创建一个新的字符串实例,如果原字符串使用 LATIN1
编码,那么直接复制对应的字节范围;如果使用 UTF16
编码,那么复制的范围会左移一位(因为每个字符占用两个字节)。
这些优化提高了字符串操作的性能和内存效率,同时减少了内存泄漏的风险。
Hi 大家好,我是 DHL,在美团、快手、小米工作过。公众号:ByteCode ,分享有用的原创文章,涉及鸿蒙、Android、Java、Kotlin、性能优化、大厂面经,真诚推荐你关注我。
最新文章
- 鸿蒙力作:揭晓 ArkTS,如何重塑语法,打造更健壮和可靠的代码
- 华为跟 Android 说再见,解读鸿蒙应用全部虚拟机化
- 鸿蒙:5 分钟秒懂 ArkTs,不能错过的知识点解析
- 学习鸿蒙,解决这几个关键问题
- 国内个人开发者太难了,APP备案保姆级过程
- 鸿蒙,流氓软件的终结者
- 使用 14 年的 API 被下线了
- Android 14 彻底终结大厂流氓应用
- 适配 Android 14,功能和权限的变更,你的应用受影响了吗
- Android 14 新增权限
- Android 13这些权限废弃,你的应用受影响了吗?
- 国外大厂面试题, 7 个 Android Lifecycle 重要的知识点
- Twitter 上有趣的代码
- 谁动了我的内存,揭秘 OOM 崩溃下降 90% 的秘密
- 反射技巧让你的性能提升 N 倍
- 90%人不懂的泛型局限性,泛型擦除,星投影
- 揭秘反射真的很耗时吗,射 10 万次耗时多久
- 影响性能的 Kotlin 代码(一)
- 揭秘 Kotlin 中的 == 和 ===
开源新项目
-
云同步编译工具(SyncKit),本地写代码,远程编译,欢迎前去查看 SyncKit
-
KtKit 小巧而实用,用 Kotlin 语言编写的工具库,欢迎前去查看 KtKit
-
最全、最新的 AndroidX Jetpack 相关组件的实战项目以及相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看 AndroidX-Jetpack-Practice
-
LeetCode / 剑指 offer,包含多种解题思路、时间复杂度、空间复杂度分析,在线阅读
转载自:https://juejin.cn/post/7374265502670471202