实时 Java,第 5 部分: 编写和部署实时 Java 应用程序

示例、提示和技巧

这篇文章是讨论实时 JavaT™ 的 6 部分 系列文章 的第 5 部分,展示了如何使用 IBM WebSphere Real Time 附带的工具,编写和部署实时 Java 应用程序。作者利用示例应用程序,展示了用于控制垃圾收集暂停的 Metronome 垃圾收集器、用于避免运行时编译暂停的预编译器(Ahead-of-time compiler),以及用于满足最迫切的时间需求的 NoHeapRealtimeThread

Caroline Gough (goughc@uk.ibm.com), 软件工程师, EMC

Caroline Gough在加入 IBM Hursley Laboratory 的 Java Technology Centre System Test 团队之前,Caroline Gough 曾在一家小软件作坊做了三年的开发人员。她现在是高级测试师,擅长压力测试和 RAS(可靠性、可用性和可服务性)工具使用。她参与了 IBM WebSphere Real Time V1.0 的工作,现正准备测试 Java 平台的未来发布版。



Andrew Hall, 软件工程师, IBM Hursley Lab

Andrew HallAndrew Hall 于 2004 年加入 IBM 的 Java Technology Centre,在此之前,他在 Southampton 大学学习电子学与人工智能。Andrew 在 Java System Test 小组工作了两年,主要关注测试自动化和负载测试 Java 运行时 —— 包括 WebSphere Real Time V1.0,如今是 Java 5.0 Service Team 的一名成员。在业余时间,他喜欢读书、摄影和魔术。



Helen Masters (helen_postlethwaite@uk.ibm.com), 软件工程师, IBM Hursley Lab

Helen MastersHelen Masters 1995 年毕业于 Nottingham 大学,1996 年加入 IBM Global Services 组织,参与了一个大型国防项目的软件开发。她于 2000 年调入 IBM 的 Hursley Laboratory,在那里凭借其专业技术担任了多个领导角色。Helen 目前负责领导进行 IBM WebSphere Real Time V1.0 测试工作的团队。



Alan Stevens, 软件工程师, IBM Hursley Lab

Alan StevensAlan Stevens 于 1988 年加入 IBM Hursley Laboratory。他擅长改进 IBM 产品(如 CICS 和 WebSphere)以及 IBM Java 技术的性能、可伸缩性和确定性。他在 Java 工具和在 JSR 163 上表示 IBM(JVMTI 定义)方面有着广泛的经验。他目前在领导 IBM WebSphere Real Time Java performance 小组。



2007 年 7 月 11 日

本系列 的前几篇文章讨论了 IBM WebSphere Real Time 如何解决了不确定性问题,从而获得极低的 timescale 值(延迟值)。这种功能将 Java 平台的范围和收益扩展到原本仅适用于特定的实时(RT)编程语言(如 Ada)的领域之中。RT 硬件和操作系统往往是定制的,难以理解。与之不同,WebSphere Real Time 运行在兼容 IBM BladeCenter® LS20(请参见 参考资料)和类似硬件的 Linux® RT 版本之上。它支持典型 RT 应用程序的需求:

  • 低延迟:确保在有限时间内响应信号。
  • 确定性:不存在垃圾收集(GC)的无限暂停。
  • 可预测性:线程优先级监管执行的次数,执行时间一致。
  • 无优先级反转:高优先级的线程不会因中优先级线程正在运行,而被持有其所需锁的低优先级线程阻塞。
  • 对物理存储器的访问:诸如设备驱动程序之类的 RT 应用程序总是需要追溯根源。

这篇文章展示了如何使用 WebSphere Real Time 提供的工具编写和部署 RT Java 应用程序。文中引用了本系列之前的文章,以展示如何使程序以更高级别的 RT 确定性执行。(这可能很有帮助,但阅读之前的文章并非必需。)您将看到如何使用一种 RT GC 策略(如 Metronome)在 WebSphere Real Time 附带的 Lunar Lander 示例应用程序中改进可预测性。您还会学习如何预编译(AOT)您的应用程序,以便改进一个 RT 环境中的确定性。最后,您将使用不受垃圾收集器控制的存储器设计和实现一个 RT 应用程序,发现使您的 RT Java 应用程序发挥最大效能的提示与技巧。

如果您希望运行本文介绍的某些程序 —— 当然,最好是编写您自己的 RT Java 应用程序 —— 那么您就需要访问一个安装了 WebSphere Real Time 的系统(关于获得此技术的更多信息,请参见 参考资料)。

Metronome 垃圾收集器的优势

Metronome 是 WebSphere Real Time 的垃圾收集器。您可以通过启动 WebSphere Real Time 附带的示例应用程序来观察其优势。安装 WebSphere Real Time 后,可以在安装目录 /sdk/demo/realtime/sample_application.zip 处找到这个示例应用程序。

示例应用程序模拟了无人值守的 Lunar Lander 登月舱的控制技术。为实现安全着陆,登月舱的火箭推进器必须正确部署:

  • 降低下降速率的垂直推进器。
  • 对准着陆地点的水平推进器。

为了计算出 Lander 登月舱的位置,Controller 利用为雷达脉冲获取的时间返回这个位置。图 1 展示了这一模拟:

图 1. Lunar Lander
发生在 Lunar Lander 应用程序之内的交互

如果在所返回的信号中出现任何延迟(例如,因 GC 暂停引起的延迟),计算出的登月舱位置就是错误的。所返回的雷达脉冲时间较长就意味着更远的距离,Controller 随后将根据不正确的估计位置作出调整。显然,这会给登月舱或任何 RT 系统造成灾难性的后果。

显示标准 Java 不适合运行 RT 应用程序的方法之一就是:度量 Controller 能够多么准确地保持登月舱位于正确的轨道上,以及着陆的成功情况如何。图 2 中的图表显示了对使用标准 Java VM 的 Controller 的模拟。红线显示了登月舱的实际位置,蓝线显示了雷达度量的位置。

图 2. 使用标准 Java VM 的 Controller 的模拟
使用标准 Java VM 的 Controller 的模拟

尽管这次飞行以成功着陆结束,图 2 中的图表还是显示出一些陡峭的峰值(蓝线)。这些峰值对应于 GC 暂停。在有些时候,GC 暂停会使位置度量中产生极其严重的错误,从而因着陆速度过高(垂直位置错误)或着陆地点丢失(水平位置错误)导致事故。这种不确定的运行时行为阐明了 RT 应用程序一直未应用标准 Java 平台的主要原因之一。

Java 实时规范(RTSJ)为 GC 暂停的问题提供了多种解决方案。它使 Java 程序员了解到自动内存管理的重要性,也引入了新的存储区,避免了要求程序员重新接管内存的 GC 影响。如介绍 NoHeapRealtimeThread 的一节所述,这会带来一些挑战,提高编写可靠 Java 应用程序的门槛。还有一种替代方案,适用于许多可以容忍极短暂停的 RT 应用程序,也就是使用一种 RT 垃圾收集器,例如 WebSphere Real Time 中的 Metronome。

使用 Metronome 运行 Lunar Lander 应用程序会将登月舱引领到更贴近正确位置的地方,而在高度度量中不会产生任何显著峰值,保证每次都安全着陆(参见图 3)。

图 3. 使用 WebSphere Real Time 的 Controller 的模拟
使用 WebSphere Real Time 的 Controller 的模拟

在这次也就是第二次运行中,Controller 的 Java 代码保留原样,这是一个收益于 RT 垃圾收集器的普通 J2SE 应用程序。

可以为示例的调用添加 -verbose:gc 参数,以显示减少的 GC 暂停的细节,如以下输出所示:

<gc type="heartbeat" id="2" timestamp="Tue Apr 24 04:00:58 2007" intervalms="1002.940">
  <summary quantumcount="171">
    <quantum minms="0.006" meanms="0.470" maxms="0.656" />
    <heap minfree="142311424" meanfree="171371274" maxfree="264060928" />
    <immortal minfree="15964488" meanfree="15969577" maxfree="15969820" />
  </summary>
</gc>

这个示例输出报告了演示程序的一次运行内为时 1 秒的间隔内的 GC 活动。此处显示出 GC 运行了 171 次(quantumcount),还给出了应用程序从这些增量式 GC 暂停中得到的平均暂停时间(meanms)是 0.470 毫秒。

关于应用程序工作与 GC 暂停间交错的更具体观点,可录制一份 Metronome 跟踪文件,并用 TuningFork 分析工具查看(参见 参考资料),如图 4 所示:

图 4. 部分演示程序在 TuningFork 中的显示效果
部分演示程序在 TuningFork 中的显示效果

一旦 GC 暂停被最小化,其他可能给一个运行中的应用程序造成干扰的因素就变得重要起来。其中之一就是即时(JIT)编译器的活动。将 Java 字节码编译成本地文件实际上是为了获得更好的性能,但生成本地代码时可能会导致暂停。此问题的解决方案之一是使用 AOT 编译预先编译 Java 字节码。


应用程序的 AOT 编译

Java 运行时通常使用一个 JIT 编译器,为一个 Java 应用程序内执行最频繁的方法动态生成本地代码。在 RT 环境中,有些应用程序(比如说有着严格的截止日期的应用程序)可能无法容忍与动态编译活动相关的不确定性。而对于其他一些应用程序,编译众多用于启动一个复杂应用程序的负载也是不合人意的。面临这些问题的应用程序开发人员能够受益于使用 AOT 编译。

有一种常见的误解,那就是假设预先编译的代码总是能够改进一个应用程序的性能。事实并非总是如此,因为在解释代码和预先编译代码之间进行切换成本极高。如果应用程序的一部分是预先编译的,而其他部分不是这样,那么应用程序的运行速度要比未使用 AOT 编译的时候慢。因此,在选择应用 AOT 编译的内容时应谨慎行事。

AOT 编译涉及到在应用程序执行前为应用程序的 Java 方法生成本地代码。这使用户能够避免动态编译的不确定性,同时又能获得与本地编译相关的最大性能收益。有必要了解,通常运行 AOT 编译(也称为预先编译)的代码要比用动态 JIT 编译器时稍慢。预先编译的代码有着静态的本质 —— 与 JIT 编译器动态生成的代码不同,因而不可能在经过一段时间后,得益于常用方法的进一步优化。WebSphere Real Time 目前不允许混合使用动态 JIT 编译和预先编译的代码。总之,AOT 编译能够以更低的运行时影响提供更确定的运行时性能,原因就是未出现动态编译,而通过支持动态解析来维护 Java 兼容性。

请阅读 “实时 Java,第 2 部分: 比较编译技术”,进一步了解 JIT 编译器用于执行优化的技术、JIT 和 AOT 各自的优缺点,以及两者的对比。

生成预先编译的代码

AOT 编译工具 jxeinajar 会从以 JAR 或 ZIP 文件格式存储的类生成本地代码。该工具可以创建 AOT 编译的代码,可以是为各 JAR 文件的类中的所有方法,也可以是为一个固定的方法集合。如果 JIT 使用了一种固定的优化级别,那么 AOT 编译的代码就等同于 JIT 编译器生成的本地代码。代码以称为 Java eXEcutable(JXE)的内部格式存储。jxeinajar 工具将 JXE 文件包装在一个 JAR 文件中,WebSphere Real Time 随后即可执行此文件。

-Xrealtime 选项表明您希望运行 RT Java 运行时环境。jxeinajar 工具只有在 -Xrealtime 选项未被忽略时才能正常工作。如果未指定此选项,则将调用标准 IBM SDK and Runtime Environment for Linux Platforms,Java 2 Technology v5.0。

AOT 编译是一个分两阶段的过程。第一步,AOT 代码生成(使用 jxeinajar 工具),使用 AOT 编译器生成本地代码。第二步,在 Java Runtime Environment(JRE)内执行这些代码。

以下命令(其中的 aotJarPath 是希望将预先编译的文件写入其中的目录)为当前目录中的所有 JAR 或 ZIP 文件创建 AOP 编译的代码,假定 $JAVA_HOME/bin 位于 $PATH上:

jxeinajar -Xrealtime -outPath aotJarPath

执行此命令后,您将看到如下输出:

J9 Java(TM) jxeinajar 2.0
Licensed Materials - Property of IBM

(c) Copyright IBM Corp. 1991, 2006 All Rights Reserved
IBM is a registered trademark of IBM Corp.
Java and all Java-based marks and logos are trademarks or registered
trademarks of Sun Microsystems, Inc.

Searching for .jar files to convert
Found /home/rtjaxxon/demo.jar
Searching for .zip files to convert
Converting files
Converting /home/rtjaxxon/demo.jar into /home/rtjaxxon/aot///demo.jar
JVMJ2JX002I Precompiled 3098 of 3106 method(s) for target ia32-linux.
Succeeded to JXE jar file /home/rtjaxxon/demo.jar

Processing complete

Return code of 0 from jxeinajar

所创建的 JAR 文件并非真正的 JAR。它也 包含类文件。与此不同,它包含用于所有类和占位符类文件的 JXE 文件,用于访问本地代码。这些文件无法为其他 Java 运行时所用,是 WebSphere Real Time 的这个版本专用的。

可在命令行中指定单个的 JAR 或 ZIP 文件,来重写默认行为。如果要将输入文件的搜索扩展为包含子目录,可向命令中添加 -recurse 选项。

识别预先编译的文件

jxeinajar 工具提供的文件格式包含一个 JXE 文件,还有对该 JXE 文件内各类文件的指针。通过列举 JAR 或 ZIP 文件的内容,您就可以迅速识别出,该文件是否由 jxeinajar 工具生成。如果希望查看 demo.jar,那么列出其内容的命令是:

jar vtf demo.jar

jxeinajar 生成的 JAR 文件提供如下输出:

0 Thu Apr 19 13:59:14 CDT 2006 META-INF/
71 Thu Apr 19 13:59:14 CDT 2006 META-INF/MANIFEST.MF
68 Thu Apr 19 13:59:14 CDT 2006 demo.class
4119 Thu Apr 19 13:59:14 CDT 2006 jxe22A6B69D-010D-1000-8001-810D22A6B69D.class

JAR 文件内的另一个 JXE 文件将其标识为 jxeinajar 工具生成的 JAR 文件。否则,输出应如下所示:

0 Thu Apr 19 09:00:01 CDT 2006 META-INF/
71 Thu Apr 19 09:00:01 CDT 2006 META-INF/MANIFEST.MF
846 Thu Apr 19 09:00:01 CDT 2006 demo.class

执行预先编译的代码

对您的应用程序进行了 AOT 编译之后,就可以使用此命令来运行它了:

java -Xrealtime -Xnojit -classpath aotJarPath AppName

务必进行检查,使任何预先编译的应用程序 JAR 文件都列于类路径的开头处,从而确保预先编译的代码得到执行。

切记,在 WebSphere Real Time 中,动态 JIT 编译和 AOT 编译无法混合使用。如果您忽略了 -Xnojit 选项,那么任何可供 Java VM 使用的 AOT 编译代码都不会被使用。相反,这些代码将会由 JIT 解释或动态编译。命令中的 -Xrealtime 选项启用了 RT Java VM。如果未提供此选项,则将使用 WebSphere Real Time 附带的 SE Java VM。

设置了 -Xnojit 标记后,WebSphere Real Time 将使用此解释器来运行未被预先编译的任何方法。这也就是说,如果它发现未被预先编译的应用程序版本(无论是在预先编译的 JAR 文件中还是在类路径指定的其他 JAR 文件中),代码仅能按照解释的速度运行。

AOT 编译系统 JAR

我们建议,不仅要预先编译您的应用程序,还要对关键的系统 JAR 文件进行 AOT 编译。使用标准 Java API 的任何应用程序实际上都只得到了部分编译,除非系统 JAR 文件也被编译。大多数标准 API 类都存储在 core.jar 和 vm.jar 文件中,因此建议您首先对着两个文件进行 AOT 编译。对于 RT 应用程序。还应预先编译 realtime.jar。除此之外,应用程序的本质决定了还有哪些系统文件的预先编译能够带来性能收益。

AOT 编译系统 JAR 文件的过程与其他 JAR 文件的 AOT 编译过程截然不同。然而,由于系统 JAR 文件是从引导类路径加载的,所以您必须使用如下命令来将预先编译好的系统 JAR 文件附到引导类路径,从而确保其被使用:

java -Xrealtime -Xnojit 
-Xbootclasspath/p:aotSystemJarPath/core.jar:aotSystemJarPath/vm.jar:
aotSystemJarPath/realtime.jar -classpath aotJarPath/realTimeApp.jar realTimeApp

-Xbootclasspath/p: 选项中的 /p 将预先编译好的系统 JAR 文件附到引导类路径。还可通过 -Xbootclasspath:-Xbootclasspath/a: 选项(分别设置对应于设置和添加)操纵引导类路径。然而,如果您使用了 -Xbootclasspath:-Xbootclasspath/a: 将 AOT 编译的 JAR 文件放到引导类路径中,那么编译好的类将不会被使用。

确认选取的是预先编译的 JAR

非常容易在类路径中出错,尤其是在您的应用程序包含多个 JAR 文件,而且您又预先编译了系统 JAR 文件的时候。错误会导致运行非预先编译的代码,而不是所需的预先编译代码。以下选项的组合可帮助您确定所使用的类是预先编译的:

  • -verbose:relocations 将预先编译代码的重定位信息打印到 STDERR。每次执行一个预先编译的方法时,都会打印一份日志记录消息。此选项的输出如下所示:

    Relocation: realTimeApp.main([Ljava/lang/String;)V <B7F42A30-B7F42B28> Time: 10 usec
  • -verbose:class 为其载入的每个类向 STDERR 写入一条消息。该选项产生的输出如下所示:
    class load: java/lang/Object
    class load: java/lang/J9VMInternals
    class load: java/io/Serializable
    class load: java/lang/reflect/GenericDeclaration
    class load: java/lang/reflect/Type
    class load: java/lang/reflect/AnnotatedElement
  • -verbose:dynload 提供关于 Java VM 所加载的各类的详细信息。此信息包含类名、其软件包以及类文件的位置。该信息的格式如下所示:

    <Loaded java/lang/String from /myjdk/sdk/jre/lib/vm.jar>
    <Class size 17258; ROM size 21080; debug size 0>
    <Read time 27368 usec; Load time 782 usec; Translate time 927 usec>


    遗憾的是,这一选项不会列出预先编译的 JAR 文件中的类。然而如果将其与 -verbose:class 选项结合使用,就可以根据类未出现的情况判断出该类已预先编译。列于 -verbose:class 输出之中但未列于 -verbose:dynload 输出之中的任何类都必然是从一个预先编译的 JAR 文件中加载的。您需要的 verbose 选项是 -verbose:class,dynload

配置文件导向的 AOT 编译

您可以构建一组更为优化的预先编译 JAR 文件,方法是创建一个您的应用程序频繁使用的方法配置文件,然后仅用 AOT 编译这些方法。

可以用 -Xjit:verbose={precompile},vlog=optFileName 选项(其中 optFileName 是列举您希望预先编译的方法的文件名)运行您的应用程序,从而动态创建这个配置文件:

java -Xjit:verbose={precompile},vlog=optFileName -classpath appJarPath realTimeApp

该选项生成一个选项文件,其中包含一个方法签名列表,对应于 JIT 编译器在应用程序运行的时候编译的那些方法。如果有必要,您可以利用文本编辑器轻而易举地编辑这个文件。然后可将此文件提供给 jxeinajar 工具,来控制哪些方法将被预先编译。使用以下命令将该文件提供给工具:

jxeinajar -Xrealtime -outPath aotJarPath-optFile optFileName

WebSphere Real Time 附带的 InfoCenter 也讨论了配置式 AOT 编译(参见 参考资料,获得在线 InfoCenter 的链接)。它会指导您为上一节讨论的 Lunar Lander 生成运行时配置文件,还会为您说明如何使用此文件来选择性地预先编译 Lunar Lander 应用程序和系统 JAR 文件。此外,如果您希望尝试预先编译另外一个应用程序,还可以使用下一节讨论的 Sweet Factory 应用程序。


使用 NHRT

WebSphere Real Time 包含 RTSJ 的完整实现。RTSJ 是在 RT 垃圾收集器(如 Metronome)出现之前设计的,包含实现 Java 运行时的可预测、低延迟性能的可选方法。

在 RTSJ 编写之时,Java 运行时中实现可预测式执行有两大阻碍,那就是 JIT 编译器和垃圾收集器。这两种技术都要使用应用程序编程人员无法控制的处理器时间。它们有着动态的本质,这也就是说,两种技术都会给 Java 应用程序引入不可预测的延迟。在某些情况下,这些延迟可能会持续数秒,这对于许多 RT 系统来说都是无法接受的。

JIT 编译器可关闭,或者用其他技术取而代之,如 AOT 编译,但 GC 无法轻易禁用。在移除 GC 之前,必须提供内存管理的替代解决方案。

为了支持无法容忍标准垃圾收集器导致延迟的 RT 系统,RTSJ 定义了不朽作用域 存储区,补充了标准 Java 堆。RTSJ 还添加了对两个新线程类的支持 —— RealtimeThreadNoHeapRealtimeThread(NHRT),使应用程序编程人员能够利用其他 RT 特性,包括使用堆以外的存储区。

NHRT 是无法与 Java 堆上创建的对象协同工作的线程。这使之能够独立于垃圾收集器运行,实现低延迟、可预测的执行。NHRT 必须使用作用域或不朽存储器创建其对象。这需要一种与基于堆的标准 Java 编程截然不同的编程风格。

下面,我们将使用 NHRT 开发一个简单的应用程序,展示使用非堆内存的独特挑战。

示例场景

我们将为一家糖果厂实现一个自动化系统。这家工厂有多条生产线,将原材料加工成各种类型的糖果,然后将其装罐。该系统将设计用于检测已装罐但所装糖果过多或过少的罐子,并通知工厂工人处理装罐不当的罐子。

装罐完成后,就进入称重阶段,检查各罐内装入的糖果数量。如果一罐中的糖果数量超出目标 2%,则必须向工厂工人的控制屏幕发送一条消息,将此问题通知给工人。工人使用控制面板上显示的罐子 ID 来找到它,将其移出包装队列,然后在控制面板上确认该罐已移除。各罐质量必须写入日志文件,以便审计。

图 5 给出了示例场景的示意图:

图 5. 糖果厂场景
糖果厂场景

显然,这个示例有些刻意,但它能帮助您了解创建一个 NHRT 应用程序的挑战,尤其是在 NHRT 和其他线程类型间共享数据时。

外部接口

系统必须处理三类外部实体:生产线上的称重机、工人的控制台、审计日志。生产线和工人的控制台已封装在系统提供的 Java 接口中。

称重机的接口有一个方法 —— weighJarGrams() —— 它将一直阻塞到下一个罐子传过称重机,并返回该罐子以克数计算的质量。罐子成功传过称重机的比率是变量,但可低至每 10 毫秒 1 个罐子。若 weighJarGrams() 方法未得到足够频繁的轮询,则可能错过某些罐子。

称重机是生产线的一个组件,它具有一些方法,查询所生产的糖果类型以及所填装的罐子规格。

工人的控制台有两个方法 —— jarOverfilled()jarUnderfilled(),两者都要接受一个罐子的 ID。这些方法将阻塞至工人确认消息(可能要花上几秒钟的时间)。

我们将实现 MonitoringSystem 接口,它有两个方法:startMonitoring()stopMonitoring()startMonitoring() 方法接受 ProductionLine 对象和需要将其作为参数来与之通信的 WorkerConsole 对象。

审计日志被指定为一个名为 audit.log 的平面文件,其中的每一行都是一个以逗号分隔的字符串,格式为 timestamp,jar id,sweet type code, jar size code,mass of jar

图 6 是一个展示了这些接口的 UML 类图:

图 6. 接口的 UML 类图
接口的 UML 类图

设计解决方案

既然已经有了规范,那么就可以设计解决方案了。问题可以分解成两部分:第一,轮询生产线,并检查罐子的质量;第二,写审计日志。

轮询生产线

如果考虑 WeighingMachine 接口,weighJar() 方法需要频繁轮询,因此明智的做法是为每个 ProductionLine 使用一个专用线程,使设计可伸缩。我们将使用一个 NHRT 来最小化轮询被垃圾收集器中断的线程以及丢失度量结果的可能性。

对一个罐子进行称重之后,我们需要计算出该质量等于多少糖果,并将其与目标值相比较。预测一次度量所需进一步处理的数量极为困难,如果一个罐子中的糖果数量超出容许偏差,那么我们就必须考虑与 WorkerConsole 通信,这可能要花上几秒钟的时间。

经过 10 毫秒之后,可能又会有大批罐子传送到此,因此我们显然不能在轮询的线程上进行计算。我们需要将度量结果传递给一个单独的计算线程。由于某些处理可能要占用较长的时间,因而需要为每条生产线使用多个处理线程,确保总有一个线程能来处理最新的度量结果。

可为所产生的每个数据片段生成一个新线程,但这会将大量处理器时间浪费在启动和停止线程上。为更好地利用 CPU 时间,我们可以创建一个 NHRT 池来处理数据,通过维护一个运行中线程的池,在运行的时候就不存在任何线程启动和停止开销了。

可以使用一个由全部生产线共享的线程池,但任何可由多个线程共享的数据结构都需要同步。使用单独一个线程池可能会导致严重的锁争用。为了使我们的解决方案可伸缩,每条生产线都将附有自己的小线程池。

线程池的设计涉及到多方面的考虑事项,例如池的大小和管理技术,这些内容超出了本文讨论范围。就本文的目的而言,我们将为每个 ProductionLine 对象创建 10 个入池线程,如果出于某些原因耗尽线程,我们还会扩展线程池。

写审计日志

与本系统中的其他组件不同,审计日志记录组件并非时间关键的。如果我们(天真地)忽视了计算机崩溃或关闭的可能性,那么惟一重要的考虑事项就是在某些时刻记录的度量结果了。

考虑到这一点,我们将使用一个 java.lang.Thread 来写出到日志文件。在 NHRT 等待更多工作、垃圾收集器不活动时,它将完成这一工作。这样的设计决策具有广泛的影响,原因在于我们在传统基于堆的环境和 NHRT 的非堆环境之间引入了一个接口。稍后您将看到,在处理这个接口时需要格外谨慎。

图 7 是该架构的高级示意图:

图 7. 高级架构图
高级架构图

单亲规则

为根据共享作用域在线程间共享数据而设计架构是完全可能实现的。然而,这非常困难,编写起来往往非常别扭,主要原因就是单亲规则。单亲规则规定了一个作用域只能有单独一个父亲。

为了理解单亲规则的必要性,请考虑 RT 线程可依次进入各存储区并构建起一个存储区栈的情况。如果其中某些存储区是作用域,那么您就面临着某些内存会被迅速收集,而某些对象(不朽存储区内的对象)则永远不会被收集。

RTSJ 必须定义规则,防止应用程序编程人员创建出对象可能被意外释放或神秘消失的架构。然而,这些规则可能难以理解,在编程中应用起来也令人畏缩。单亲规则并不是 RT Java 编程人员必须考虑的惟一内存访问规则,但它是使线程间共享作用域变得极为困难的规则。

作用域存储区具有父作用域(parent scope)的概念。当您进入一个作用域时,您最后进入而尚未离开的作用域(当前线程的存储区栈中的下一个作用域)就变为刚刚进入的作用域的父亲。如果这是一个线程的作用域栈中的第一个作用域,那么该作用域的父亲就是原始作用域 —— 一个逻辑作用域,而非您能够实际创建对象的位置。如果一个作用域未被使用,那么也就没有父亲。

单亲使得线程间共享作用域更为艰难,因为您不得不倍加小心地控制进入跨多个线程的存储区的顺序。事实上,您就是在将各线程的数据(线程存储区栈)与线程共享数据(作用域的父域)对齐。非法操作的一个最明显的示例就是启动两个线程,每个线程都带有一个不同的作用域作为其初始存储区,并使每个线程都尝试进入一个共享作用域来交换一些数据。这将为一个作用域创建两个父亲,这是不允许的。

一个线程的存储区栈实际上就是一个仙人掌式栈(cactus stack);有可能在不离开存储区的前提下回溯栈,然后创建一个较低的分支。创建和维护复杂的存储区栈结构比较困难,如果没有有力的原因,不应尝试这样去做。然而,若能谨慎地使用存储区栈,就有可能在多种环境中共享作用域。

在两个线程间共享作用域的最简单方法如下;

  • 启动两个线程,均以共享的作用域作为初始存储区。该作用域的父亲应为原始作用域。
  • 在不朽存储区内启动两个线程,并从这里进入作用域。由于这将是两个线程进入的第一个作用域,因此该作用域的父亲也就是原始作用域,未违背单亲规则。

现在您已经对希望 NHRT 应用程序实现的功能有了一点头绪,下一个挑战就是找出系统在其中完成这些任务的存储区。

RT Java 中的非堆内存

要为我们的设计应用作用域和不朽内存,首选需要对其工作原理略知一二。

在不朽内存中创建的对象从来不会被清除,在应用程序的生命周期中一直存在。即便是您已经使用完了对象,它依然会占据无法回收的空间。这无疑给编程人员保持跟踪在不朽内存中创建的所有对象并避免长期不断创建对象的职责造成了阻碍。不朽内存泄漏是 RT Java 应用程序中的常见错误源头。

在作用域内存中创建的对象则在于其中创建它们的作用域的生命周期中存在。作用域内存的每个区域都有一个引用计数;一个线程进入 scoped 内存的一个区域时,该引用计数将递增,当该线程离开时,引用计数则递减。当引用计数为 0 时,作用域内的对象将被释放。作用域存储区的大小有最大值,这是在其创建时指定的,并且必须用于它们的目标任务。RT 应用程序的设计者通常会将作用域与指定任务关联在一起,以便有效调优。作用域不适于使用的内存数不可预测的任务,因为作用域的大小是固定的,必须预先声明。

Sweet Factory 示例的内存架构

上面我们简要介绍了非堆内存,现在可以将其应用于之前所设计的系统了。

从内存的角度来看,审计系统比较简单。它运行在堆内存的一个 java.lang.Thread 之上。无论您使用的是标准 Java 线程还是基于堆的 RT 线程,在垃圾收集器管理的内存中进行字符串操作和 I/O都是易于察觉的,因为这些操作会以令人惊奇的方式耗用大量内存。

我们的系统中的其他线程是 NHRT,根据定义,它们不能使用 Java 堆来分配对象。我们的选择是限于作用域和不朽内存的某种组合。

所有线程都有一个初始存储区,将在该线程的生命周期中使用。在我们的设计中,我们的 NHRT 是长期运行的,因此无论选择什么作为初始存储区,在初始启动后都绝对不能使用其中的任何内存,因为无论使用的是作用域还是不朽 —— 内存都将无法再被清空,最终将被耗尽。

当前存储区仅被对象分配消耗,因此内存管理的一种途径就是仅使用固定数量的对象,或者完全避免使用对象。通过使用栈上的原始值,我们就可以在不使用当前存储区的前提下完成工作。(栈是内存的一部分,存储函数参数和方法中使用的字段。它与 Java 堆和不朽或作用域内存分离,但无法容纳对象 —— 仅能容纳原始值或对象引用。)

然而,Java 语言机器类库鼓励您使用对象来达成目标。因此,对于本例,我们将假设 NHRT 需要执行的操作会创建一些对象,并在每次执行时占用一些内存。

在这个场景中(系统必须在未指定的较长时间内具有一个平面内存配置文件,但依然会创建对象),最佳方法是在不朽内存中启动线程,并为指定、限定的任务分配区域。

在一个线程运行时,只要它需要执行一项任务,就应该进入一个作用域(其大小是专为该任务校准的)、执行任务,然后离开作用域以释放所占用的内存。要使此技术更为健壮,您执行的任务必须是限定的,那样您才能够预测并校准所需的作用域内存数量。

在多个线程间共享作用域是可行的,但比较困难,原因就是内存作用域的单亲规则(参见 单亲规则)。管理共享的作用域并不简单,因为一个作用域只有在所有的线程都离开它时才能被回收。这也就是说,作用域的大小必须合理设定,以允许多个线程同时执行任务。

总之,如果您坚持一次对一个线程上的一个任务使用一个作用域,使用 NHRT 进行开发就比较简单。例如,每个生产线轮询线程都将在不朽内存中启动,查询 ProductionLine 之前,要为其预先创建一个作用域。每个筛选池线程都将在不朽内存中启动,并使用栈上的原始数据进行计算。每个线程都将有一个作用域可进入,如果它需要使用 WorkerConsole 接口(对象将在其中创建)。

线程间通信

我们最终的内存问题是如何在线程间通信。ProductionLine 轮询线程需要向筛选池发送数据,筛选池中的每个线程都需要向审计线程传递数据。

通过将原始值作为方法的参数传递就能够轻松解决这个问题。因为所有的数据都将位于栈上,我们就不会遇到关于存储区的问题。

为了使示例应用程序更有趣味性,我们将创建 Measurement 类,其对象将用于度量相关数据。但应在哪个存储区内创建这些对象呢?我们不能使用这个架构,没有任何作用域在线程间共享。

由于忽略了堆和作用域,我们剩下的只有不朽内存。我们知道,不朽内存永远不会还原,因此无法如愿地继续创建 Measurement 对象,因为那样将耗尽内存。答案是:在不朽内存中创建有限个 Measurement 对象,然后重用它们 —— 实际上就是创建了一个对象池。

用 MeasurementManager 轮询度量对象

我们将创建 MeasurementManager 类,它带有一些静态方法,用于获取和返回可重用的 Measurement 实例。作为 Java SE 编程人员,可能会尝试使用一个现有的 LinkedListQueue 类,来提供一个数据存储,容纳我们的度量结果。然而,这种做法不会成功,原因有二:第一个原因是大多数 SE 集合类都在后台创建对象来维护数据结构 —— 比如说链表中的节点,以这种方式创建对象可能导致不朽内存泄漏。第二个原因更为微妙,我们在尝试桥接堆和非堆上下文中运行的线程,对于大多数多线程应用程序,我们需要使用锁来保证对所用一切数据结构的排他访问。这种 NHRT 和基于堆的线程间的锁共享会致使垃圾收集器将 NHRT 作为优先级反转保护的副作用抢占。如果进入垃圾收集器很可能中断了 NHRT 的地方,那么首先就会丧失使用非堆内存的所有收益。可以说,不应在 NHRT 和基于堆的线程间共享锁;关于该问题的具体解释,请参见 “实时 Java,第 3 部分: 线程化和同步”。

RTSJ 提供了一种在 NHRT 和基于堆的线程间共享数据的解决方案,那就是 WaitFreeQueue 类。这些类是具有无等待 端的队列,在这里,一个 NHRT 可以请求读或写某些数据(具体取决于类),而不存在阻塞的风险。队列的另一端使用传统的 Java 同步,由堆线程使用。通过避免非堆和基于堆的环境中的锁,我们就可以安全地交换数据了。

我们的 MeasurementManager 将由 NHRT 用于获取度量结果,由基于堆的审计线程用于返回度量结果。因此,我们使用一个 WaitFreeReadQueue 来管理此结构。WaitFreeQueue 的无等待端专门设计成单线程。WaitFreeReadQueue 则为多个写入方、单一读取方的应用程序而设计。我们使用的是多个读取方、单一写入方的应用程序,因此必须添加自己的同步,来确保同一时间只有一个 NHRT 请求一个度量结果。这看似因添加额外的同步而违背了使用 WaitFreeQueue 的目的。但监控器控制 read() 方法的访问将仅在 NHRT 间共享,因而不会存在堆和非堆上下文中的危险锁共享。

这就带来了 NHRT 应用程序开发中的又一大挑战,为在非堆环境中使用而重用现有部分 Java 代码变得无比艰难。如您所见,您被迫谨慎考虑从何处分配每个对象以及如何避免内存泄漏。总体上来说,Java 语言和面向对象编程的一大优势 —— 实现细节的封装 —— 在非堆环境中变成了一大薄弱环节,因为您不再能够预测和管理内存使用情况。

至此,我们已经设计好了内存模型,图 8 展示了更新后的系统图,其中标出了存储区:

图 8. 标出了存储区的高级架构图
标出了存储区的高级架构图

线程优先级

使用 WebSphere Real Time 进行开发时,选择恰当的线程优先级比标准 Java 编码中重要得多。糟糕的选择可能会使垃圾收集器抢占您的 NHRT,或导致部分系统耗尽 CPU 资源。

实时 Java,第 3 部分: 线程化和同步” 探讨了线程优先级的详细内容。对于我们的示例系统,设置优先级的目标如下:

  • 给予轮询线程最大优先级,从而最小化丢失度量结果的风险。
  • 避免筛选池线程被垃圾收集器中断。

为此,我们将轮询线程的线程优先级设置为 38(最高的 RT 优先级),将筛选池线程的优先级设置为 37。由于审计线程是一个常规 Java SE 线程,使用标准优先级 5,因此其优先级远远低于 NHRT。

这种配置意味这垃圾收集器线程的优先级略高于审计线程 —— 远低于 NHRT。

引导考虑事项:启动应用程序

至此,我们只观察了应用程序的稳定方面 —— 也就是说,它开始运行之后的工作方式。我们尚未考虑它如何启动。WebSphere Real Time 应用程序的启动与标准 Java 应用程序类似:在 Java 堆中的一个 java.lang.Thread 上运行。从这里,我们需要在其他存储区中启动一些线程类型。

在我们的应用程序中,所有的引导操作都是在 MonitoringSystemImpl 类的 startMonitoring() 方法中执行的,我们假设该类由堆内存中运行的一个 java.lang.Thread 调用。

我们的引导任务是:

  • 在不朽内存中创建一个或多个轮询线程。
  • 在不朽内存中创建一个或多个线程池对象,各入池线程也在不朽内存中创建和运行。
  • 在不朽内存中创建审计线程对象,在堆中运行。

可以使用 ImmortalMemory.newInstance() 方法,通过一个 java.lang.Thread 反射地在不朽内存中创建对象。对于带有少数几个构造方法参数的类,或者如果您正创建同一个类的多个方法,这是可行的,但是对于构造方法具有大量参数的类来说,很快就会变得杂乱无章。

java.lang.Thread 不同,RealtimeThread 可进入不朽内存来执行某些工作(通过向 ImmortalMemory.enter()) 提供实现 Runnable 的对象或将不朽内存作为线程的初始存储区提供)。这种方式的优势是您可以编写标准 Java 代码,每个 new 操作都将在不朽内存中创建一个对象。缺点是从不朽内存中运行的 RT 线程上基于堆的 java.lang.Thread 获取代码难免看上去有些混乱。

在示例代码中,我们编写了一个实用工具方法 —— Bootstrapper.runInArea,它获取一个 MemoryArea 和一个 Runnable 对象。在内部,它在所提供的存储区内启动一个短期的 RealtimeThread,从而执行 Runnable。这是一种较为干净的引导方法。

无论多么努力地尝试,使此类引导代码优雅、干净、易读也是非常困难的。在不回头参考架构图的前提下,为任何阅读代码的人解释存储区和线程类型间的不断切换都很困难,也将产生会令老练的 Java 编程人员迷惑不解的结构。最好的建议就是使此类代码本地化,在开发者文档中花上一番工夫去解释其背后的思想。

现在,我们已经介绍了设计中的主要组件,下面就可以体验一下最终得到的应用程序了。


演示

本文附带了上述设计的一个实现(下载源代码)。我们建议您阅读源代码,看看我们所讨论的理论如何实现为可运行的代码。

随同监控系的实现一起,我们还提供了一个虚拟的生产线和工人控制台,以供测试监控系统。罐子按正态分布装罐,偶尔会出现装得过多或者不满的罐子。

演示程序作为一个控制台应用程序运行,提供表明系统状况的消息。

构建演示程序

演示程序包中包含以下目录和文件:

  • src —— 演示程序的 Java 源代码。
  • build.sh —— 用于构建演示程序的 bash shell 脚本。
  • MANIFEST.MF —— 演示 JAR 文件的清单文件。

要构建演示程序,将程序包解压到任意目录,进入 SweetFactory 目录,运行 build.sh。您需要具有 WebSphere Real Time 提供的 jar、javac 和 jxeinajar 版本(可在 PATH 中找到),这样 build.sh 脚本才能正常工作。

build.sh 脚本执行一些操作:

  • 创建 bin 目录来存储类。
  • 使用 javac 构建 Java 源代码。
  • 构建一个可执行 JAR 文件 —— sweetfactory.jar。
  • 使用 jxeinajar 对 sweetfactory.jar 进行 AOT 编译。

运行构建脚本会生成如下输出:

清单 1. 构建脚本的输出
[andhall@rtj-opt2 ~]$ cd SweetFactory/
[andhall@rtj-opt2 SweetFactory]$ java -Xrealtime -version
java version "1.5.0"
Java(TM) 2 Runtime Environment, Standard Edition (build pxi32rt23-20070122 (SR1)
)
IBM J9 VM (build 2.3, J2RE 1.5.0 IBM J9 2.3 Linux x86-32 j9vmxi32rt23-20070105 (
JIT enabled)
J9VM - 20070103_10821_lHdRRr
JIT  - 20061222_1810_r8.rt
GC   - 200612_11-Metronome
RT   - GA_2_3_RTJ--2006-12-08-AA-IMPORT)
JCL  - 20070119
[andhall@rtj-opt2 SweetFactory]$ ls -l
total 16
-rwxr-xr-x  1 andhall andhall  773 Apr  1 15:41 build.sh
-rw-r--r--  1 andhall andhall   76 Mar 31 14:20 MANIFEST.MF
drwx------  4 andhall andhall 4096 Mar 31 14:16 src
[andhall@rtj-opt2 SweetFactory]$ ./build.sh
Working dir = .
Building source
Building jar
AOTing the jar
J9 Java(TM) jxeinajar 2.0
Licensed Materials - Property of IBM

(c) Copyright IBM Corp. 1991, 2006  All Rights Reserved
IBM is a registered trademark of IBM Corp.
Java and all Java-based marks and logos are trademarks or registered
trademarks of Sun Microsystems, Inc.

Found /home/andhall/SweetFactory/sweetfactory.jar
Converting files
Converting /home/andhall/SweetFactory/sweetfactory.jar into /home/andhall/
SweetFactory/aot//sweetfactory.jar
JVMJ2JX002I Precompiled 156 of 168 method(s) for target ia32-linux.
Succeeded to JXE jar file sweetfactory.jar

Processing complete

Return code of 0 from jxeinajar
[andhall@rtj-opt2 SweetFactory]$ ls -l
total 252
drwxrwxr-x  3 andhall andhall   4096 Apr  1 15:42 bin
-rwxr-xr-x  1 andhall andhall    773 Apr  1 15:41 build.sh
-rw-r--r--  1 andhall andhall     76 Mar 31 14:20 MANIFEST.MF
drwx------  4 andhall andhall   4096 Mar 31 14:16 src
-rw-rw-r--  1 andhall andhall 233819 Apr  1 15:42 sweetfactory.jar

y运行 build.sh 脚本生成了 sweetfactory.jar —— Sweet Factory 演示程序的一个 AOT 编译版本。

运行演示程序

现在,Sweet Factory 演示程序已经成功构建,可以运行了。演示程序是用 WebSphere Real Time v1.0 的 SR1 版本实现和测试的,建议您使用 SR1 或更新版本来运行它。

清单 2. Sweet Factory 演示程序
[andhall@rtj-opt2 ~]$ java -Xnojit -Xrealtime -jar sweetfactory.jar
Sweetfactory RTJ Demo

Usage:

java -Xrealtime -jar sweetfactory.jar [runtime seconds 
[number of production lines [production line period millis] ] ]

Default runtime is 60 seconds
Default number of production lines is 3
Default production line period (time between jars arriving) is 20 milliseconds
No arguments supplied - using defaults
Starting demo
1173021249509: Jar 32 overfilled
1173021250228: Jar 139 underfilled
1173021252770: Jar 521 underfilled
1173021260233: Jar 1640 underfilled
1173021260938: Jar 1746 overfilled
1173021263717: Jar 2162 underfilled
1173021264219: Jar 2238 overfilled
1173021272824: Jar 3528 overfilled
1173021272842: Jar 3529 underfilled
1173021276342: Jar 4054 overfilled
1173021280427: Jar 4667 underfilled
1173021281410: Jar 4815 overfilled
1173021286265: Jar 5542 overfilled
1173021288052: Jar 5810 underfilled
1173021288913: Jar 5940 overfilled
1173021294247: Jar 6739 underfilled
1173021298832: Jar 7426 underfilled
1173021305079: Jar 8362 overfilled
Stopping demo
Run summary:

Production line stats:
Line #  Sweet Type  Jar Type  # of Missed Jars  Max Triage Pool Size  Min Triage Pool Size
0       Giant Gobstoppers       Large   0       10      7
1       Chocolate Caramels      Large   0       10      8
2       Giant Gobstoppers       Large   0       10      8


Total missed jars: 0


Measurement object pool stats:
Minimum queue depth (degree of exhaustion): 391


Audit stats:
Maximum incoming queue depth: 5


Processing stats:
Total overfilled jars: 9
Total underfilled jars: 9
Total jars processed: 8998
Demo stopped
[andhall@rtj-opt2 ~]$

在输出中,您可以看到,默认情况下,演示程序会启动三条生产线,各罐子到达之间的延迟为 20 毫秒。

请注意,我们将 -Xnojit 选项传递给了 Java VM,以使其能够使用应用程序的 AOT 版本。

演示程序运行的时候,不同的罐子装得过多或不满,此时会向控制台打印一条消息,按一个时间戳挂起。最后,打印出一份表格,显示各生产线上遗漏了多少个罐子。

最后的统计表是对系统负载情况的度量结果。最小队列深度显示了度量对象池有多浅。如果池变空,那么我们就会遗漏罐子,因为轮询线程没有空间再去存储传入的度量结果。

审计最大传入队列深度显示了同一时刻有多少个 measurement 对象正在排队等候审计线程处理。如果这个数字较大,则提醒审计日志记录程序没有足够的时间进行处理,队列过大。

体验 Sweet Factory 演示程序

默认情况下,演示程序在 Opteron 硬件的功能中运行良好 —— 它就是在这种硬件上开发的;遗漏一个罐子没有什么危险的。然而,可将此演示程序参数化,增加生产线的数量、减少罐子到达的时间间隔。

通过更改参数,您可使机器更好地工作,如果过度降低工作负载,演示程序就会开始遗漏罐子。

演示程序接受 3 个参数:以秒计算的运行时间、生产线数量、以毫秒计算的罐子到达的时间间隔。

开始积极地调整工作负载之前,应注意生产线数量的增加会使演示程序中运行的线程数量线性增加。每隔 NHRT 都有一个作用域方法,因此增加线程的数量将会增加 —— 最终耗尽总作用域内存空间。

运行默认总作用域内存空间为 8MB 的 java -Xrealtime -verbose:sizes -version 即可查看这种情况:

清单 3. java -Xrealtime -verbose:sizes -version
[andhall@rtj-opt2 SweetFactory]$ java -Xrealtime -verbose:sizes -version
  -Xmca32K        RAM class segment increment
  -Xmco128K       ROM class segment increment
  -Xms64M         initial memory size
  -Xgc:immortalMemorySize=16M immortal memory space size
  -Xgc:scopedMemoryMaximumSize=8M scoped memory space maximum size
  -Xmx64M         memory maximum
  -Xmso256K       OS thread stack size
  -Xiss2K         java thread stack initial size
  -Xssi16K        java thread stack increment
  -Xss256K        java thread stack maximum size
java version "1.5.0"
Java(TM) 2 Runtime Environment, Standard Edition (build pxi32rt23-20070122 (SR1)
)
IBM J9 VM (build 2.3, J2RE 1.5.0 IBM J9 2.3 Linux x86-32 j9vmxi32rt23-20070105 (
JIT enabled)
J9VM - 20070103_10821_lHdRRr
JIT  - 20061222_1810_r8.rt
GC   - 200612_11-Metronome
RT   - GA_2_3_RTJ--2006-12-08-AA-IMPORT)
JCL  - 20070119
[andhall@rtj-opt2 SweetFactory]$

我们为各任务分配的作用域内存比较充足:每个 NHRT 100KB,我们为各生产线创建了 11 个 NHRT。可利用这些数字它来预估为使用 -Xgc:scopedMemoryMaximumSize 来尝试某些更积极的工作负载所需的作用域内存的总量。

例如,要以 10 毫秒的周期运行 50 个生产线,我们至少需要 55MB 作用域内存。我们将使用 60MB,以便有一定的活动余地。我们将用来运行这一场景 60 秒的命令是:

java -Xrealtime -Xnojit -Xgc:scopedMemoryMaximumSize=60M -jar sweetfactory.jar 60 50 10

如果您足够大地增加了生产线的数量(约 10 毫秒的间隔 70 个似乎是我们系统的极限),演示程序就会开始遗漏罐子。发生这种情况时,您将看到一条类似于下面这样的消息打印到控制台:

Error: measurement pool exhausted
1175439878160 : Missed 20 jars!

第一条消息来自轮询线程,当轮询线程尝试和未能成功从池中获取 measurement 时发出。第二条显示了轮询线程最终设法获得一个 measurement 对象时遗漏了多少个罐子。

在这些场景中,大多数 CPU 时间都用在处理传入的度量结果上。随着负载的增加,不再有足够的时间来运行 Metronome 和写出审计日志。度量结果在审计系统前构建到队列中,耗尽度量结果池。仅当度量结果用完、轮询线程被迫等待更多内容返回时,日志记录线程才会获取其写日志及将部分度量结果返回池。


实现提示与技巧

使用 WebSphere Real Time 约 1 年之后,我们归纳出一些使 RT 应用程序发挥最大作用的提示与技巧。这一节介绍了其中最有用的几条。

设定线程类型和存储区验证

使用非堆内存进行开发时,必须谨慎考虑您在哪个存储区中、何种线程之上执行代码的各行。

执行非法分配或(比如说)尝试从 java.lang.Thread 进入一个存储区非常有可能导致令人迷惑的 bug。

在代码中放置 assert() 语句来进行参数的健全性检查是 Java SE 中的一项良好的编程实践,在 RT Java 代码中,对线程上下文和您所在的存储区进行断言是明智的。

示例 Sweet Factory 应用程序包含专用的 ContextChecker 类,它提供一个 checkContext 方法和一组常量,来表示不同的上下文。

为错误处理预留 runnable 对象和存储区

在标准 Java 代码中 —— 多亏有其托管内存环境,错误处理只是又一块代码。在非堆 RT Java 中,错误处理则是一个大麻烦。

如前所述,您希望在 NHRT 上执行的大多数任务都占用内存,您必须为那些特殊的任务校准作用域的使用或轮询对象。

如果遇到错误,即便简单的行为,如打印一条错误消息,都会突然变得困难重重,因为您可能没有内存来执行这些操作。一种选择是在所有环境中提供足够的开销,在崩溃之前打印几行调试信息,但这不太现实。

我们发现的最好方法就是为各错误处理条件创建一个类,扩展 Runnable,并提供方法来提供关于故障的数据(这样您就能够获得足够的信息,了解究竟发生了什么)。预先创建此类的一个实例,以便随时使用,而无需占用内存。预留出足够大的作用域存储区来执行错误处理操作。

通过一个预先分配的 Runnable 对象和一个单独的作用域,在错误发生时,您就应该总是能够报告问题,而无需使用任何内存。对于不可能创建对象时抛出 OutOfMemoryError 之类的场景来说,这是非常有用的。

我们在 Sweet Factory 演示程序的 ProductionLinePoller 类中演示了这种技巧,其中定义:若无法从池中获取 Measurement,则使用 errorReportingRunnable


结束语

我们介绍了如何在 WebSphere Real Time 平台上开发和部署 RT Java 应用程序,从而满足日益紧迫的确定性特征要求。与编写普通的基于堆的应用程序相比,使用非堆内存的 NHRT 编程使工作量大大提高。考虑 Sweet Factory 演示程序。在堆环境中编写类似的功能只是小事一桩。Java SE 标准库已提供了我们需要的大部分功能,包括线程池和集合类。

使用 NHRT 的最大阻碍就是不仅有许多新技术要去学习,许多从 Java SE 中总结出来的最佳实践 —— 包括大多数模式 —— 均不适用,且会导致内存泄漏。

令人高兴的是,您可以使用 WebSphere Real Time 完成许多软 RT 目标,无需在 NHRT 上调用构造方法。Metronome 垃圾收集器的性能使您能够获得可预测的执行,达到几毫秒的精确度。然而,如果您需要最大化的响应性并乐于迎接挑战,WebSphere Real Time 的非堆特性将帮您实现目标。


下载

描述名字大小
本文的 Sweet Factory 演示j-rtj5sweetfactory.tgz16KB

参考资料

学习

获得产品和技术

  • WebSphere Real Time:WebSphere Real Time 使应用程序依托于精确的响应时间,利用标准 Java 技术,而未牺牲确定性。
  • 实时 Java 技术:访问 IBM alphaWorks 上的实时 Java 技术搜索站点,查找 TuningFork 和 RT 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=239278
ArticleTitle=实时 Java,第 5 部分: 编写和部署实时 Java 应用程序
publish-date=07112007