Java 21实现了性能改进


 在 Java 21 中,由于最近在 Java 核心库中进行了内部性能优化,旧代码的运行速度可能会大大加快。在本文中,我们将仔细研究其中的一些变化,看看您最喜欢的编程语言变得有多快。

在将原始值(如int和long值)与某些外部表示法(如file文件)来回转换时,会使用内部类java.io.Bits。
在以前的Java版本中,这个类中的转换是使用显式位移的,如下所示:

static long getLong(byte[] b, int off) {

     return ((b[off + 7] & 0xFFL)      ) +

            ((b[off + 6] & 0xFFL) <<  8) +

            ((b[off + 5] & 0xFFL) << 16) +

            ((b[off + 4] & 0xFFL) << 24) +

            ((b[off + 3] & 0xFFL) << 32) +

            ((b[off + 2] & 0xFFL) << 40) +

            ((b[off + 1] & 0xFFL) << 48) +

            (((long) b[off])      << 56);

}

当仔细观察时,可以看到,代码将从一个后备字节数组中提取一个长,方法是连续提取一个字节值并将其左移不同的步骤,然后将这些字节相加。

由于最低索引的字节是最有意义的(即它被左移得最多),所以提取是按big-endian顺序(也称为 "网络顺序")进行的。算法中有八个类似的步骤,每个步骤都在一个单独的行上,每个步骤包括六个子操作。

  • 在所提供的关闭参数中加入一个常数
  • 从提供的b数组的索引处提取一个字节值,包括检查索引的界限
  • 将字节值转换为长值(因为即将与LHS上的另一个长值进行AND操作)。
  • 与长值0XFF进行AND操作
  • 将结果向左移动若干步
  • 累积结果值(通过+操作)。

因此,总共有8次6个操作(=48个操作)需要执行。
实际上,Java 能够稍微优化这些操作,例如通过利用可以在单个步骤中执行多个操作的 CPU 指令。
从外部循环调用getLong()需要多次检查索引边界,因为由于方法的复杂性,很难将边界检查提升到外部循环之外。

Java 21 的改进
在 Java 21 中,转换是使用VarHandle结构进行的,类java.io.Bits被移动并重命名为jdk.internal.util.ByteArray以便来自不同包的其他类也可以从中受益。下面是ByteArray::getLong方法在 Java 21 中的样子:

private static final VarHandle LONG = 

        MethodHandles.byteArrayViewVarHandle(long[], ByteOrder.BIG_ENDIAN);



static long getLong(byte[] b, int off) {

     return (long) LONG.get(b, off);

}

这里,看起来只有一个操作,然而,实际上,在VarHandle::get操作的掩盖下,有几件事正在进行:在使用 little-endian 的平台上(这几乎是 100% 的用户群),字节顺序需要被调换,此外,还必须检查索引的边界。

为了防止LONG VarHandle的返回值被自动装箱/解箱(auto-boxing/un-boxing ),需要进行cast(long)。否则,VarHandle对象的内部工作和它们的坐标就超出了本文的范围。

由于VarHandles是Java语言的第一等公民,我们为使其高效而付出了巨大的努力。我们只能假设字节交换操作是针对手头的平台进行优化的。另外,数组的边界检查可以在许多子步骤之外进行,所以只需要一个检查。

除了内部的边界检查外,VarHandle结构使Java更容易在外循环外进一步进行边界检查,而在Java 21之前使用的是更古老、更复杂的实现。

Bits/ByteArray中几乎所有的方法都被重写了,不仅仅是getLong()。因此,现在读写short、int、float、long和double值都快多了。

受影响的类别和影响
改进后的java.util.ByteArray类直接由以下核心库类使用:

  • ObjectInputStream
  • ObjectOutputStream
  • ObjectStreamClass
  • RandomAccessFile

尽管ByteArray的直接使用似乎是有限的,但这些类的使用非常广泛。例如,上面的三个 Object Stream 类广泛地与序列化结合使用。 
这意味着,在许多情况下,Java 序列化现在要快得多!
RandomAccessFile类在JDK 内部用于图形和声音输入/输出以及 Zip 和目录处理。 
更重要的是,有大量的第三方库依赖于这些改进后的类。它们和所有使用它们的应用程序将自动受益于这些速度改进。无需更改您的应用程序代码。它只是运行得更快!

如果您使用这些改进类中的一个或多个(直接或间接),您的应用程序在 Java 21 下实际运行速度会提高多少?只有一种方法可以找出答案:立即下载JDK 21 早期访问版本,在 Java 21 上运行您自己的代码。