实时 Java,第 1 部分: 使用 Java 语言编写实时系统

本文是关于实时 Java™ 的系列文章(共 5 部分)的第一篇,描述了使用 Java 语言开发能够满足实时性能需求的系统的过程中遇到的主要挑战。全面概述了实时应用程序开发的意义,以及如何设计运行时系统以满足实时应用程序的需求。作者介绍了一个实现,该实现通过组合一些基于标准的技术解决了实时 Java 带来的挑战。

Mark Stoodley, 咨询软件开发人员, IBM Toronto Lab

Mark StoodleyMark Stoodley 在 2001 年从多伦多大学获得计算机工程的博士学位,并于 2002 年加入 IBM 多伦多实验室,研究 Java JIT 编译技术。从 2005 年起,通过采用现有的 JIT 编译器并在实时环境中进行操作,他开始为 IBM WebSphere Real Time 开发 JIT 技术。他现在是 Java 编译控制团队的团队负责人,从事在本地代码的执行环境中提高本地代码编译效率的工作。工作以外,他喜欢收拾自己的家。



Mike Fulton (fultonm@ca.ibm.com), 高级技术人员, IBM Toronto Lab

Mike FultonMike Fulton 于 1989 年毕业于加拿大 British Columbia 的 Simon Fraser University,并获得了计算机科学学位,专长于编译技术。他在 IBM 多伦多实验室从事编译领域的工作,在过去的 18 年里,他从事过测试、代码开发、文档编制、服务、架构和性能分析等工作。他开发过的产品有 C、C++、Java 编程、解析器技术、调试器、分析器,最近几年,他开发了用于 JIT Java 编译的编译器优化。在 2005 年,Mike 的工作重点转向开发实时 Java 解决方案,尽管自从加入实验室以来,他一直从事 IBM zSeries 技术的开发。自 1999 年起,Mike 开始在 Maple Ridge 远程办公,这是一座临近 Vancouver 的小城市。



Michael Dawson, 咨询软件开发人员, IBM Ottawa Lab

Michael DawsonMichael Dawson 在 1989 年毕业于 Waterloo 大学,获得计算机工程学士学位,在 1991 年从 Queens 大学获得了电子工程的硕士学位,专长于密码技术。他曾从事过安全顾问工作并开发了 EDI 安全产品。随后,他领导开发了一个启动工程,该工程用于跨多个平台交付安全产品。从此成为团队领导者,该团队从事电子商务应用程序的开发并将其交付为各种服务,包括 EDI 通信服务,信用卡流程、在线拍卖以及电子发票等。涉及到的技术包括 C/C++、Java、J2EE 平台以及跨越多个操作系统的组件。在 2006 年,Michael 加入了 IBM 并从事 J9 JVM 和 WebSphere Real Time 的开发。



Ryan Sciampacone, 高级软件开发人员, IBM Ottawa Lab

Ryan Sciampacone自 1997 年从 Carleton University 获得 BCS 后,Ryan Sciampacone 就参与了虚拟机所有方面的开发工作,包括核心 VM 实现、JNI API 层、Ahead-of-time 编译。从 2002 年开始,他成为 J9 VM 的垃圾收集机制的主要架构师和技术主管。他负责开发用于 JSE 实现、Metronome 收集器和 ME 配置收集器的一整套可伸缩收集器。工作以外的生活,Ryan 喜欢打曲棍球、练习瑜伽和骑脚踏车。



John Kacur, 软件开发人员, IBM Toronto Lab

John KacurJohn Kacur 从 Acadia University 获得了美术专业的文学士学位,并从 Brock University 获得了计算机科学的理学士学位。他在乌克兰学习过俄语并曾在德国教授英语,之后,在 2000 年开始为 IBM Toronto Lab 工作,从事 Java JIT 编译器的开发。他对 Linux 操作系统的狂热众所周知。除了 JIT,他还研究 Linux 分析器并在 2005 年参与了实时 Java 项目。



2007 年 5 月 10 日

由于很多重要原因,Java 语言在实时系统中的应用非常有限。这些原因包括 Java 语言设计中固有的不确定性性能影响,例如动态类加载,以及 Java 运行时环境(Java Runtime Environment,JRE)本身的不确定性性能影响,例如垃圾收集器和本地代码编译。Real-time Specification for Java (RTSJ) 是一种开放的规范,它进一步增强了 Java 语言的开放性,使它能够用来构建实时系统(参见 参考资料)。要实现 RTSJ 规范,要求具备操作系统、JRE 和 Java 类库(Java Class Library,JCL)的支持。本文探究了使用 Java 语言实现实时系统存在的挑战,并介绍了能够应对这些挑战的开发工具包和运行时环境。本系列后续文章将更深入解释本文介绍的这些概念和技术。

实时需求

Real-time (RT) 是一个含义广泛的术语,用来描述需要与真实世界同步的应用程序。比如,一个反应缓慢的用户界面不能够满足一般用户的正常 RT 需求。这类应用程序通常被称为 RT 应用程序。“应用程序对鼠标单击的反应时间不能超过 0.1 秒”,这样表述可能更明白一些。如果不能满足这种需求,那么这就是一个软故障:应用程序可以继续运行,尽管用户不高兴,但仍然能使用它。相比之下,那些必须严格地满足实时同步需求的应用程序通常被称为 RT 应用程序。比方说,控制飞机方向的应用程序不能够有任何原因的延迟,否则将导致灾难性的后果。RT 应用程序的含义很大程度上取决于应用程序的容错程度,即偏离实时需求到何种程度时会被认为是错误。

RT 需求的另一个关键因素是响应时间。对于编写硬或软 RT 应用程序的编程人员来说,理解响应时间的约束至关重要。要求满足 1 微秒硬响应的技术与那些要求满足 100 毫秒硬响应的技术截然不同。在实践中,要使响应时间小于几十微秒,需要组合定制的软硬件,很可能没有(或者有非常薄的)操作系统层。

最后,健壮 RT 应用程序的设计者通常需要一些可计量的确定的性能特征,以便设计能够满足响应时间需求的应用程序。不可预测的性能影响非常严重,致使系统无法满足应用程序响应时间的要求,因此难于(或者根本不可能)正确地设计应用程序。大多数 RT 执行环境的设计者投入了很多精力来减少不确定性性能影响,来满足最大范围内 RT 应用程序的响应时间需求。


RT Java 应用程序面临的挑战

在通用操作系统中,运行在通用 JVM 上的标准 Java 应用程序只能期望满足几百毫秒级别的软 RT 需求。原因涉及该语言的几个基本方面:线程管理、类加载、Just-in-time (JIT) 编译活动以及垃圾收集(garbage collection,GC)。应用程序设计者可以减轻其中的一些问题,但需付出大量的工作。

线程管理

标准的 Java 语言没有为线程调度和线程优先级提供任何保证。一个必须在精确时间内响应事件的应用程序无法确保不会在一个高优先级线程之前调度另一个低优先级线程。为了弥补这个缺点,程序员需要将应用程序划分为一组子应用程序,这样操作系统才可以在不同优先级上运行。这种划分将增加事件的开销,并使得事件间的通信更加困难。

类加载

一个与 Java 一致的 JVM 必须延迟加载类,直到程序第一次引用该类。根据被加载类所在的介质(磁盘或其他)的速度、类的大小、类加载器本身的开销,类加载的时间有所不同。加载类的延迟通常高达 10 毫秒。如果需要加载几十或几百个类,则加载时间本身就会引起很长时间的意外延迟。仔细地设计应用程序,使应用程序在启动时加载所有的类,但是这必须手动完成,因为 Java 语言规范不让 JVM 提前执行这一步。

停止所有处理

一直以来,只有应用程序停止时才会执行垃圾收集,这个过程称为停止所有处理(STW)。在收集期间,从一组 “根” 对象(由静态字段、当前活动在一些线程堆栈中的对象等指向的对象)开始跟踪活动的对象,并将不用的内存释放回自由列表中,供稍后分配请求使用。

使用 STW 垃圾收集器时,应用程序会在执行 GC 过程时感觉到暂停。这些 STW 暂停的持续时间很长,并且通常对应用程序有很大的干扰性,暂停持续时间的范围是几百毫秒到几秒。暂停的时间长度取决于堆大小、堆中活动数据的数量以及收集器回收自由内存的强度。

很多现代收集器使用了诸如并发和增量算法的技术,用来减少暂停次数。但即使使用了这些技术,GC 暂停仍会不时地出现,并且持续很长时间。

垃圾收集

应用程序开发中 GC 的益处 —— 包括指针安全、避免内存泄露以及使开发人员免于编写定制的内存管理工具 —— 已经被很好地证明。然而,对于使用 Java 语言的硬 RT 编程人员来说,GC 是使他们备受挫折的另一个原因。当 Java 堆耗尽后仍然不 能满足分配请求时,将自动进行垃圾收集。应用程序本身也能触发垃圾收集。

一方面,GC 对于 Java 程序员来说非常不错。在诸如 C 和 C++ 这样的语言中,由于需要明确地管理内存而引发的错误是最难诊断的一些问题。在部署应用程序时,检验是否存在这类错误同样也是一个基本难题。Java 编程模型的一个主要优点就是:由 JVM 而不是应用程序来执行内存管理,这将为应用程序编程人员去掉这一负担。

另一方面,传统的垃圾收集器可导致长时间的延迟,而应用程序编程人员几乎不可能预测出这一时间。几百毫秒的延迟并不少见。在应用程序层上解决这一问题的惟一方法就是通过创建一组重用对象来阻止 GC,从而确保不会耗尽 Java 堆的内存。换言之,编程人员放弃了使用 JVM 管理内存这一优点,而是通过亲自明确地管理内存来解决问题。实践中,这个方法通常不奏效,因为它阻止编程人员使用 JDK 和其他类供应商提供的众多类库,这可能会创建大量的临时对象从而最终将堆填满。

编译

将 Java 代码编译为本地代码引发了与类加载类似的问题。大多数现代 JVM 开始先解释 Java 方法,然后仅将频繁执行的方法编译成本地代码。延迟编译促成了快速启动,并减少了应用程序运行期间执行的编译数量。但是使用解释后的代码执行任务和使用编译后的代码执行任务在时间上有巨大的差异。对于硬 RT 应用程序来说,由于无法预测何时发生编译,将导致很大程度的不确定性,从而无法有效地规划应用程序的行为。对于类加载,通过在应用程序启动阶段使用 Compiler 类以编程的方式编译方法可以减轻这一问题,但是维护这样的方法非常乏味并且容易发生错误。


Java 实时规范

RTSJ 的创建是为了解决 Java 语言的一些限制,这些限制阻止了它在 RT 执行环境中的广泛应用。RTSJ 处理了几个有问题的地方,包括调度、内存管理、线程、同步、计时、时钟和异步事件处理。

调度

RT 系统需要严格控制线程的调度方式并保证线程调度的确定性:就是说,按照给定的相同的一组条件调度线程。尽管 JCL 定义了线程优先权的概念,然而传统 JVM 并不需要执行优先权。同样,非 RT Java 实现通常使用循环抢占式调度方法,该方法根据不可预测的调度顺序进行调度。在 RTSJ 规范中,RT 线程需要真正的优先级,以及具有优先级继承支持的固定优先级抢占式调度程序。这种调度方法确保了最高优先级的活动线程总是被执行,并且它能够一直执行直到自愿释放 CPU 或者被更高优先级的线程抢占。优先级继承确保了在高优先级线程需要的资源被低优先级线程占用时,避免发生优先级反转。优先级反转在 RT 系统中是一个很严重的问题,因此我们将在 RT Linux® 中更详细地讨论它。

内存管理

虽然一些 RT 系统能够容忍由垃圾收集器引起的延迟,但是在很多情况下,延迟是不可接受的。为了支持那些不能容忍被 GC 打断的任务,RTSJ 定义了不朽(immortal)作用域(scoped)内存区域,以补充标准的 Java 堆。如果垃圾收集器需要释放堆中的内存,则这些内存区域将允许任务使用内存而不需要中断任务。分配在不朽内存中的对象可以被所有的线程访问并且从来不会被收集。正由于它永远不会被收集,不朽内存是一个有限制的资源,必须谨慎使用。程序员可以控制作用域内存区域的创建和销毁。每个作用域内存区域按最大空间分配,并且可以用于对象分配。为了确保对象之间引用的完整性,RTSJ 定义了管理一个内存区域(堆、不朽内存、作用域内存)中的对象如何引用其他内存区域对象的规则。更多的规则定义了作用域内存中的对象何时终结,以及何时可以重用内存区域。由于这些复杂性,建议只对那些不能够忍受 GC 暂停的组件使用不朽和作用域内存。

线程

RTSJ 增加了对两个新线程类的支持,它们为使用 RT 行为 RealtimeThreadNoHeapRealtimeThread(NHRT) 执行任务提供基础。这两个类为优先级、周期性行为、处理程序时限(当超过时限时将触发程序)以及除堆外的内存区域的使用提供了支持。NHRT 不能访问堆,因此,与其他类型线程不同,NHRT 很少会被 GC 打断或抢占。RT 系统通常将具有高优先级的 NHRT 用于那些延迟需求最严格的任务。将 RealtimeThread 用于那些延迟需求能够适应垃圾收集器的任务,将常规 Java 线程用于其他任务。由于 NHRT 不能访问堆,因此使用这些线程必须非常谨慎。例如,即使使用标准 JCL 中的容器类也必须小心,以避免容器类无意中为堆创建临时或内部对象。

同步

在 RT 系统中必须小心地管理同步,以防止高优先级线程等待低优先级线程的情况发生。RTSJ 包括了优先级继承支持,可以在发生同步时进行管理,并且还提供了线程通信的能力,而不需要通过无等待(wait-free)读写队列实现同步。

计时和时钟

与标准 Java 代码提供的时钟相比,RT 系统需要更高分辨率时钟。新的 HighResolutionTimeClock 类封装了这些计时服务。

异步事件处理

RT 系统通常管理并响应异步事件。RTSJ 包含了对异步事件处理的支持,触发异步事件的事件包括大量包含计时器的源代码、操作系统信号、超过时限和其他应用程序定义的事件。


IBM WebSphere Real Time

和 WebSphere Application Server 不同,WebSphere Real Time 并没有包括 Java Enterprise Edition 应用服务器。

要实现 RTSJ 需要来自底层操作系统及 JRE 组件的广泛支持。在 2006 年 8 月发布的 IBM® WebSphere® Real Time(参见 参考资料)与 RTSJ 全面兼容,并且包含了一些新技术,旨在改进 RT 系统的运行时行为并减少应用程序设计者创建 RT 系统时必须的工作量。图 1 展示了 WebSphere Real Time 组件简化后的表示:

图 1. WebSphere Real Time 概览
WebSphere Real Time 概览

RTSJ 实现的(小)世界

运行在 Linux 上的其他两个遵守 RTSJ 规范的实现是 TimeSys RTSJ Reference Implementation 和 Apogee Aphelion。Sun 的 Java SE Real-time (Java RTS) 运行在 Sparc/Solaris 上。更多信息请参见 参考资料

WebSphere Real Time 的基础是 IBM 的跨平台 J9 技术。Linux 操作系统应用了开源的 RT 补丁,提供了基本的 RT 服务以支持 RT 行为,特别是支持 RTSJ 规范要求的行为。得到极大加强的 GC 技术支持 1 毫秒的暂停时间。JIT 编译可用于软 RT 场景,其中编译将在不需要执行高优先级任务时发生。同样还引入了新的 Ahead-of-time (AOT) 编译技术(图 1 没有显示),为不适合使用 JIT 编译的系统提供了硬 RT 性能。接下来的几节将介绍每一种技术;本系列后续文章将详细阐述每种技术的工作原理。


RT Linux

WebSphere Real Time 运行在一个定制的完全开源的 Linux 版本上。其中进行了几处更改用以创建 RT Java 环境。这些更改提供了完全抢占式内核、线程中断处理程序、高分辨率计时器、优先级继承和健壮的互斥锁(mutex)。

完全抢占式内核

RT Java 线程是使用固定优先级 调度实现的,又称为静态优先级 调度,它使用先进先出(first-in-first-out)的调度策略。标准的 Linux 内核提供了软 RT 行为,尽管并没有保证高优先级线程在抢占低优先级线程时等待时间的上限,但可以粗略估算出时间为几十微秒。在 RT Linux 中,几乎每个内核行为都是抢占式的,因此需要减少低优先级线程被抢占的时间,并允许高优先级线程运行。剩下的不能被抢占的临界部分非常短,并且可以确定地执行。这三个等级的执行顺序将改善 RT 调度延迟,并且可以粗略测量为几十微秒。

减少延迟的线程中断处理程序

几乎所有的中断处理程序都被转换为运行在进程中的内核线程。由于处理程序变为用户可配置的、可调度的实体,可以像其他任何进程一样被抢占和优先化,因此延迟变得更短并且更具确定性。

高分辨率计时器

高分辨率时钟和计时器提供了更高的分辨率和准确性。RT Java 将这些特性用于高分辨率休眠和定时等待。Linux 高分辨率计时器是使用高精度、64 位数据类型实现的。与传统 Linux 不同(其时钟和计时器依赖于低分辨率的系统滴答,限制了计时器事件的粒度),RT Linux 使用独立的可编程的高分辨率计时器事件,可以实现每微秒以内的计时。

优先级继承

优先级继承技术可以避免常见的优先级反转问题。图 2 最上面的图表解释了一个最简单的优先级反转示例,它涉及三个线程:一个高级(H)、一个中级(M)和一个低级(L)优先级线程。设想 H 和 M 最初等待事件被触发而处于休眠状态,而 L 处于活动状态并持有一个锁。如果 H 被唤醒去处理事件,它将抢占 L 并开始执行。在这里考虑一下如果 H 在 L 持有的锁处出现阻塞会发生什么情况。由于只有 L 释放了锁后 H 才能进行处理,H 被阻塞,而 L 再次开始执行。如果此时 M 被事件触发,M 将抢占 L 并且执行时间取决于其需要。这种情况就被称为优先级反转,因为 M 在 H 之前执行,即使 H 比 M 的优先级高。

图 2. 优先级反转和优先级继承示例
优先级反转和优先级继承示例

RT Linux 通过使用名为优先级继承(也称作优先级借出(priority lending))的策略防止发生优先级反转,图 2 底部的图表做了解释。当 H 在 L 持有的锁处阻塞时,H 将其优先级交给 L,这将保证在 L 释放 H 需要的锁之前,比 H 优先级低的任务都不能抢占 L。当 L 释放锁后,L 的优先级恢复到最初的值,这样 H 就可以进行处理而不需要等待 L 完成。应用程序设计者应该尽量避免发生高优先级线程所需的资源被低优先级线程占有的情况,但是优先级继承机制增强了健壮性,从而防止了优先级反转的发生。

健壮的互斥锁和 rt-mutexes

快速用户空间互斥(也被称为 futexes)支持 Linux pthread 互斥锁。Futexes 对获得非竞争锁的时间进行了优化,而不用依赖内核;只有对竞争的锁才需要内核干涉。健壮的互斥锁解决了在持有锁的应用程序崩溃后正确清理锁的问题。同样,rt-mutexes 对优先级继承协议扩展了健壮的互斥锁,后者允许 RT JVM 通过 pthread 库依赖优先级继承行为。


确定性垃圾收集

假设一个 RT 操作系统(比如 RT Linux)为 RT 行为提供了基础,那么可以构建 JVM 的其他主要部分来呈现 RT 行为。GC 是 JVM 中造成不确定性行为主要原因之一,但是这种不确定性可以通过谨慎地设计和使用 RT Linux 的特性而减轻。

GC 暂停的不确定性影响将严重破坏 RT 应用程序在特定时限内完成任务的能力(请参见 垃圾收集)。多数 GC 实现将妨碍 RT 应用程序实现其延迟目标,只有同步需求宽松的大型任务才承受得起 GC 技术引起的延迟。RTSJ 对这一问题的解决方案就是引入由编程人员管理的内存分配,方法是使用不朽和作用域内存区域以及 NHRT,但是这个解决方案对于 Java 应用程序设计者来说开始成为巨大的负担。

WebSphere Real Time 使编程人员依赖于 RTSJ 内存区域(如果他们愿意的话),但是推荐您只对延迟需求极度严格的任务使用这种方法。对于允许 GC 暂停时间为 1 毫秒级的任务,IBM 创建了确定性 GC 技术,该技术可以使编程人员从轻松地编写自动内存管理 具有可预测性能的管理任务中获益。

IBM 的确定性 GC 技术基于两个简单的前提:

  • 没有一个 GC 暂停超出了最大上限。
  • GC 消耗的时间要不多于给定时限,方法是控制该时限内暂停的次数。

牢记这两个前提并用之管理 GC 行为将显著增加应用程序实现其 RT 目标的可能性。

Metronome GC

WebSphere Real Time 使用 Metronome GC 实现 JVM 中的确定性低暂停时间的 GC 行为(参见 参考资料)。Metronome GC 使用基于时间的调度方法,该方法在固定的调度中交叉运行收集器和应用程序(GC 称之为 mutator,因为从垃圾收集器的角度看,应用程序的行为就是随时间改变活动对象的图表)。

针对时间而不是分配率进行调度的原因是:在应用程序执行期间,分配率通常都是不均匀的。完成 GC 工作对分配来说是个负担,GC 暂停很可能是不均匀分配的,这样降低了 GC 行为的确定性。通过使用基于时间的调度,Metronome GC 可以实现一致的、确定性的、有限的暂停时间。并且,由于不需要任何语言扩展或对现有代码做修改,常规 Java 应用程序可以透明地使用 Metronome 并从其确定性特征受益。

Metronome 将时间分为一些离散的量子(quanta),大约 500 微秒但是不会超过 1 毫秒,专门用于 GC 或者应用程序。尽管量子非常的短,如果将一些量子用于执行 GC 任务,那么应用程序仍然会感觉到较长的暂停时间,这会影响到 RT 时限。为了更好地支持 RT 时限,Metronome 将量子分配给 GC,而应用程序应该具有最小的时间百分比。这个百分比被称为 utilization,是用户提供的参数。在任何时间间隔内,用于应用程序量子数不得少于指定的 utilization。默认情况下,utilization 值为 70%:在任何 10 毫秒的时限内,至少有 7 毫秒专门用于应用程序。

用户可以在启动应用程序时设置 utilization 的值。图 3 展示了较长时间期限内应用程序 utilization 的示例。注意垃圾收集器活动状态下对应于时间量子的周期性下降。图 3 所示的整个时间期限内,应用程序 utilization 保持在指定的 70%(0.7)或以上。

图 3. 示例 utilization 图表
示例 utilization 图表

图 4 演示了使用 Metronome 技术后的 GC 暂停时间的确定性。只有一小部分暂停超过了 500 微秒。

图 4. GC 暂停时间直方图
GC 暂停时间直方图

为保持较短的单次 GC 暂停时间,Metronome 在堆内使用了写屏障(write barrier)以及相关的亚结构(metastructure)来跟踪活动的和可能死亡了的对象。跟踪活动的对象需要一些 GC 量子来确定应该保持哪些对象为活动的,哪些对象应该回收。由于这种跟踪工作与程序的执行是交叉进行的,应用程序通过执行加载或存储将某些对象 “隐藏”,因此 GC 可能会丢失这些对象的跟踪。

隐藏活动的对象并不一定是恶意应用程序代码的结果。这种现象很常见,因为应用程序没有意识到垃圾收集器的行为。为了确保收集器不会丢失对象,GC 和 VM 互相协作,跟踪对象之间的链接,因为它们是通过存储应用程序执行的操作而被创建和销毁的。在应用程序执行存储操作之前执行写障碍将完成这种跟踪。写障碍的目的就是:如果存储将导致隐藏活动的对象,那么就记录对象链接状态的变化。这些写障碍表示平衡确定性行为的性能和内存占用开销。

大型对象的分配对很多 GC 策略都是件棘手的事情。在很多情况下,堆中存在太多碎片以至于无法存放一个大的对象,比如说数组。因此,必然会引发长时间的暂停来整理碎片或进行压缩,堆将很多小块的自由内存区域整合为一块大的自由内存区域,从而满足大型分配请求。Metronome 将一种新的两级对象模型用于 arraylets 数组。arraylets 将大数组分为一些小的数组,以使没有进行碎片整理的堆更容易满足大数组分配请求。arraylets 对象的第一级称为 spine,包含指针列表,指向数组的更小部分,称为 leaves。每一个 leaf 大小相同,这样简化了查找数组中特定元素的计算,并使收集器更容易查找到合适的自由空间来分配每个 leaf。将数组分成更小的不连接的部分,这样使数组可以分配在很多小的自由区域里(堆中经常会有这样的区域),而不需要进行压缩。

与传统的 STW 垃圾收集器实现不同,Metronome 在整个应用程序生存期内连续执行 GC 过程,而前者则使用 GC 周期的概念表示垃圾收集的开始和结束。在整个应用程序生存期内,都保证了应用程序的 utilization,其值要高于不需要进行太多的 GC 工作情况下 utilization 的最小值。随着收集器将找到的自由内存返回给应用程序,自由内存量不断上下变化。


RT 中的本地代码编译

大多数现代 JVM 结合使用了解释和编译的代码执行。为了消除解释的高性能代价,JIT 编译器选择将频繁执行的代码直接转换为 CPU 的本地指令。Java 语言的动态特性通常会将编译器的操作作为程序执行,而不是作为发生在程序运行前的一个步骤(C++ 或 Fortran 语言中就属于后一情形)。JIT 编译器将选择它要编译的代码,这样它花费的编译时间很可能通过对代码性能的改进得到补偿。在动态编译行为的最顶层,传统 JIT 编译器使用了大量的推测性优化,这些优化利用了正在运行程序的动态特性,这对于应用程序执行的某个阶段中的一点也许是正确的,但是不一定适合整个应用程序的执行阶段。如果对于该特性的假设稍后变为错误,那么这种优化是 “未完成的”。

在传统的非 RT 环境中,在程序执行时编译代码效果非常好,因为大部分的编译器操作对应用程序性能是透明的。然而,在 RT 环境中,JIT 编译器引入了不可预知的运行时行为,这将严重影响最坏情况时执行时间分析。但是这种环境中,编译的代码的性能优势仍然非常重要,因为它能够使更复杂的任务在较短的时间内完成。

WebSphere Real Time 引入了两种解决方案在不同的折中点平衡这两种需求。第一个解决方案是使用 JIT 编译器,在较低的非 RT 优先级上运行,它已被修改为很少执行主动的推测性优化。非 RT 优先级的操作使操作系统保证编译器不会干扰 RT 任务的执行。但是,代码性能会随时间改变这一事实是一种不确定影响,这使得这种解决方案更适合于软 RT 环境而不适合硬 RT 环境。

对于硬 RT 环境来说,WebSphere Real Time 引入了 AOT,用于应用程序编程。存储在 JAR 文件中的 Java 类文件可以通过简单的命令行预编译为 Java eXEcutable (JXE) 文件。通过将这些 JXE 文件(而不是原始的 JAR 文件)指定到应用程序类路径中,将能够使应用程序得到调用,因此就执行了 AOT 编译的代码 —— 而不是经解释过的字节码或 JIT 编译器编译过的本地代码。在第一个 WebSphere Real Time 发布版中,使用 AOT 代码意味着没有使用 JIT 编译器,这样有两个主要的好处:更低的内存消耗以及不会受到来自 JIT 编译线程和样例线程的动态性能影响,这些线程用来标识频繁执行的代码。

图 5 展示了在使用 AOT 代码时,在 WebSphere Real Time 中如何执行 Java 代码:

图 5. 如何使用 AOT 代码
如何使用 AOT 代码

首先看图 5 的左上方,和任何 Java 开发项目一样,开发人员将 Java 源代码编译成类文件,类文件被绑定到 JAR 文件,然后由 AOT 使用 jxeinajar 工具对其进行编译。这个工具可以编译 JAR 文件中所有类的所有方法,也可以根据一个基于 JIT 的样例程序的执行输出选择性地编译某些方法,这个程序标识了需要编译的最重要的方法。jxeinajar 工具编译 JAR 文件中的方法并构造一个 JXE 文件,后者包含原始 JAR 文件的内容以及 AOT 编译器生成的本地代码。在执行程序时,JXE 文件可以直接取代 JAR 文件。如果使用 -Xnojit 选项调用 JVM,那么将加载类路径中 JXE 文件的 AOT 编译的代码(根据 Java 语言的规则)。在程序执行期间,将对从 JAR 文件加载的方法或者从 JXE 文件加载的未经编译的方法进行解释。将从 JXE 中加载的编译了的方法作为本地代码执行。在图 5 中,同样还需要使用 -Xrealtime 命令行选项指定应该调用 RT VM。该命令行选项只有在 WebSphere Real Time 中可用。

AOT 代码的缺点

尽管 AOT 代码能够产生更确定的性能,它同样具有一些缺点。用于存储 AOT 代码的 JXE 文件通常要比存放类文件的 JAR 文件大很多,这是因为本地代码一般要比存储在类文件中的字节码密度小。执行本地代码还需要各种补充数据,用以描述如何将代码绑定到 JVM 以及如何捕获异常(比如),从而执行代码。第二个缺点是,尽管 AOT 编译过的代码比解释过的代码执行速度快,但是却比 JIT 编译的代码慢得多。最后一点,将解释的方法转换为编译的方法所用的时间(反之亦然)要比从其他解释方法调用一种解释方法(或从其他编译的方法调用编译方法)要长。在具有活动 JIT 编译器的 JVM 中,会通过不断编译编译代码的 “边缘部分” ,直到转换次数不会影响性能为止,最终消除这种代价。在具有 AOT 编译代码而没有 JIT 编译器的 JVM 中,转换次数是由被编译到 JXE 中的方法决定的。因此,我们通常推荐使用 AOT 编译整个应用程序以及应用程序所依赖的 Java 库类。正如上文提到的,扩展编译了的方法的数量将影响内存消耗,尽管性能从中获得的好处往往要比增加内存消耗更重要。

AOT 代码通常要比 JIT 代码的执行速度慢,这是由于 Java 语言自身的特性引起的。Java 语言需要执行的 程序在第一次引用类时对类进行解析。由于在程序执行前进行编译,AOT 编译器必须谨慎对待它所编译的代码所引用的类、字段和方法。AOT 编译的代码通常要比 JIT 编译的代码慢,这是因为 JIT 具有一个优点,它是在所执行的应用程序解析了大量这样的引用之后才执行编译的。然而,JIT 编译器还必须注意抵消它编译程序消耗的时间,因为这些时间将延长程序的执行时间。因此,JIT 编译器不会使用相同的优化度来编译所有的代码。而 AOT 应用程序则无此限制,因此它能够使用更主动的编译技术,所以有时可产生优于 JIT 编译代码的性能。此外,AOT 能够比 JIT 编译器编译更多的方法,因此 AOT 编译也能比 JIT 编译产生更好的性能。虽然如此,通常 AOT 编译的代码的速度要慢于 JIT 编译的代码。

要避免不确定性性能影响,WebSphere Real Time 提供的 JIT 编译器或者 AOT 编译器都不会使用现代 JIT 经常采用的主动的推测性优化。执行这类优化通常能够显著改善性能,但是并不适合在 RT 环境中使用。此外,为了全面支持 RTSJ 以及 Metronome 垃圾收集器,引入了一些开销到编译的代码中,而传统编译器不需要执行这些代码。鉴于以上这些原因,在 RT 环境下编译的代码的运行速度通常要比非 RT 环境编译的代码慢。


未来发展方向

在可预测性能和原始吞吐量方面,需要做更多的工作以加快 RT Java 环境。必须对 Java 语言实现两个关键领域的改进,从而使其在 RT 应用程序领域内获得成功:

  • 为运行传统操作系统而希望获得更好的预测性的用户提供 RT 技术。
  • 使 RT 技术简单易用。

面向软 RT

WebSphere Real Time 的很多特性对以传统操作系统为目标的编程人员非常有用。增量 GC 和基于优先级的线程必定会在大量应用程序中得到应用,即使只能够提供软 RT 性能而不能保证硬 RT 的实现。比如,对于多数开发人员来说,提供可预测性能而不会出现不可预测的 GC 延迟的应用服务器是个非常有吸引力的想法。类似地,使用合理的调度目标,使应用程序运行高优先级并良好监视的 Java 线程将简化 Java 服务器的开发。

令 RT 更加简单

对于开发人员来说,将 Java 语言的优点引入 RT 系统的创建中将带来非常大的优势。但是,始终有要改善的地方,并且我们不断开发新的特性来进一步简化 RT 编程。您可以访问 IBM alphaWorks 站点试用我们研究的实时线程研究技术,它能够使开发人员管理使用非常频繁的事件,而这些事件对变化或延迟的要求很高(参见 参考资料)。通过预加载、预初始化以及预编译代码处理事件,然后使用比 RTSJ 中的 NHRT 更少的繁琐限制运行独立于垃圾收集器的代码,从而实现更具确定性的行为。您还将发现名为 TuningFork 的工具,它用来跟踪从操作系统到 JVM,再到应用程序内部的路径,能够更轻松地执行详细的性能分析。

参考资料

学习

获得产品和技术

  • WebSphere Real Time:WebSphere Real Time 利用了标准的 Java 技术并且没有损失确定性,使应用程序依赖于精确的响应时间。
  • Real-time Java 技术:访问作者的 IBM alphaWorks 研究站点,查找用于实时 Java 的先进技术。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=219117
ArticleTitle=实时 Java,第 1 部分: 使用 Java 语言编写实时系统
publish-date=05102007