likes
comments
collection
share

Java编译器对字符串拼接优化,以及对内占用内存做了那些优化

作者站长头像
站长
· 阅读数 28

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 指令时,将调用 StringConcatFactoryBootstrap 方法。这个方法会选择并生成一个 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() 方法通过右移操作 >> 来计算字符串长度。

  • 如果 coder0(表示 LATIN1 编码),则不会进行移位操作,直接返回 value.length
  • 如果 coder1(表示 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、性能优化、大厂面经,真诚推荐你关注我。


最新文章


开源新项目

  • 云同步编译工具(SyncKit),本地写代码,远程编译,欢迎前去查看 SyncKit

  • KtKit 小巧而实用,用 Kotlin 语言编写的工具库,欢迎前去查看 KtKit

  • 最全、最新的 AndroidX Jetpack 相关组件的实战项目以及相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看 AndroidX-Jetpack-Practice

  • LeetCode / 剑指 offer,包含多种解题思路、时间复杂度、空间复杂度分析,在线阅读

转载自:https://juejin.cn/post/7374265502670471202
评论
请登录