程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 實時Java,第5部分 - 編寫和部署實時Java應用程序

實時Java,第5部分 - 編寫和部署實時Java應用程序

編輯:關於JAVA

本系列 的前幾篇文章討論了 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

如果在所返回的信號中出現任何延遲(例如,因 GC 暫停引起的延遲),計算出的登月艙位置就是錯誤的。所返回的雷達脈沖時間較長就意味著更遠的距離,Controller 隨後將根據不正確的估計位置作出調整。顯然,這會給登月艙或任何 RT 系統造成災難性的後果。

顯示標准 Java 不適合運行 RT 應用程序的方法之一就是:度量 Controller 能夠多麼准確地保持登月艙位於正確的軌道上,以及著陸的成功情況如何。圖 2 中的圖表顯示了對使用標准 Java VM 的 Controller 的模擬。紅線顯示了登月艙的實際位置,藍線顯示了雷達度量的位置。

圖 2. 使用標准 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 的模擬

在這次也就是第二次運行中,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 中的顯示效果

一旦 GC 暫停被最小化,其他可能給一個運行中的應用程序造成干擾的因素就變得重要起來。其中之一就是即時(JIT)編譯器的活動。將 Java 字節碼編譯成本地文件實際上是為了獲得更好的性能,但生成本地代碼時可能會導致暫停。此問題的解決方案之一是使用 AOT 編譯預先編譯 Java 字節碼。

應用程序的 AOT 編譯

Java 運行時通常使用一個 JIT 編譯器,為一個 Java 應用程序內執行最頻繁的方法動態生成本地代碼。在 RT 環境中,有些應用程序(比如說有著嚴格的截止日期的應用程序)可能無法容忍與動態編譯活動相關的不確定性。而對於其他一些應用程序,編譯眾多用於啟動一個復雜應用程序的負載也是不合人意的。面臨這些問題的應用程序開發人員能夠受益於使用 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 隨後即可執行此文件。

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

切記,在 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 還添加了對兩個新線程類的支持 —— RealtimeThread 和 NoHeapRealtimeThread(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 類圖

設計解決方案

既然已經有了規范,那麼就可以設計解決方案了。問題可以分解成兩部分:第一,輪詢生產線,並檢查罐子的質量;第二,寫審計日志。

輪詢生產線

如果考慮 WeighingMachine 接口,weighJar() 方法需要頻繁輪詢,因此明智的做法是為每個 ProductionLine 使用一個專用線程,使設計可伸縮。我們將使用一個 NHRT 來最小化輪詢被垃圾收集器中斷的線程以及丟失度量結果的可能性。

對一個罐子進行稱重之後,我們需要計算出該質量等於多少糖果,並將其與目標值相比較。預測一次度量所需進一步處理的數量極為困難,如果一個罐子中的糖果數量超出容許偏差,那麼我們就必須考慮與 WorkerConsole 通信,這可能要花上幾秒鐘的時間。

經過 10 毫秒之後,可能又會有大批罐子傳送到此,因此我們顯然不能在輪詢的線程上進行計算。我們需要將度量結果傳遞給一個單獨的計算線程。由於某些處理可能要占用較長的時間,因而需要為每條生產線使用多個處理線程,確保總有一個線程能來處理最新的度量結果。

可為所產生的每個數據片段生成一個新線程,但這會將大量處理器時間浪費在啟動和停止線程上。為更好地利用 CPU 時間,我們可以創建一個 NHRT 池來處理數據,通過維護一個運行中線程的池,在運行的時候就不存在任何線程啟動和停止開銷了。

可以使用一個由全部生產線共享的線程池,但任何可由多個線程共享的數據結構都需要同步。使用單獨一個線程池可能會導致嚴重的鎖爭用。為了使我們的解決方案可伸縮,每條生產線都將附有自己的小線程池。

線程池的設計涉及到多方面的考慮事項,例如池的大小和管理技術,這些內容超出了本文討論范圍。就本文的目的而言,我們將為每個 ProductionLine 對象創建 10 個入池線程,如果出於某些原因耗盡線程,我們還會擴展線程池。

寫審計日志

與本系統中的其他組件不同,審計日志記錄組件並非時間關鍵的。如果我們(天真地)忽視了計算機崩潰或關閉的可能性,那麼惟一重要的考慮事項就是在某些時刻記錄的度量結果了。

考慮到這一點,我們將使用一個 java.lang.Thread 來寫出到日志文件。在 NHRT 等待更多工作、垃圾收集器不活動時,它將完成這一工作。這樣的設計決策具有廣泛的影響,原因在於我們在傳統基於堆的環境和 NHRT 的非堆環境之間引入了一個接口。稍後您將看到,在處理這個接口時需要格外謹慎。

圖 7 是該架構的高級示意圖:

圖 7. 高級架構圖

現在您已經對希望 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 編程人員,可能會嘗試使用一個現有的 LinkedList 或 Queue 類,來提供一個數據存儲,容納我們的度量結果。然而,這種做法不會成功,原因有二:第一個原因是大多數 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 的非堆特性將幫您實現目標。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved