Contents

Java 9中的紧凑字符串

1. 概述

Java 中的String在内部由包含String字符的char[]表示。而且,每个char由 2 个字节组成,因为Java 内部使用 UTF-16。

例如,如果String包含英语中的单词,则每个char的前 8 位都将为 0 ,因为 ASCII 字符可以使用单个字节表示。

许多字符需要 16 位来表示它们,但统计上大多数只需要 8 位 - LATIN-1 字符表示。因此,内存消耗和性能仍有提升空间。

同样重要的是String通常占据 JVM 堆空间的很大一部分。而且,由于 JVM 存储它们的方式,在大多数情况下,String实例会占用它实际需要的两倍空间。

在本文中,我们将讨论 JDK6 中引入的 Compressed String 选项和最近随 JDK9 引入的新 Compact String。这两个都是为了优化 JMV 上字符串的内存消耗。

2. 压缩String——Java 6

JDK 6 update 21 Performance Release,引入了一个新的 VM 选项:

-XX:+UseCompressedStrings

启用此选项后,字符串将存储为byte[]而不是char[] -因此节省了大量内存。然而,这个选项最终在 JDK 7 中被删除,主要是因为它会产生一些意想不到的性能后果。

3. 紧凑String——Java 9

Java 9 重新引入了紧凑String的概念。

这意味着每当我们创建一个String时,如果String的所有字符都可以使用一个字节(LATIN-1 表示)来表示,那么内部将使用一个字节数组,这样一个字节就给一个字符。

在其他情况下,如果任何字符需要超过 8 位来表示它,则所有字符都使用两个字节存储 - UTF-16 表示。

所以基本上,只要有可能,它只会为每个字符使用一个字节。

现在,问题是——所有String操作将如何工作?它将如何区分 LATIN-1 和 UTF-16 表示?

好吧,为了解决这个问题,对String的内部实现进行了另一项更改。我们有一个最终的字段coder,它保留了这些信息。

3.1. Java 9 中的String实现

到目前为止,String被存储为char[]

private final char[] value;

从现在开始,它将是一个char[]

private final byte[] value;

变量coder

private final byte coder;

coder可以在哪里:

static final byte LATIN1 = 0;
static final byte UTF16 = 1;

大多数String操作现在检查编码器并分派到具体的实现:

public int indexOf(int ch, int fromIndex) {
    return isLatin1() 
      ? StringLatin1.indexOf(value, ch, fromIndex) 
      : StringUTF16.indexOf(value, ch, fromIndex);
}  
private boolean isLatin1() {
    return COMPACT_STRINGS && coder == LATIN1;
}

在 JVM 需要的所有信息准备就绪且可用后,CompactString VM 选项默认启用。要禁用它,我们可以使用:

+XX:-CompactStrings

3.2. coder的工作原理

在 Java 9 String类实现中,长度计算如下:

public int length() {
    return value.length >> coder;
}

如果String仅包含 LATIN-1,则coder的值将为 0,因此String的长度将与字节数组的长度相同。

在其他情况下,如果字符串是 UTF-16 表示,则coder的值将为 1,因此长度将是实际字节数组大小的一半。

请注意,对 Compact *String所做的所有更改都在String类的内部实现中,并且对于使用String*的开发人员来说是完全透明的。**

4. 紧凑String与压缩String

在 JDK 6 压缩String的情况下,面临的一个主要问题是String构造函数只接受char[]作为参数。除此之外,许多String操作依赖于*char[]*表示而不是字节数组。因此,必须进行大量拆包,这影响了性能。

而在紧凑String的情况下,维护额外的字段“coder”也会增加开销。为了降低coder的成本和将byte解包为char(在 UTF-16 表示的情况下),一些方法被内在 化并且由 JIT 编译器生成的 ASM 代码也得到了改进。

这种变化导致了一些违反直觉的结果。LATIN-1 indexOf(String)调用内部方法,而indexOf(char)不调用。在 UTF-16 的情况下,这两种方法都调用内部方法。此问题仅影响 LATIN-1字符串,并将在未来版本中修复。 因此,紧凑String在性能方面优于压缩String

为了了解使用紧凑String节省了多少内存,我们分析了各种 Java 应用程序堆转储。而且,虽然结果在很大程度上取决于特定的应用程序,但总体改进几乎总是相当可观的。

4.1. 性能差异

让我们看一个非常简单的例子来说明启用和禁用紧凑String之间的性能差异:

long startTime = System.currentTimeMillis();
 
List strings = IntStream.rangeClosed(1, 10_000_000)
  .mapToObj(Integer::toString) 
  .collect(toList());
 
long totalTime = System.currentTimeMillis() - startTime;
System.out.println(
  "Generated " + strings.size() + " strings in " + totalTime + " ms.");
startTime = System.currentTimeMillis();
 
String appended = (String) strings.stream()
  .limit(100_000)
  .reduce("", (l, r) -> l.toString() + r.toString());
 
totalTime = System.currentTimeMillis() - startTime;
System.out.println("Created string of length " + appended.length() 
  + " in " + totalTime + " ms.");

在这里,我们创建了 1000 万个String,然后以一种幼稚的方式附加它们。当我们运行这段代码时(默认启用紧凑字符串),我们得到输出:

Generated 10000000 strings in 854 ms.
Created string of length 488895 in 5130 ms.

同样,如果我们通过使用*-XX:-CompactStrings*选项禁用压缩字符串来运行它,输出为:

Generated 10000000 strings in 936 ms.
Created string of length 488895 in 9727 ms.

显然,这是一个表面级别的测试,它不能具有很高的代表性——它只是新选项在这种特定情况下提高性能的一个快照。