Java 和低延迟

我已经记不清有多少次有人告诉我,Java 不适合开发性能至关重要的应用程序。我的第一反应通常是要求澄清“性能”的真正含义,因为吞吐量和延迟这两个最常见的指标有时会相互冲突,而且针对其中一项指标的优化可能会对另一项指标产生不利影响。

现有的 Java 应用程序开发技术可以达到甚至超越使用传统编程语言构建的应用程序的性能要求。然而,即使如此,从延迟角度来看,也可能不足以获得最佳性能。Java 应用程序仍然需要依赖操作系统来访问底层硬件。通常,延迟敏感(通常称为“实时”)应用程序在几乎直接访问底层硬件时运行最佳,Java 也是如此。在本文中,我们将介绍一些方法,以便我们能够最有效地利用系统资源。

Java 从一开始就被设计为能够在各种硬件和系统架构上以二进制级别进行移植。为此,它设计并实现了一个虚拟机(执行平台的抽象模型),并让其执行Java 源代码编译器的输出。其理念是,迁移到不同类型的硬件平台只需移植虚拟机即可。应用程序和库无需修改即可运行(“一次编写,随处运行”的口号)。

然而,对延迟和性能有严格要求的应用程序通常要求在执行时尽可能接近硬件 - 它们希望从硬件中榨取所有性能,并且不希望纯粹为了可移植性或抽象编程概念(如动态内存管理)而出现的中间代码妨碍执行。

多年来,Java 虚拟机已经发展成为一个极其复杂的执行平台,它能够在运行时将 Java 字节码生成机器码,并根据动态收集的指标优化该代码。这是 C++ 等静态编译语言无法做到的,因为它们缺乏所需的运行时信息。谨慎选择数据结构和算法可以最大限度地减少甚至消除垃圾收集的需求——这或许是 Java 运行时环境中避免一致延迟的最明显因素。

但归根结底,Java 虚拟机仅仅是虚拟的——它需要在操作系统上运行才能管理其对硬件平台的访问。无论该操作系统是 Linux(可能是服务器端环境中使用最广泛的操作系统)、Windows 还是其他操作系统,问题仍然存在。

Linux 的“问题”

Linux作为 Unix 操作系统家族的一员,多年来不断发展。Unix 的第一个版本开发于 20 世纪 60 年代末;它最初在学术和研究领域发展壮大,并获得了巨大的普及,随后以各种形式出现在商业领域。Linux 已成为 Unix 的主流变体——尽管它仍然保留了许多原始特性。如今,随着基于容器的执行环境和云计算的兴起,它的主导地位已几乎完全确立。

然而,从实时(或延迟敏感型)应用程序的角度来看,Linux/Unix 确实存在问题。这些问题主要源于 Unix 被设计为分时系统这一基本事实。它最初的硬件平台是小型计算机,由许多不同的用户同时共享。所有用户都有自己的工作要做,而 Unix 则竭尽全力确保所有用户都能“公平地”共享计算机资源。

确实,操作系统会偏向那些执行大量 I/O(包括在终端上与系统交互)的用户,而牺牲那些主要执行计算的任务(所谓的 CPU 密集型任务)。考虑到当时的计算机几乎都是单 CPU(单核),这种做法是有道理的。

然而,随着多 CPU 计算机的发展,Unix 操作系统的核心需要进行一些重大的重新设计,才能有效地利用这些执行核心。但同样的方法仍然适用,交互式任务始终比 CPU 密集型任务更受青睐。有了多核可用,最终效果仍然是整体性能的提升。

如今,几乎每台计算机都拥有多核,从手机等移动设备到工作站,再到服务器级机器。研究这些环境并探索是否有不同的方法可以改进平台,使其更有效地支持实时、延迟敏感的应用程序,这似乎是合理的。

我们如何解决这些问题?

在我工作的 Chronicle Software 公司,我们基于多年经验,开发了许多开源库,用于支持构建针对低延迟优化的应用程序。本文的其余部分将介绍我们实现这一目标的一些经验。

Java 运行时

影响 Java 应用程序延迟的主要问题在于垃圾回收堆的管理以及使用锁同步访问共享资源。目前已有技术可以解决这两个问题,尽管这需要开发人员在一定程度上偏离惯用的 Java 编程风格。理想情况下,我们会使用一些封装底层细节和专用技术的库,但我们确实需要了解“幕后”发生的事情。

为低延迟应用程序设计的框架和库青睐的一种方法是绕过 Java 垃圾收集器,利用不属于正常 Java 堆的内存(称为“堆外”内存)。这些内存使用正常的操作系统机制映射到持久存储,或者通过网络连接复制到其他系统。

这种方法的明显优势在于,内存访问不受垃圾收集器非确定性干预的影响。其缺点在于,在这些区域中创建的对象的生命周期管理变成了应用程序或库的责任。

现代应用程序的常见架构包含组件之间某种形式的通信,通常基于消息传递。在通信过程中,消息会被序列化为 JSON 或 YAML 等标准格式,并从中反序列化,而提供此功能的库通常会引入大量的对象分配。经过仔细考虑,可以选择经过精心设计的库来最大限度地减少新 Java 对象的创建,从而对性能产生积极的影响。

从 Java 诞生之初,对共享可变数据的并发访问就一直使用互斥锁进行同步。如果一个线程尝试获取另一个线程持有的锁,则会被阻塞,直到锁被释放。在多核环境中,可以使用无需获取锁的线程阻塞的替代技术来实现同步,并且事实证明,在大多数情况下,这在降低延迟方面具有积极作用。

编写这类代码并不简单,但是,可以将其封装在标准 Java 库的 Lock 接口后面,或者进一步通过定义允许通过标准 API 进行安全、无锁并发访问的数据结构来实现。一些标准 Java 集合库也采用了这种方法,尽管这对用户来说是透明的。

Linux

公平地说,多年来,Unix 的“实时”变体已经出现一段时间,它们为特定应用程序提供了不同的执行环境。虽然这些产品通常都是小众产品,但如今,许多类似的方法和功能在主流的 Unix 和 Linux 发行版中都已提供。

最小化延迟的功能通常分为两类:内存管理和线程调度。

Linux 进程中的所有内存,包括 Java 的垃圾回收堆,都可能被临时“分页”到磁盘,以便其他进程可以在需要重新分配内存之前将内存用于自身用途。这一切对进程来说完全透明,内存中数据和后备存储中数据的访问时间差异可能达到几个数量级。当然,堆外内存也受此影响。

然而,现代 Unix 和 Linux 系统允许标记内存区域,以便操作系统在从进程中寻找可回收区域时忽略这些区域。这意味着,对于该进程中的这些内存区域,内存访问时间将保持一致(并且总体上感觉更快)。需要指出的是,在繁忙的 Java 应用程序中,访问进程内存的频率会降低该内存被调出的可能性,但风险仍然存在。

以这种方式固定进程的内存意味着其他进程的内存会减少,从而导致其他进程受到影响,但在“实时”世界中,我们必须有些自私!

为低延迟而设计的数据结构通常会默认或通过选项提供将其内存锁定或固定在 RAM 中的能力。

Java 程序中的线程,就像其他应用程序甚至操作系统任务中的线程一样,其 CPU 访问权限由操作系统的一个组件(称为调度程序)管理。调度程序有一组策略来决定哪些线程需要访问 CPU(称为可运行线程)——通常可运行线程的数量会多于 CPU 数量。

如前所述,Unix/Linux 中的传统调度策略旨在优先考虑交互式线程,而非 CPU 密集型线程。如果我们尝试运行延迟敏感的应用程序,这对我们来说毫无帮助——我们希望我们的线程能够以某种方式优先于其他非延迟敏感线程。

现代 Unix/Linux 系统提供了可以提供这些功能的替代调度策略,通过允许线程调度优先级固定在高水平,这样当它们处于可运行时,它们将始终从其他线程接管 CPU 资源,这意味着它们可以更快地响应事件。

但是,我们还可以进一步影响调度程序的行为。通常,管理线程时会使用所有可用的 CPU 资源。现在,我们可以更改调度程序使用的 CPU。我们可以将某些 CPU 从调度程序可用的 CPU 中完全移除,并将其专门用于我们指定的线程。

或者,我们可以将 CPU 划分成组,并将一组 CPU 与特定的线程组关联。此功能是 Linux 中一个更通用的资源管理组件(称为“组”)的一部分。它是 Linux 对虚拟化支持的一部分,也是在现代环境中实现容器(例如由 Docker 生成的容器)的关键。但是,一般应用程序可以通过特定的系统调用使用它。

就像上面描述的内存固定一样,我们的做法是自私的,因为这样做显然会对系统的其他部分产生负面影响。为了获得最佳结果,需要非常谨慎地进行配置,因为出错的可能性很高,而且错误的后果可能很严重。

结论

编写和部署低延迟应用程序是一项高技能的工作,不仅需要了解所使用的编程语言,还需要了解应用程序的运行环境。在本文中,我概述了一些需要考虑的领域以及如何解决这些问题。

相关文章

Java虚拟机中的垃圾回收算法:清理内存的艺术

Java虚拟机中的垃圾回收算法:清理内存的艺术Java程序员们经常提到“垃圾回收”这个词,但你知道它是如何工作的吗?今天,我们就来揭开Java虚拟机(JVM)中垃圾回收(GC)的神秘面纱,看看它是如何...

Java程序GC垃圾回收机制优化指南

Java程序GC垃圾回收机制优化指南作为一个Java开发者,我们经常会在任务管理器里看到Java进程占用内存不断增长,然后突然下降的现象。这其实就是在Java虚拟机中运行的垃圾回收(GC)机制在起作用...

Java虚拟机垃 圾回收算法大揭秘

Java虚拟机垃圾回收算法大揭秘提到Java虚拟机(JVM),就不得不提它的核心功能之一——垃 圾回收(GC)。GC作为JVM中至关重要的模块,承担着自动管理内存的重要职责。它就像一位默默无闻的“家务...

JVM垃圾回收机制详解与参数调优:让程序跑得更欢畅

JVM垃圾回收机制详解与参数调优:让程序跑得更欢畅提到Java编程,不得不提的就是JVM(Java虚拟机)。JVM作为Java程序运行的基础平台,其内部的垃圾回收机制更是不可或缺的一环。今天我们就来聊...

Java 垃 圾回收机制:让内存管理更轻松

Java 垃圾回收机制:让内存管理更轻松在Java的世界里,内存管理是一门艺术,而垃 圾回收机制正是这门艺术中最耀眼的明珠之一。它让我们从繁重的手动内存分配和释放工作中解脱出来,转而专注于业务逻辑的实...

垃 圾回收:Java程序背后的“清道夫”

垃圾回收:Java程序背后的“清道夫”在这个数字化的时代,内存管理成了每个程序员都绕不开的话题。而Java作为一门以“自动内存管理”著称的编程语言,它的垃 圾回收机制就像一位默默无闻却至关重要的“清道...