歡迎您光臨本站 註冊首頁

使用實時Java降低Java應用程序的易變性(1)

←手機掃碼閱讀     火星人 @ 2014-03-09 , reply:0

  一些Java應用程序未能提供適當的服務質量,儘管實現了其他性能目標,比如平均延遲或總吞吐量.通過引入不受應用程序控制的暫停或中斷機制,Java 語言和運行時系統有時可能使應用程序無法滿足服務性能指標.本文解釋JVM中的延遲和中斷的根源,介紹可用於減輕這些問題的技術,使您的應用程序能夠交付更加一致的服務質量.

  Java 應用程序中的易變性(通常是由暫停或延遲導致的,其發生時間無法預測)可能在整個軟體棧中發生.延遲可由以下因素引起:

  ?硬體(緩存期間)

  ?固件(處理 CPU 溫度數據等系統管理中斷的過程中)

  ?操作系統(響應一個中斷或執行定期調度的後台活動)

  ?在相同系統上運行的其他程序

  ?JVM(垃圾收集、即時編譯和類載入)

  ?Java 應用程序本身

  很難在較高級別上補償較低級別上的延遲,所以,如果您試圖僅在應用程序級別解決易變性,您可能只是轉移了 JVM 或 OS 延遲,並沒有解決實際問題.幸運的是,較低級別的延遲可能比較高級別上的延遲相對短一些,所以只有在降低易變性的需求非常強烈時,才需要深入到比 JVM 或 OS 更低的級別上.如果需求不是那麼強烈,您可以將精力集中在 JVM 級別上或應用程序中.

  實時 Java 提供了必要的工具來堵截 JVM 和應用程序中的易變性源頭,交付用戶要求的服務質量.本文詳細介紹 JVM 和應用程序級別上的易變性源頭,介紹可用於減輕其影響的工具和技術.然後介紹一個簡單的 Java 伺服器應用程序來演示其中一些概念.

  解決易變性源頭

  JVM 中的易變性主要源自於 Java 語言的動態特性:

  ?內存絕不會被應用程序顯式釋放,而是被垃圾收集器定期回收.

  ?類在被應用程序首次使用時才進行解析.

  ?本機代碼在應用程序運行時由即時(JIT)編譯器編譯(可以重新編譯),基於經常調用的類和方法.

  在 Java 應用程序級別上,線程管理是與易變性相關的關鍵區域.

  垃圾收集暫停

  當垃圾收集器回收程序不再使用的內存時,它可以停止任何應用程序線程.(這種類型的收集器稱為 Stop-the-world 或 STW 收集器).或者它可以與應用程序同時執行自己的一些工作.無論是哪種情況,垃圾收集器需要的資源都不能供應用程序使用,所以,眾所周知,垃圾收集(GC)是 Java 應用程序性能中的暫停和易變性的源頭.儘管許多 GC 模型都具有自己的優缺點,但當應用程序的目標是縮短 GC 暫停時,兩個主要的選擇將是分代(generational)和實時 收集器.

  分代收集器將堆組織為至少兩個部分,這兩個部分通常稱為新 和舊(有時稱為保留)空間.新對象始終在新空間中分配.當新空間耗盡空閑內存時,將僅在該空間中進行垃圾收集.使用相對較小的新空間可能時 GC 周期更短.在多次新空間垃圾收集過程中存留下來的對象會被提升到舊空間中.舊空間垃圾收集發生的頻率通常比新空間垃圾收集低得多,但是由於舊空間比新空間大得多,所以這些 GC 周期可能長得多.分代垃圾收集器提供了相對較短的平均 GC 暫停時間,但是舊空間收集的開銷可能導致這些暫停時間的標準偏差非常大.對於活動數據集不會經常更改,但會產生大量垃圾的應用程序而言,分代收集器是最有效的.在這種場景中,舊空間收集極少發生,因此 GC 暫停時間取決於短的新空間收集時間.

  與分代收集器相反,實時垃圾收集器會控制自身的行為,以顯著縮短 GC 周期的長度(通過在應用程序空閑時執行周期)或減輕這些周期對應用程序性能的影響(通過基於與應用程序之間的一種 「契約」,以更小的增量執行工作).使用這類收集器,您可以預測完成特定任務的最遭情形.例如,IBM? WebSphere? Real-Time JVM 中的垃圾收集器將 GC 周期劃分為較小的工作片段(稱為 GC 限額),這些限額可以增量方式完成.對限額的調度對應用程序性能的影響極小,其延遲可低至幾百微秒,通常小於 1 毫秒.為了達到這種延遲級別,垃圾收集器必須能夠計劃自己的工作,方法是引入應用程序利用契約 的概念.此契約管理允許 GC 中斷應用程序執行工作的頻率.例如,默認的利用契約為 70%,也就是在實時操作系統上運行時,僅允許 GC 使用每 10 毫秒中的至多 3 毫秒,典型的暫停時間大約為 500 微秒.

  在實時垃圾收集器上運行應用程序時,堆大小和應用程序利用率是要考慮的重要調優選項.隨著應用程序利用率的增加,垃圾收集器完成其工作的時間會更短,因此需要更大的堆來確保 GC 周期可以增量式地完成.如果垃圾收集器無法跟上分配速度,GC 將採用同步收集.

  例如,與在使用分代垃圾收集器的 JVM 上(未提供利用契約)運行時相比,在 IBM WebSphere Real-Time JVM 上運行的應用程序(具有 70% 的默認應用程序利用契約)默認需要更大的堆.由於實時垃圾收集器控制著 GC 暫停時間的長度,所以增加堆大小會降低 GC 頻率,不會延長各次暫停時間.另一方面,在非實時垃圾收集器中,增加堆大小通常會降低 GC 周期的頻率,這會降低垃圾收集器的總體影響.當發生垃圾收集時,暫停時間通常會更長(需要檢查更大的堆).

  在 IBM WebSphere Real Time JVM 中,可以使用 -Xmx 選項調整堆大小.例如,-Xmx512m 指定堆大小為 512MB.還可以調整應用程序利用率.例如,-Xgc:targetUtilization=80 將利用率設置為 80%.

  Java 類載入暫停

  Java 語言規範要求在應用程序首次引用類時對類進行解析、載入、驗證和初始化.如果對一個類 C 的首次引用發生在時間關鍵型操作期間,那麼解析、驗證、載入和初始化 C 的時間可能導致執行操作的時間比預期更長.由於載入 C 涉及到驗證該類(這可能需要載入其他類),所以 Java 應用程序為了能夠首次使用特定類而發生的總延遲可能比預期長很多.

  為什麼類只能在應用程序執行期間首次被引用?很少執行的路徑是載入新類的一個常見原因.例如,清單 1 中的代碼包含一個可能很少發生的 if 條件.(為了簡單起見,我們儘可能省略了本文中所有清單中的異常和錯誤處理).

  清單 1. 用於載入新類的很少執行的條件示例

  Iterator<MyClass> cursor = list.iterator();

  while (cursor.hasNext()) {

  MyClass o = cursor.next();

  if (o.getID() == 17) {

  NeverBeforeLoadedClass o2 = new NeverBeforeLoadedClass(o);

  // do something with o2

  }

  else {

  // do something with o

  }

  }

  異常類是只能在應用程序執行期間載入的類的另一個例子,異常在理想情況下(但不一定會遇到這種情況)很少發生.由於異常通常難以快速處理,所以載入額外的類的附加開銷可能使操作延遲超出重要閾值.一般而言,應該儘可能避免在時間關鍵型操作期間拋出異常.

  也可以在 Java 類庫中使用某些服務(比如反射)時載入新類.反射類的底層實現會動態生成將載入到 JVM 中的新類.在時間敏感型代碼中反覆使用反射類可能導致持續不斷的類載入活動,這會引起延遲.使用 -verbose:class 選項是檢測正在被創建的類的最佳方式.或許避免在程序執行期間創建這些類的最佳方式在於,避免在應用程序的時間關鍵型部分使用反射服務來從字元串映射類、欄位或方法.相反,在應用程序執行過程中儘早調用這些服務並存儲結果共以後使用,這可以避免在不需要時動態創建大部分這樣的類.

  一種在應用程序的時間敏感型部分避免類載入延遲通用技術是,在應用程序啟動或初始化期間預先載入類.儘管這個預載入步驟帶來一定的啟動延遲(改善一個指標通常會對其他指標帶來負面影響),但是如果小心使用,這一步可以在以後消除不需要的類載入過程.這種啟動流程很容易實現,如清單 2 所示:

  清單 2. 從一組類中以受控方式載入類

  Iterator<String> classIt = listOfClassNamesToLoad.iterator();

  while (classIt.hasNext()) {

  String className = classIt.next();

  try {

  Class clazz = Class.forName(className);

  String n=clazz.getName();

  } catch (Exception e) {

  System.err.println("Could not load class: " className);

  System.err.println(e);

  }

  注意 clazz.getName() 調用,它強制執行類初始化.構建類列表需要在應用程序運行時從其中收集信息,或者使用一個實用工具來確定應用程序將載入哪些類.例如,可以使用 -verbose:class 選項在程序運行時捕獲輸出.清單 3 顯示了在使用 IBM WebSphere Real Time 產品時,此命令的可能輸出:

  清單 3. 使用 -verbose:class 命令運行 java 的部分輸出

  ...

  class load: java/util/zip/ZipConstants

  class load: java/util/zip/ZipFile

  class load: java/util/jar/JarFile

  class load: sun/misc/JavaUtilJarAccess

  class load: java/util/jar/JavaUtilJarAccessImpl

  class load: java/util/zip/ZipEntry

  class load: java/util/jar/JarEntry

  class load: java/util/jar/JarFile$JarFileEntry

  class load: java/net/URLConnection

  class load: java/net/JarURLConnection

  class load: sun/net/www/protocol/jar/JarURLConnection

  ...

  通過存儲應用程序在執行時將載入的類列表,並使用該列表填充 清單 2 中顯示的循環的類名稱列表,可以確保在應用程序開始運行之前載入這些類.當然,不同時刻執行應用程序可能載入不同的路徑,所以一次執行的列表可能並不完整.出於此原因,如果應用程序正在開發之中,新編寫或修改的代碼可能依賴於未包含在列表中的新類(或者雖然包含在列表中,但不再需要的類)不幸的是,維護類列表可能是使用此方法預載入類的非常模麻煩的一部分.如果使用此方法,請記住,-verbose:class 輸出的類名稱與 -verbose:class does not match the format that's needed by Class.forName() 需要的格式並不匹配:詳細輸出中使用正斜杠將類包分開,而 Class.forName() 期望用句點來分開它們.

  對於存在類載入問題的應用程序,可以藉助一些工具來管理預載入,包括 Real Time Class Analysis Tool (RATCAT) 和 IBM Real Time Application Execution Optimizer for Java.這些工具能夠在一定程度上自動識別要預載入的正確類列表,以及將類預載入代碼合併到應用程序中.

  JIT 代碼編譯暫停

  JVM 中的第三個延遲來源是 JIT 編譯器.在應用程序運行時,它將程序的方法從 javac 生成的位元組碼翻譯為運行應用程序的 CPU 的本機指令.JIT 編譯器是 Java 平台取得成功的基礎,它實現了很高的應用程序性能,沒有犧牲 Java 位元組碼的平台獨立性.在過去 10 多年中,JIT 編譯器工程師一直在儘力改善 Java 應用程序的吞吐量和延遲.

  不幸的是,這類改進帶來了 Java 應用程序性能的暫停, JIT 編譯器從應用程序 「偷取」 了一些周期來為特定方法生成已編譯(或要重新編譯)的代碼.取決於被編譯方法的大小和 JIT 選擇編譯它的積極程度,編譯時間可能小於 1 微妙,也可能大於 1 秒(對於 JIT 編譯器發現的非常大的方法,這類方法會佔用應用程序的大量執行時間).但是 JIT 編譯器本身的行為並不是應用程序計時中的意外偏差的唯一來源. JIT 編譯器工程師將絕大部分精力都用在平均性能上,以最有效地改進吞吐量和延遲性能,所以 JIT 編譯器通常執行多種優化,這些優化 「通常」 是正確的或 「在大部分情況下」 具有很高的性能.一般而言,這些優化非常有效,並且開發了啟發方法來使優化很好地符合最常見的應用程序運行場景.但是,在一些情形下,這類優化可能帶來嚴重的性能易變性.

  除了預載入所有類,還可以請求 JIT 編譯器在應用程序初始化期間顯式編譯這些類的方法.清單 4 擴展了 清單 2 中的類預載入代碼,以控制方法編譯:

  清單 4. 受控的方法編譯

  Iterator<String> classIt = listOfClassNamesToLoad.iterator();

  while (classIt.hasNext()) {

  String className = classIt.next();

  try {

  Class clazz = Class.forName(className);

  String n = clazz.name();

  java.lang.Compiler.compileClass(clazz);

  } catch (Exception e) {

  System.err.println("Could not load class: " className);

  System.err.println(e);

  }

  }

  java.lang.Compiler.disable(); // optional

  這段代碼將使 JIT 編譯器載入一組類並編譯所有這些類的方法.一行為應用程序的其餘執行部分禁用 JIT 編譯器.

  與允許 JIT 編譯器自由選擇將編譯哪些方法相比,此方法通常會導致較低的總吞吐量或延遲性能.在 JIT 編譯器運行之前不必調用方法,JIT 編譯器僅擁有少量與如何最佳地優化它要編譯的方法相關的信息,所以這些方法的執行速度會更慢.,由於編譯器被禁用,不會重新編譯任何方法,即使這些方法佔用了程序執行時間的一大部分,所以,大多數現代 JVM 中使用的這類自適應 JIT 編譯框架將不起作用.要減少大量由 JIT 編譯器引起的暫停,不是必須使用 Compiler.disable() 命令,但是保留下來的暫停將是在應用程序的熱方法上執行的更加頻繁的重編譯,這通常需要更長的編譯時間,對應用程序計時的潛在影響更大.在調用 disable() 方法時,可能不會卸載特定 JVM 中的 JIT 編譯器,所以在應用程序運行時階段,仍然可能消耗內存、載入共享庫以及出現其他 JIT 編譯器工件.

  當然,本機代碼編譯對各個應用程序的性能的影響程度不盡相同.確定編譯是否存在問題的最好方法是打開詳細輸出,確定編譯發生的時間,進而確定它們是否影響應用程序計時.例如,使用 IBM WebSphere Real Time JVM,您可以使用 -Xjit:verbose 命令行選項打開 JIT 詳細日誌.

  除了這種預載入和早期編譯方法,應用程序作者無法執行太多操作來避免由 JIT 編譯器引起的暫停,但特定於供應商的 JIT 編譯器命令行選項除外(一種充滿風險的方法).JVM 供應商很少在生產場景中支持這些選項.由於它們不是默認的配置,所以供應商沒有很好地測試它們,它們在各個版本中的名稱和含義也可能不同.

  但是,一些替代的 JVM 可以為您提供一些選項,具體取決於 JIT 編譯器引起的暫停對您有多重要.設計用於硬實時 Java 系統的實時 JVM 提供了更多選項.例如,IBM WebSphere Real Time For Real Time Linux JVM 具有 5 種代碼編譯戰略,可以將它們與各種功能結合使用來減少 JIT 編譯器暫停:

  ?默認 JIT 編譯,JIT 編譯器線程在較低優先順序上運行

  ?較低優先順序上的默認 JIT 編譯,在最初使用了提前(Ahead-of-time,AOT)編譯代碼

  ?在啟動時受程序控制的編譯,啟用了重新編譯

  ?在啟動時受程序控制的編譯,禁用了重新編譯

  ?僅 AOT 編譯代碼

  這些選項根據預期的吞吐量/延遲性能級別和預期的暫停時間的降序來排列.默認的 JIT 編譯選項使用在最低優先順序(可能低於應用程序線程)上運行的 JIT 編譯線程,該選項提供了最高的預期吞吐量性能,但也可能顯示由(這 5 個選項的)JIT 編譯引起的最多的暫停.前兩個選項使用非同步編譯,這意味著如果應用程序線程嘗試調用被選擇用於重新編譯的方法,那麼該線程無需等到編譯完成.一個選項具有最低的預期吞吐量/延遲性能,但沒有由 JIT 編譯器引起的暫停,此方案完全禁用了 JIT 編譯器.

  IBM WebSphere Real Time for Real Time Linux JVM 提供了一個稱為 admincache 的工具,支持創建包含來自一組 JAR 文件的類文件的共享類緩存,也可以在相同代碼中存儲這些類的提前編譯代碼.可以在您的 java 命令行設置一個選項,以將存儲在共享類緩存中的類從緩存載入到 JVM 中,以及在載入類時將 AOT 代碼自動載入到 JVM 中.類似於 清單 2 中所示的類預載入循環已足夠確保您充分獲取提前編譯代碼的優勢.

  線程管理

  在交易伺服器等多線程應用程序中,控制線程的執行對於消除交易時間的易變性至關重要.儘管 Java 編程語言定義了一種線程模型,該模型包含線程優先順序的概念,但實際 JVM 中的線程行為主要由實現定義,包含 Java 程序可以依賴的許多規則.例如,儘管可以為 Java 線程分配 10 個線程優先順序中的一個,但這些應用程序級優先順序到操作系統優先順序值之間的映射是由實現定義的.(對於 JVM,將所有 Java 線程優先順序映射到相同的操作系統優先順序值是一種非常有效的方法).出於此原因,Java 線程的調度策略也是由實現定義的,但是通常在最終被分成一些時間段,所以即使是高優先順序的線程最終也會與低優先順序線程共享 CPU 資源.與較低優先順序線程共享資源可能導致在調度較高優先順序線程時出現延遲,以讓其他任務可以獲得一個時間片段.請記住,線程獲取的 CPU 量不僅依賴於優先順序,還依賴於需要調度的線程總數.除非可以嚴格控制在任何給定時間有多少活動線程,否則,即使是最高優先順序線程用於執行操作的時間也可能出現相對較大的差異.

  所以,即使您為工作者線程指定最高的 Java 線程優先順序(java.lang.Thread.MAX_PRIORITY),也不會提供與系統上較低優先順序任務之間太高的隔離級別.不幸的是,除了使用固定的工作線程集(不繼續分配新線程,而依賴於 GC 收集未使用的線程,或者擴大和縮小線程池)並嘗試將在應用程序運行時系統上低優先順序活動的數量減到最少,您無法採取其他更多措施,標準 Java 線程模型未提供控制線程行為所需的工具.在這裡,即使是軟實時 JVM(如果它依賴於標準 Java 線程模型)也不能經常提供幫助.

  但是,與標準 Java 相比,支持 Real Time Specification for Java (RTSJ) 的硬實時 JVM(比如 IBM WebSphere Real Time for Real Time Linux V2.0 或 Sun 的 RTS 2)可以提供大大改進的線程行為.在對標準 Java 語言和 VM 規範的增強中,RTSJ 引入了兩類新的線程 RealtimeThread 和 NoHeapRealtimeThread,它們的定義比標準 Java 線程模型要嚴格得多.這些線程類型提供了真正基於搶佔優先順序的調度機制:如果需要執行高優先順序任務並且處理器核心上目前計劃執行一個較低優先順序任務,那麼該較低優先順序任務將被搶佔,以便高優先順序任務可以執行.

  大部分實時 OS 都能夠在數十微秒內執行這種搶佔機制,這僅會影響到具有極高的計時需求的應用程序.兩種新線程類型都是用 FIFO(先進先出)調度策略,而不是在大部分 OS 上運行的 JVM 所使用的熟悉的循環調度策略.循環調度和 FIFO 調度策略之間最明顯的區別在於,在具有相同優先順序的線程中,一旦計劃讓一個線程繼續執行,那麼它只有在遇到阻塞或資源釋放處理器時才會停止.此模型的優點在於,執行特定任務的時間更加容易預測,處理器不是共享的,即使有多個具有相同優先順序的任務.出於此原因,如果您可以通過消除同步和 I/O 優先順序來使線程不受阻塞,在線程啟動之後,OS 將不會幹預它.但是,在實際中,消除所有同步非常困難,所以很難為實際任務實現這種理想目標.儘管如此,FIFO 調度機製為嘗試減少延遲的應用程序設計師提供了一項重要幫助.

  您可以將 RTSJ 想作一個大型工具箱,它可以幫助您設計具有實時行為的應用程序,您可以僅使用兩三個工具,或者可以重新編寫應用程序來提供高度可預測的性能.修改應用程序來使用 RealtimeThread 通常不會很困難,甚至可以在不訪問實時 JVM 來編譯 Java 代碼的情況下實現這一點,但是請小心使用 Java 反射服務.

  但是,要利用 FIFO 調度機制的易變性優勢,可能需要進一步更改您的應用程序.FIFO 調度的工作方式與循環調度不同,這些區別可能導致一些 Java 程序掛起.例如,如果應用程序依賴於 Thread.yield() 來允許其他線程在核心上運行(這種技術經常用於在不使用整個核心的情況下輪詢某個條件),那麼預期的效果將不會發生,使用 FIFO 調度機制,Thread.yield() 不會阻塞當前線程.由於當前線程仍然是可調度的,並且它位於 OS 內核中的調度隊列的最前面,所以它將繼續執行.因此,意圖提供對 CPU 資源的公平訪問(等待某個條件變為真)的編碼模式實際上將消耗運行它的整個 CPU 核心.而這是最可能的結果.如果需要設置此條件的線程具有較低的優先順序,那麼它可能始終不能訪問核心來設置條件.在如今的多核處理器中,這種問題可能很少發生,但是它強調您需要謹慎考慮在採用 RealtimeThread 線程時使用哪些優先順序.最安全的方法是讓所有線程使用一個優先順序值,不使用 Thread.yield() 和將消耗整個 CPU(它們不會被阻塞)的其他類型的自旋循環.當然,充分利用可用於 RealtimeThread 的優先順序值將最容易滿足服務質量目標.


[火星人 ] 使用實時Java降低Java應用程序的易變性(1)已經有760次圍觀

http://coctec.com/docs/java/show-post-60158.html