目錄

一:概述、方法及需求

二:JVM部署模型和JVM Runtime

三:GC優化基礎

四:決定Java堆的大小以及內存佔用

五:優化延遲或者響應時間(1)

五:優化延遲或者響應時間(2)

五:優化延遲或者響應時間(3)

六:優化吞吐量

七:其他


目錄

一:概述、方法及需求

   現代JVM是一個具有靈活適應各種應用能力的軟件,儘管很多應用能夠在JVM的默認配置下運行良好,但是有些應用還是需要優化JVM配置以達到其性能要求。由於各種各樣的應用能夠運行在現在JVM上面,所以大量的JVM選項可以配置來提升應用的性能。不幸的是,對一個應用而言優化得很好的JVM配置,對應另外的應用不一定適合。所以,真正理解怎樣優化JVM配置是非常有必要的。

   優化現代JVM是一門很大的藝術,但是理解和應用一些基本知識能夠讓優化JVM的任務變得更加簡單。本章就是介紹這些基本知識和一些常規的步驟去優化Java HotSpot虛擬器。為了更好的理解本章的內容,你應該對JVM和垃圾回收器有一些基本的瞭解。

   本章以一步步的優化方法包括一些假設開始,在優化JVM之前,你需要先知道怎樣測試應用性能、性能需求、測試的基礎工具以及用來收集數據的垃圾回收器的命令行選項。接下來有幾個章節來説明怎麼樣一步步優HotSpot虛擬器行為——啓動、內存的佔用、吞吐量、延遲等。

1、方法

   下面這張圖片展示了本章要説明的方法。他由一些清晰的應用性能需求開始,這個性能需求應該是應用負責人排過優先級的。與描述計算什麼及輸出什麼的功能層面需求相比較,系統層面需求描述了系統的一些指標,比如:吞吐量、響應時間、內存消耗、啓動時間、可用性以及易管理性等等。

java 響應大小_垃圾回收器

   下一節,我們仔細看看每項系統指標對優化JVM的重要作用。

   優化JVM性能涉及很多權衡,當你提升某一項性能指標的時候,往往需要犧牲其他指標。比如説,為了最少的消耗內存,往往需要以延遲或者響應時間作為代價。或者,你想要提升應用的易管理性,你需要降低應用的的可用性的級別,由於可用性的提升是建立在多個JVM上的,多個JVM可用降低一個JVM出錯造成整個應用的無法使用的風險。由於有很多的取捨需要做,理解真正性能需求就變得極其重要了,理解需求才能夠正確的使用方法。

   一旦你知道了哪一些系統指標是重要的,接下來要做的就是選擇JVM的部署模型,選擇是部署在多JVM上面還是單個JVM上面。可用性、易管理性以及內存的佔用等系統指標在選擇合適的部署模型的時候都扮演了重要角色。

   接下來就是選擇JVM的Runtime,HotSpot虛擬器提供了聚焦在更快的啓動速度和更小的內存佔用的32位的client虛擬器,以及在32位和64位系統中有更高的吞吐量server虛擬器。系統對吞吐量、響應時間以及啓動時間的需求決定了對JVM Runtime的選擇。

   接下來就是要優化垃圾回收器,以滿足系統對內存佔用、延遲以及吞吐量的需求,我們按照首先內存佔用,其次延遲時間,最後吞吐量的順序來進行優化。

   優化是在不停地測試和調整配置中循環的,需要數次循環以達到性能的需求,另外,也有可能優化了一個點的時候,但是需要回到前面幾個步驟重新進行檢查。比如,假如你在幾次優化垃圾回收器之後,對應用的延遲還是不滿意,這個時候就有必要調整JVM的部署模型。另外一種可能是,應用程序有修改或者需要重新設定應用程序的性能需求。

   對於一些應用以及它們的系統需求來説,需要循環幾次這樣的操作,直到應用責任人對應用的性能滿意為止。

2、假設

  這個一步步的優化步驟,是基於應用都有以下執行過程的假設:

  1、初始化階段——初始化重要的數據結構和其他需要使用的依賴庫。

  2、穩定階段——應用消耗大部分的時間執行其核心函數。

  3、可選的總結階段——比如需要製作報告。

  穩定階段是我們需要主要關注的地方。

3、測試基礎設施

   為了做出關於內存佔用、延遲、吞吐量以及啓動時間等優化有根據的決定,並且為了證實選擇的JVM運行環境是正確的,我們需要從試驗中收集數據(需要注意的是這個試驗要能夠反映生產環境的實際情況)。因此,有一個能夠代表生產環境的性能測試環境就相當重要了。包括硬件和軟件都需要代表生產環境。簡單的説,測試環境和生產環境越接近,做出來的優化決定越靠譜。

下面,我們詳細介紹需求的定義。

4、性能需求詳細描述   從前面我們知道,系統層面的需求決定應用的某一方面的特性,比如它的吞吐量、響應時間、消耗的內存、它的可用性以及易管理性等等。另外,功能需求決定了應用計算的內容或者產生的輸出。

接下來的我們描述一下我們會涉及到層面的需求。

可用性

   可用性是衡量應用處於可用狀態的指標。可用性需求表明了當應用的某些組件損壞或者遇到錯誤的時候,整個應用或應用的其他部分處於可用狀態。

   在Java應用領域,高可用性可以通過把系統的分隔成各個組件,然後運行在不同JVM上面或者在多個JVM上面運行相同應用實例來實現。一個需要平衡的點是,當提升系統的可用性,系統的維護成本會升高。引入更多的JVM或者機器,那麼就有更多的JVM需要管理,這個就是造成了成本的升高和複雜性的提升。

   我們常見的可用性需求例子:“當系統某一部分出現錯誤的時候,不要讓整個應用程序崩潰”。

易管理性

   易管理性是衡量系統的運行和監控的成本以及配置應用的成本。易管理性的需求表明了這個應用被管理的容易程度。通常來講,用更少的JVM去運行應用,那麼需要付出更小的成本去維護和監控應用。而且更少的JVM應用配置也更加簡單,但是這個是建立犧牲應用的可用性上面的。

一個簡單的易管理性需求例子:“由於有限的資源,應用只能部署到儘量少的JVM上面。”

吞吐量

   吞吐量是衡量系統在單位時間裏面完成的工作數量。吞吐量需求通常忽略延遲或者響應時間。通常情況下,提升吞吐量需要以系統響應變慢和更多內存消耗作為代價。

   一個吞吐量的例子:“這個應用需要每秒完成2500個事務。”

延遲和響應時間

   延遲或者響應時間是衡量應用從接收到一個任務到完成這個任務消耗的時間。一個延遲或者響應時間的需求需要忽略吞吐量。通常來講,提升應用的響 應時間需要以更低吞吐量或提高應用的內容消耗。

一個延遲或者響應時間的例子:"這個應用會在60毫秒內,執行完成交易操作。"

內存佔用

   內存佔用是衡量應用消耗的內存,這個內存佔用是指應用在運行在某一個吞吐量、延遲以及可用性和易管理性指標下的內存消耗,內存佔用是通常描述為應用運行的時候Java堆的大小或者總共需要消耗內存。通常情況下,通過增加Java堆的大小以增加應用內存佔用可以提升吞吐量或者減少延遲,或者兩者兼具。當應用可用的內存減少的時候,吞吐量和延遲通常會受到損失。在給定內存的情況下,應用佔用的內存可以限制應用的實例數(這個會影響可用性)。

   一個例子説明內存佔用的需求是:“這個應用會單獨運行在一個8G的系統上面或者多出3個應用實例運行在一個24G的應用系統上面。”

啓動時間

   啓動時間是衡量應用初始化的時間(如:eclipse的啓動時間)。在Java應用中,大家可能對JVM優化應用的熱點需要的時間感興趣。Java應用初始化需要消耗的時間依賴於很多因素包括單不僅限於需要裝載的類的數量、需要初始化的對象數量、並且這些對象怎麼初始化,以及HotSpot虛擬器運行環境的選擇(client or server,eclipse使用的HotSpot Client,Jboss會使用HotSpot Server,兩者在初始化時間上和運行過程中對熱點的優化不一樣)。

   拋開需要加載的類的數量、需要初始化的對象的數量以及對象如何初始化,使用HotSpot client運行環境會獲得更快的啓動速度,由於他沒有做足夠的優化,這些優化可以提供更高吞吐量和更低的延遲。相反,HotSpot Server運行環境需要更長的啓動時間,由於它需要更好多的獲得應用關於Java代碼的信息,並且對生成的機器碼進行了很高優化。

   啓動時間需求的例子如:“這個應用會再15秒內完成初始化。”

5、對系統需求進行優先級排序

   優化操作的第一步就是對系統層面的需求進行優先級排序。做這個需要把主要的應用負責人叫到一起來商定優先級的排序,並且最終達成一致。這個討論需要在應用的架構和設計階段完成,由於這個討論可以提供非常明確的結論,比如説:什麼系統需求是最重要的。

   對於應用的負責人來説,系統需求的優先級決定了優化操作。最重要的系統需求促使形成一些基本決定。比如説:可用性比易管理性重要,那麼JVM部署模型就會採用部署多個JVM。相反如果易管理性比可用性重要,那麼就更加傾向於選擇單個JVM的部署模型。

   如何選擇JVM部署模型和JVM Runtime會在接下來的一節中講到。

 

二:JVM部署模型 和 JVM Runtime

1、選擇JVM部署模型

   JVM部署模型的選擇總體來説就是決定應用是部署在單個JVM實例還是多個JVM實例上(這裏簡單舉例説明一下JVM實例,比如:我們常用eclipse開發,啓動一個eclipse就是啓動了一個JVM實例,然後在JVM中運行一個main程序、又會啓動一個JVM實例,兩個JVM實例是隔離開的)。哪一個是最適合你的應用的呢?這個是前面説到系統需求和潛在規則來決定的。比如説:假如你要部署您的應用在一個64位的機器上面,可以支持更大Java堆,如果應用依賴第三方的本地代碼組件,而且這個第三方暫時不支持64位機器,那麼你就必須要強制使用32位的JVM而且要使用更小優化的Java堆。

2、單實例JVM模型

   在單實例的JVM上部署應用,有一個好處,就是可以減低管理成本,畢竟有更少的JVM需要去維護嘛。應用能夠使用的總內存更小,由於每一個單獨部署的JVM有能夠使用的內存上限。

   部署應用在單個JVM上存在的挑戰是應用的可用性存在極高的風險,比如:JVM失敗或者應用災難性錯誤。

3、多實例JVM模型

   部署Java應用在多個JVM上面有提高可用性和能夠間接降低延遲的好處,由於應用部署在多個JVM上,某一個JVM出錯,只會導致應用某部分無法使用,不會導致整個應用無法使用。多JVM部署可以提供低延遲,在多JVM部署中,Java堆的大小傾向於更小,更小Java堆可以允許有更小的垃圾回收暫停,垃圾回收器的暫停是明顯影響延遲。另外,如果應用存在明顯瓶頸,多個JVM部署可能幫助提升吞吐量,把壓力分佈到多個JVM上面,可以讓用承受更大的壓力。

   使用多個JVM,JVM可能會和處理器綁定。把JVM和處理器綁定在一起,可以避免應用和JVM的線程在多個CPU上切換,提升CPU cache的命中率。

   部署多JVM的挑戰在於管理、監控和維護需要更多的努力。

4、一般的建議

   沒有最好的JVM部署模型,做出最合適的選擇依賴於系統的需求,系統的需求才是最重要的。

   一個約束需要意識到的是,如果Java應用需要大量的內存佔用,把應用部署在單個JVM上面可能需要使用64位JVM上,64位可以提供比32位JVM更大Java堆大小。如果使用64位JVM,需要保證應用使用的任何第三方軟件需要支持64位。另外,如果任何使用JNI去調用本地組件,不管是第三方組件還是自己應用開發的程序,需要確保的是他們必須在64位環境下編譯。

   根據作者的經驗來看,越少的JVM數量越好,畢竟越容易維護。

5、選擇JVM Runtime

   為Java應用選擇JVM Runtime,就是根據實際情況來選擇使用合適的client或者server。

6、Client Runtime 或者 Server Runtime

   當使用HotSpot VM的時候,有兩種可以選擇的JVM runtime。client runtime是快速啓動,更小的內存佔用以及快速代碼(機器碼)生成的JIT編譯器。server runtime有更復雜的代碼生成優化,作為服務型應用更為靠譜。在server runtime中可以發現針對JIT編譯器有很多的優化,主要是收集了更多的關於程序的信息,以生成更高性能的代碼。

   第三種HotSpot VM runtime還在開發中,叫做tiered,他結合了client和server runtime優秀面,即更快的的啓動時間和更高性能的生成代碼。如果你使用的是Java 6 Update 25,Java 7或者更新的版本,你也許可以考慮使用tiered server runtime替換client runtime。要使用tiered server runtime可以使用這個命令行選項:-server -XX:+TieredCompilation。在這本書編寫的時候,還不是極力推薦替換掉client runtime或者server runtime。

   小提示:如果你不知道該如何選擇,可以先選擇server runtime。如果啓動時間和內存佔用無法接受,可以考慮切換成client runtime或者tiered runtime。依賴於你是使用什麼版本的JVM。

7、32位 或者 64位JVM

    除了client和server runtime的選擇,還需要在32位或者64位之間做出選擇,HotSpot VM的默認配置是32位的。做出32位和64位的選擇取決於應用需要的內存佔用以及依賴的第三方庫是否支持64位系統——如果有通過JNI使用本地接口。決定應用需要消耗的內存佔用,會在下節中介紹。下面的表格列出了一些指導幫助在32位JVM或者64位JVM之間做出選擇。注意的是HotSpot VM還沒有64位的client runtime。

java 響應大小_垃圾回收器_02

8、垃圾回收器

    在進入下一步優化之前,需要先做出選擇初始的垃圾回收器。在HotSpot VM裏面有好幾種垃圾回收器可以使用:serial,throughput,mostly concurrent以及garbage first。

   由於使用throughput垃圾回收器有可能能夠滿足應用對暫停時間的需要,可以優先選擇throughput垃圾回收器,如果有需要再切換到concurrent垃圾回收器(CMS)。如果有切換到concurrent垃圾回收器的需要,我們將會在後面的優化延遲的時候討論 。

   使用throughput垃圾回收器可以通過HotSpot VM的命令行參數來指定:-XX:+UseParallelOldGC或者-XX:+UseParallelGC。 如果在你的HotSpont VM的版本上-XX:+UseParallelOldGC選項不能使用,使用+XX:UseParallelGC。兩者的不同點在於,-XX:+UseParallelOldGC促發了多線程young代的垃圾回收和多線程old代的垃圾回收,也就是説minor垃圾回收和full垃圾回收都是多線程的。+XX:+UseParallelGC僅僅是young代的垃圾回收是多線程的,old代的垃圾回收的單線程的。因此,如果你想要young代和old代的垃圾回收都是多線程的就配置-XX:+UserParallelOldGC。而且-XX:+UserParallelOldGC是兼容-XX:+UseParallelGC。

 

三:GC優化基礎

   本節主要描述關於垃圾回收器性能的三個指標,三個關於垃圾回收器優化的基本原則,以及優化HotSpot VM的垃圾回收器的信息收集,在這些指標中權衡以及信息的收集是非常重要的。

1、性能指標

   吞吐量:衡量垃圾回收器運行在性能峯值的時候不需要關心垃圾回收器暫停的時間或者需要佔用內存的能力。

   延遲:衡量垃圾回收器最小化甚至消滅由垃圾回收器引起的暫停時間和應用抖動的能力。

   內存佔用:衡量為了高效的運行,垃圾回收器需要的內存。

   一項指標的提升,往往需要犧牲其他一項或者兩項指標。換一句話説,一項指標的妥協通常是為了支持提升其他一項或者兩項指標。然而,對於大多數應用來説,很少有3項指標都非常重要,通常,一項或者兩項比其他的更重要。

   由於始終需要各種權衡,那麼知道哪項指標對應用是最有必要的就顯得非常重要。

2、原則

   在優化JVM垃圾回收器的時候,有3項基本原則

  • 在minor垃圾回收器中,最大量的對象被回收,這個被稱為Minor GC回收原則。秉承這個原則可以減少由應用產生的full垃圾回收數量和頻率,Full垃圾回收往往需要更長的時間,以致於應用無法達到延遲和吞吐量的需求。
  • 更多的內存分配給垃圾回收器,也就是説更大的Java堆空間,垃圾回收器和應用在吞吐量和延遲上會表現得更好,這條原則被稱為GC最大內存原則。
  • 優化JVM垃圾回收器的3個指標中的2個,這個被稱為2/3 GC優化原則。

   在進行優化JVM垃圾回收器的時候, 牢牢記住這三條原則會讓你的優化任務更容易完成。

3、命令行選項和GC日誌

   從垃圾回收器獲取監控信息,是優化JVM的重要操作。收集垃圾回收器信息的最好辦法就是收集日誌。這個意味着通過HotSpot VM的命令行選項可以收集垃圾回收器的統計信息。開啓垃圾回收器日誌(即使在生產環境)是很好的主意,其實開啓垃圾回收器的開銷很小而且可以提供豐富的信息,這些信息和垃圾回收器應用事件或者JVM事件有關係,比如説:一個應用在運行過程中出現了一個比較長的暫停,如果有垃圾回收信息,就可以判斷出是垃圾回收器引起的暫停還是應用進行的其他操作引起的暫停。

   有很多的HotSpot VM命令行選項可以用在垃圾回收的日誌上面,下面列舉幾個推薦使用的命令行選項:

-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:<filename>

   -XX:+PrintGCTimeStamps打印出垃圾回收發生的時間是距離HotSpot VM啓動時間的秒數。

  -XX:+PrintGCDetails提供了垃圾回收特有的統計信息而且具體信息依賴於使用的垃圾回收器類型。

  -Xloggc:<filename>表示垃圾回收器的信息輸出到叫<filename>的文件。

   下面是通過使用-XX:+UseParallelOldGC或者-XX:+UseParallelGC選項來打印出來的垃圾回收信息,而且使用了前面列出的3個選項。

45.152: [GC [PSYoungGen: 295648K->32968K(306432K)] 296198K->33518K(1006848K), 0.1083183 secs]
[Times: user=1.83 sys=0.01, real=0.11 secs]

   45.152是表明距離JVM啓動到垃圾回收的秒數,

   GC標籤表明是Minor GC或者young代垃圾回收,

   [PSYoungGen: 295648K->32968K(306432K)]提供了young代的空間信息,PSYoungGen表示young代的垃圾回收是使用throughput垃圾回收器。其他可能的young代垃圾回收有ParNew(使用CMS垃圾回收器多線程回收young代空間)、DefNew(使用serial垃圾回收器單線程回收young代空間)。

   在“->”左邊的數字(295648K)表示垃圾回收前young代的空間大小,右邊數字(32968K)表示垃圾回收後的young代空間大小。young代被分為eden區域和survivor區域。由於在垃圾回收之後,eden區域是空的,右邊的數字其實就是survivor區域的空間。在括號裏面的數字(306432K)表示young代的總空間。

   296198K->33518K(1006848K)提供了Java堆在垃圾回收前後的使用情況。另外,他提供Java堆的總大小,是young代和old代的和。在->左邊的數字(296198K)表示,在垃圾回收前Java堆佔用的空間,->右邊的數字(33518K)表示垃圾回收後Java堆佔用空間。括號裏面的數字(1006848K)表示Java堆總共的空間。

   通過young代的佔用空間和Java堆佔用的空間,可以快速的計算出old代佔用的空間。比如:Java堆的大小是1006848K,young代的空間大小是306432K,因此可以計算出old代的空間大小是1006848K-306432K=700416K。在垃圾回收之前,296198K-295648K=550K是old代使用了的空間。在垃圾回收後33518K-32968K=550K。在這個例子中,在垃圾回收前後沒有對象從young代移動到old代。這是一個重要的觀察説明了Minor GC回收原則。如果有對象被移動到old代,然後變成不可讀取的,就不是最大量的對象被回收,會違反Minor GC回收原則。

   0.1083183 secs表明垃圾回收執行的時間。

   [Times: user=1.83 sys=0.01, real=0.11 secs]提供了CPU和佔用時間。user表明垃圾回收在用户模式下執行消耗的CPU時間,即:在JVM裏面運行的時間,在這個例子中,垃圾回收器在用户模式下消耗1.83秒的CPU時間。sys表示操作系統代表垃圾回收器消耗的時間,在這裏例子中,垃圾回收器使用0.01秒的操作系統CPU時間。real表示垃圾回收執行的時間的。這幾個數字精確到0.01秒。

-XX:+PrintGCDetails選項的結果:

2012-06-21T09:57:10.518-0500: [GC[PSYoungGen: 295648K->32968K(306432K)]296198K->33518K(1006848K), 0.1083183 secs]
[Times: user=1.83 sys=0.01, real=0.11 secs]

   2012-06-21T09:57:10.518-0500字段是使用了ISO 8601日期和時間戳。格式是YYYY-MM-DDTHH-MM-SS.mmm-TZ,分別的意思是:

    YYYY表示4位數的年

    MM表示2位數月,如果只有一位數,前面加0

    DD表示2位數的天,如果只有一位數,前面加0

    T是一個字符用來隔開日期和時間

只有一位數,前面加0

只有一位數,前面加0

只有一位數,前面加0

    mmm表示3位數毫秒,如果不足三位,前面加0或者00

    TZ表示格林尼治時間的時區

   儘管時區已經包含在輸出裏面了,但是輸出日期和時間不是GMT時間,而是本地化過的時間。

   當為了低延遲而優化HotSpot VM的時候,下面的兩個選項是非常有用的,這兩個選項會報告應用由於虛擬機的安全點(Safepoint)操作而阻塞的時間以及應用程序在安全點(Safepoint)執行了多長的時間。

-XX:+PrintGCApplicationStoppedTime  

-XX:+PrintGCApplicationConcurrentTime

   安全點(Safepoint)操作會讓JVM進入一種所有應用程序的線程都被阻塞以及阻止任何正在執行的本地程序把結果返回給Java代碼的狀態。當需要進行優化虛擬機內部操作的時候,安全點(Safepoint操作會被執行以使得所有線程都進入阻塞狀態避免影響Java堆(垃圾回收是一種安全點(Safepoint操作)。

   由於安全點(Safepoint)操作阻塞了所有Java程序的執行,所以知道程序的響應時間延遲是否和安全點(Safepoint)操作有關係就顯得非常重要了。因此,能夠觀察程序什麼時候被阻塞( 通過設置-XX:+PrintGCApplicationStoppedTime選項)通過應用的日誌信息能夠幫組你識別出,當應用的響應時間超過預期的時候,是安全點(Safepoint)操作引起的還是應用或者操作系統的其他操作引起的。-XX:+PrintSafepointStatistics可以幫助區別垃圾回收的安全點(Safepoint)以及其他的安全點(Safepoint)。

   在發現應用的響應時間超過預期的預期的時候,-XX:+PrintGCApplicationConcurrentTime選項可以用來判斷程序是否被執行以及執行了多長時間。

   下圖總結了前面提到的垃圾回收器的選項以及給出了使用它們的合適情況。

java 響應大小_hotspot_03

java 響應大小_垃圾回收器_04

四:決定Java堆的大小以及內存佔用

   到目前為止,還沒有做明確的優化工作。只是做了初始化選擇工作,比如説:JVM部署模型、JVM運行環境、收集哪些垃圾回收器的信息以及需要遵守垃圾回收原則。這一步將介紹如何評估應用需要的內存大小以及Java堆大小。首先需要判斷出應用存活的數據的大小,存活數據的大小是決定配置應用需要的Java堆大小的重要條件,也能夠決定是否需要重新審視一下應用的內存需求或者修改應用程序以滿足內存需求。

   注意:存活數據是指,應用處於穩定運行狀態下,在Java堆裏面長期存活的對象。換一句話説,就是應用在穩定運行的狀態下,Full GC之後,Java堆的所佔的空間。

1、約束

   有多少物理內存可以供JVM使用?是部署多個JVM或者單個JVM?對做出的決定有重要影響。下面列出了一些要點可以幫助決定有多少物理內存可以供使用。

   1、一個機器上面只是部署一個JVM,且就一個應用使用?如果是這種情況,那麼機器的所有物理內存可以供JVM使用。

   2、一個機器上部署了多個JVM?或者一個機器上部署了多個應用?如果是這兩個中的任何一種情況,你就必須要決定每一個JVM或者應用需要分配多少內存了。

   無論是前面的哪種情況,都需要給操作系統留出一些內存。

2、HotSpot VM的堆結構

   在做內存佔用測量之前,我們必須要先理解HotSpot VM Java堆的結構,理解這個對決定應用需要的Java堆大小以及優化垃圾收器性能有很好的幫助。

java 響應大小_JVM_05

java 響應大小_java_06

   HotSpot VM有3個主要的空間:young代、old代以及permanent代,如上圖所示。

   當Java應用分配Java對象時,這些對象被分配到young代。在經歷過幾次minor GC之後,如果對象還是存活的,就會被轉移到old代空間。permanent代空間存儲了VM和Java類的元數據,比如內置的字符串和類的靜態變量。

   -Xmx和-Xms這個兩個命令行選項分別指定yound代加上old代空間的總和的初始最大值和最小值,也就是Java堆的大小。當-Xms的值小於-Xmx的值的時候,Java堆的大小可以在最大值和最小值之前浮動。

   當Java應用強調吞吐量和延遲的時候,傾向於把-Xms和-Xmx設置成相同的值,由於調整young代或者old代的大小都需要進行Full GC,Full GC降低吞吐量以及加強延遲。

   young代的空間可以通過下面的任意一個命令行選項來設置:

[html] view plain copy

  1. -XX:NewSize=<n>[g|m|k]  

      young代的初始值和最小值。<n>是大小,[g|m|k]表示單位是G字節,M字節或者千字節。young代的大小不會小於這個值。當設定-XX:NewSize=<n>[g|m|k]的時候,-XX:MaxNewSize=<n>[g|m|k]需要被指定。

[html] view plain copy

  1. -XX:MaxNewSize=<n>[g|m|k]  

   young區空間的最大值。同上面反過來,當指定-XX:MaxNewSize=<n>[g|m|k]的需要指定-XX:NewSize=<n>[g|m|k]。

[html] view plain copy

  1. -Xmn<n>[g|m|k]  

   直接指定young代的初始值、最小值以及最大值。也就是説,young區的大小被固定成這個值了。這個值用來鎖定young代的大小很方便。

   有一點需要注意的是,如果-Xms和-Xmx沒有被設定成相同的值,而且-Xmn被使用了,當調整Java堆的大小的時候,不會調整young代的空間大小,young代的空間大小會保持恆定。因此,-Xmn應該在-Xms和-Xmx設定成相同的時候才指定。

   old代的空間大小可以基於young代的大小進行計算,old代的初始值的大小是-Xms的值減去-XX:NewSize,最大值是-Xmx減去-XX:MaxNewSize,如果-Xmx和-Xms設置成了相同的值,而且使用-Xmn選項或者-XX:NewSize和-XX:MaxNewSize設置成了相同的值,那麼old代的大小就是-Xmx減去-Xmn。

   permanent代的大小通過下面命令行參數指定

[html] view plain copy

  1. -XX:PermSize=<n>[g|m|k]  

   表示permanent代的初始值和最小值,n表示數值,g|m|k表示單位、permanent的空間一定不會比這個空間小。

[html] view plain copy

  1. -XX:MaxPermSize=<n>[g|m|k]  

   permanent代的最大值,permanent代的大小不會超過這個值。

   Java應用應該指定這兩個值成為同一個值,由於這個值的調整會導致Full GC。

   如果上面提到的Java堆大小、young代、permanent代的大小都沒有指定,那麼JVM會根據應用的情況自行計算。

   在young代、old代以及permanent代中任何一個空間裏面無法分配對象的時候就會觸發垃圾回收,理解這點,對後面的優化非常重要。當young代沒有足夠空間分配Java對象的時候,觸發minor GC。minor GC相對於Full GC來説會更短暫。

   一個對象在經歷過一定次數的Minor GC之後,如果還存活,那麼會被轉移到old代(對象有一個“任期閥值”的概念,優化延遲的時候再介紹)。當old代沒有足夠空間放置對象的時候,HotSpot VM觸發Full GC。實際上在進行Minor GC的時候發現沒有old代足夠的空間來進行對象的轉移,就會觸發Full GC,相對於在Minor GC的過程中發現對象的移動失敗瞭然後觸發Full GC,這樣的策略會有更小的花費。當permanent代的空間不夠用的時候的,也會觸發Full GC。

   如果Full GC是由於old代滿了而觸發的,old代和permanent代的空間都會被垃圾回收,即使permanent代的空間還沒有滿。同理,如果Full GC是由於permanent代滿了而觸發的,old代和permanent代的空間都會被垃圾回收,即使old代的空間還沒有滿。另外,young代同樣會被垃圾回收,除非-XX:+ScavengeBeforeFullGC選項被指定了,-XX:+ScavengeBeforeFullGC關閉FullGC的時候young代的垃圾回收。

 

3、堆大小優化的起點

   為了進行Java堆大小的優化,一個合適的起點很重要。這節描述的方案是需要先使用比應用需要量更大的Java堆作為開始。這一步的目的是收集一些初始化信息以及為了進一步優化Java堆大小需要的數據。

   就像在“選擇JVM runtime”小節裏面提到過的,由吞吐量垃圾回收器(throughput garbage collector)開始。記住,使用吞吐量垃圾回收器通過設置-XX:+UserParallelOldGC命令行選項,如果你使用的HotSpot VM不支持的這個選項,那麼就使用-XX:+UserParallelGC。

   如果你能夠準確的預估到應用需要消耗的Java堆空間,可以通過設定-Xmx和-Xms來作為這個步驟的起點。如果你不知道該設定什麼值,就讓JVM來選擇吧,反正後面,都會根據實際情況進行優化調整。

   關於如何監控GC日誌前面的“GC優化基礎”已經描述過了。GC日誌會展示在使用中的java堆的大小。初始化和最大的堆大小可以通過-XX:+PrintCommandLineFlags來查看。-XX:+PrintCommandLineFlags打印出在HotSpot VM初始化的時候選擇的初始值和最大值比如-XX:InitialHeapSize=<n> -XX:MaxHeapSize=<m>,這裏n表示初始化的java堆大小值,m表示java堆的最大值。

   不管你是指定java堆的大小還是使用默認的大小,必須讓應用進入穩定運行的狀態,你必須要有能力和手段讓應用處於和線上穩定運行的狀態相同的狀態。

   如果在企圖讓應用進入穩定狀態的時候,你在垃圾回收日誌裏面觀察到OutOfMemoryError,注意是old代溢出還是permanent代溢出。下面一個old代溢出的例子:

[html] view plain copy

  1. 2012-07-15T18:51:03.895-0600: [Full GC[PSYoungGen: 279700K->267300K(358400K)]  
  2. [ParOldGen: 685165K->685165K(685170K)]  
  3. 964865K->964865K(1043570K)  
  4. [PSPermGen: 32390K->32390K(65536K)],0.2499342 secs]  
  5. [Times: user=0.08 sys=0.00, real=0.05 secs]  
  6. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space  

標紅了,由於使用的是吞吐量垃圾回收器,old代的統計信息標示為ParOldGen。這行表示了old代的在Full GC的時候佔用的空間。從這個結果來看,可以得出的結論是old代的空間太小了,由於Full GC前後old代的被佔用的空間和分配的空間基本相等了,因此,JVM報了OutOfMemoryError。相比較,通過PSPermGen這行可以看出permanent代的空間佔用是32390K,和他的容量(65536K)比還是有一定的距離。

   下面的例子展示了由於permanent太少了而導致的OutOfMemoryError發生的例子:

[html] view plain copy

  1. 2012-07-15T18:26:37.755-0600: [Full GC  
  2.   [PSYoungGen: 0K->0K(141632K)]  
  3. ParOldGen: 132538K->132538K(350208K)]  
  4.   32538K->32538K(491840K)  
  5. PSPermGen: 65536K->65536K(65536K)],  
  6.   0.2430136 secs]  
  7.   [Times: user=0.37 sys=0.00, real=0.24 secs]  
  8.   java.lang.OutOfMemoryError: PermGen space  

上面重要的部分加粗標紅了,通過PSPermGen這行可以看出在Full GC前後,他的空間佔用量都和他的容量相同,可以得出的結論是permanent代的空間條小了,這樣就導致了OutOfMemoryError。在這個例子裏面,old的佔用空間(132538K)遠比他的容量(350208K)小。

   如果在垃圾回收日誌中觀察到OutOfMemoryError,嘗試把Java堆的大小擴大到物理內存的 80% ~ 90%。尤其需要注意的是堆空間導致的OutOfMemoryError以及一定要增加空間。比如説,增加-Xms和-Xmx的值來解決old代的OutOfMemoryError,增加-XX:PermSize和-XX:MaxPermSize來解決permanent代引起的OutOfMemoryError。記住一點Java堆能夠使用的容量受限於硬件以及是否使用64位的JVM。在擴大了Java堆的大小之後,再檢查垃圾回收日誌,直到沒有OutOfMemoryError為止。

   如果應用運行在穩定狀態下沒有OutOfMemoryError就可以進入下一步了,計算活動對象的大小。

4、計算活動對象的大小

   就像前面提到的,活動對象的大小是應用處於穩定運行狀態時,長時間存活數據佔用的Java堆的空間大小。換句話説,就是應用穩定運行是,在Full GC之後,old代和permanent代的空間大小。

   活動對象的大小可以通過垃圾回收日誌查看,它提供了一些優化信息,如下:

   1、應用處於穩定運行狀態下,old代的Java堆空間佔用數量。

   2、應用處於穩定運行狀態下,permanent代的Java堆空間佔用數量。

   為了保證能夠準確的評估應用的活動對象大小,最好的做法是多看幾次Full GC之後Java堆空間的大小,保證FullGC是發生在應用處於穩定運行的狀態。

   如果應用沒有發生FullGC或者發生FullGC的次數很少,在性能測試環境,可以通過Java監控工具來觸發FullGC,比如使用VisualVM和JConsole,這些工具在最新的JDK的bin目錄下可以找到,VisualVM集成了JConsole,VisualVM或者JConsole上面有一個觸發GC的按鈕。

   另外,jmap命令可以選擇來強制HotSpot VM進行FullGC。jmap 需要-histo:live命令選項以及JVM進程id。JVM的進程id可以通過jps命令獲取。比如JVM的進程id是348,jmap命令用來觸發FullGC可以想如下這樣寫:

[html] view plain copy

  1. $ jmap -histo:live 348  

   jmap不僅僅觸發FullGC,而且產生堆的關於對象分配的概要信息。不過就目前這步的目的而言,可以忽略產生的堆概要信息。

5、初始化堆大小配置

   本節描述了怎樣利用活動對象的大小來決定初始化的Java堆的大小。下面的圖,給出了應用存活的對象的大小。比較明智的做法是多收集幾次Full GC信息,有更多的信息,能夠做出更加好的決定。

java 響應大小_hotspot_07

   通過活動對象大小的信息,可以做出關於Java堆的大小有根據的決定,以及可以估計出最壞情況下會導致的延遲。

   比較常規是,Java堆大小的初始化值和最大值(通過-Xms和-Xmx選項來指定)應該是old代活動對象的大小的3到4倍。

   在上圖中顯示的FullGC信息中,在FullGC之後old代的大小是295111K,差不多是295M,即活動的對象的大小是295M。因此,推薦的Java堆的初始化和最大值應該是885M到1180M,即可以設置為-Xms885m -Xmx1180m。在這個例子中,Java堆的大小是1048570K差不多1048M,在推薦值範圍內。

   另外一個常規是,permanent的初始值和最大值(-XX:PermSize和-XX:MaxPermSize)應該permanent代活動對象大小的1.2到1.5倍。在上圖中看到在FullGC之後permanent代佔用空間是32390K,差不多32M。因此,permanent代的推薦大小是38M到48M,即可以設置為-XX:PermSize=48m -XX:MaxPermSize=48m(1.5倍)。這個例子裏面,permanent代的空間大小是65536K即64M,大出了17M,不過在1G內存的系統的中,這個數值完全可以忍受。

   另外一個常規是,young代空間應該是old代活動對象大小的1到1.5倍。那麼在這裏例子中,young代的大小可以設置為295M到442M。本例裏面,young代的空間大小的358400K,差不多358M,在推薦值中間。

   如果推薦的Java堆的初始值和最大值是活動對象大小3到4倍,而young代的推薦只是1到1.5倍,那麼old代空間大小應該是2到3倍。

   通過以上規則,我們可以使用的Java命令可以是這樣的:

[html] view plain copy

  1. java -Xms1180m -Xmx1180m -Xmn295m -XX:PermSize=48m -XX:MaxPermSize=48m

6、另外一些考慮

   本節將提及到在進行應用內存佔用評估的時候,另外一些需要記住的點。首先,必須要知道,前面只是評估的Java堆的大小,而不是Java應用佔用的所有的內存,如果要查看Java應用佔用的所有內存在linux下可以通過top命令查看或者在window下面通過任務管理器來查看,儘管Java堆的大小可能對Java應用佔用內存做出了最大的貢獻。 比如説,為了存儲線程堆棧,應用需要額外的內存,越多的線程,越多內存被線程棧消耗,越深的方法間調用,線程棧越多。另外,本地庫需要分配額外的內存,I/O緩存也需要額外的內存。應用的內存消耗需要評估到應用任何一個會消耗內存的地方。

   記住,這一步操作不一定能夠滿足應用內存消耗的需求,如果不能滿足,就回過頭來看需求是否合理或者修改應用程序。比較可行的一種辦法是修改應用程序減小對象的分配,從而減少內存的消耗。

   Java堆的大小計算僅僅只是開始,根據需求,在後面的優化步驟中可能會修改。

 

五:優化延遲或者響應時間(1)

   本節的目標是做一些優化以滿足對應用對延遲的需求。這次需要幾個步驟,包括完善Java堆大小的配置,評估垃圾回收佔用的時間和頻率,也許還要嘗試切換到不同的垃圾回收器,以及由於使用了不同的垃圾回收器,需要重新優化Java堆空間大小。

    這一步有如下可能的結果:

    1、應用的延遲需求被滿足了。如果這一步的優化操作滿足了應用的延遲需求,你可以繼續下一步優化(優化吞吐量)。

    2、應用的延遲需求未被滿足。如果這一步的優化操作未能滿足延遲需求,你可能需要重新看看延遲需求是否合理或者修改應用程序。一些可能的問題可以幫助改善應用的延遲問題:

    a、優化Java堆以及修改應用以減少對象的分配和對象的長時間存活。

    b、修改JVM的部署結構,讓每一個JVM做更少的工作。

    上面的兩個步驟都可以減少JVM的對象分配,因此減少垃圾回收的頻率。

    這一步從查看垃圾回收對應用的延遲的影響開始,基於前面一節“決定內存消耗”計算出來的Java堆大小。

    下面列出了評估垃圾回收對延遲的影響需要進行的幾個事情:

    1、測量Minor GC的時間。

    2、測量Minor GC的頻率。

    3、測量Full GC的時間。

    4、測量Full GC的頻率。

    測量垃圾回收的時間頻率對於改善Java堆大小配置來説是非常重要的。Minor GC的時間和頻率的測量結果可以用來改善young代的空間大小。測量最壞情況下FullGC的時間和頻率可以用來決定old代的大小,以及是否需要切換成吞吐量垃圾回收器(通過使用-XX:+UseParalleOldGC或者-XX:+UseParallelGC)或者併發垃圾回收器(CMS,通過使用-XX:+UseConcMarkSweepGC)。在使用吞吐量垃圾回收器的時候,如果垃圾回收的延遲和頻率太高以導致應用的延遲需求無法滿足的時候才切換到CMS,如果選擇了切換,需要對CMS垃圾回收器進行優化,後面會詳細介紹這個問題。

    接下來詳細介紹前面提到的各種情況。

   需求

    下面列舉了幾個這一步優化操作需求,它們來源於應用的系統需求:

    1、可以接收的平均暫停時間。平均暫停時間需求用於和Minor GC消耗的時間比較。

    2、可以接收的Minor GC的頻率。其實頻道對於應用負責人來説,沒有平均延遲時間重要。

    3、應用負責人能夠接受的最大延遲時間。這個時間受到Full GC的影響。

    4、應用負責人能夠接收的最大延遲的頻率,即Full GC的頻率。其實,大多數時間應用管理員還是更加關心應用的的最大延遲時間超過了最大延遲的頻率。

    一旦確定了需求,這些垃圾回收器的時間消耗和頻率都可以通過垃圾回收日誌收集到。先把垃圾回收器設置為吞吐量垃圾回收器(設置-XX:+UseParallelOldeGC或者-XX:+UseParallelGC)。通過反覆測試,可以讓young代和old代滿足上面的要求。下面2節介紹如何優化young代和old代空間大小來觀察Minor GC和最壞情況的Full GC的消耗時間和頻率。

       改善young代的大小

    確定young代的大小是通過評估垃圾回收的統計信息以及觀察Minor GC的消耗時間和頻率,下面舉例説明如何通過垃圾回收的統計信息來確定young代的大小。

    儘管Minor GC消耗的時間和young代裏面的存活的對象數量有直接關係,但是一般情況下,更小young代空間,更短的Minor GC時間。如果不考慮MinorGC的時間消耗,減少young代的大小會導致Minor GC變得更加頻繁,由於更小的空間,用玩空間會用更少的時間。同理,提高young代的大小會降低MinorGC的頻率。

    當測試垃圾回收數據的時候,發現MinorGC的時間太長了,正確的做法就是減少young代的空間大小。如果MinorGC太頻繁了就增加young代的空間大小。

java 響應大小_垃圾回收器_08

       上圖是一個展示了MinorGC的例子,這個例子是運行在如下的HotSpot VM命令參數下的。

[html] view plain copy

  1. -Xms6144m -Xmx6144m -Xmn2048m -XX:PermSize=96m -XX:MaxPermSize=96m

    上圖顯示了Minor GC平均的消耗時間是0.05秒,平均的頻率是2.147秒1次。當計算Minor GC的消耗時間和頻率的時候,越多的數據參與計算,準確性會越高。並且應用要處於穩定運行狀態下來收集Minor GC信息也是非常重要的。

    下一步是比較MinorGC的平均時間和系統對延遲的要求,如果MinorGC的平均時間大於了系統的要求,減少young代的空間大小,然後繼續測試,再收集數據以及重新評估。

    如果MinorGC的頻率大於了系統的要求,就增加young代的空間大小,然後繼續測試,再收集以及重新評估。

    也許需要數次重複才能夠讓系統達到延遲要求。當你改變young代的空間大小的時候,儘量保持old代的空間大小不要改變。

    從上圖的垃圾回收信息來看,如果應用的延遲要求是40毫秒的話,觀察到的MinorGC的延遲是58毫秒,比系統的要求高出了不少。上面例子使用的命令選項是

[html] view plain copy

  1. -Xms6144m -Xmx6144m -Xmn2048m -XX:PermSize=96m -XX:MaxPermSize=96m

    意味着old代的空間大小是4096M,減小young代的空間大小的10%而且要保持old代的空間大小不變,可以使用如下選項。

[html] view plain copy

  1. -Xms5940m -Xmx5940m -Xmn1844m -XX:PermSize=96 -XX:MaxPermSize=96

    注意的是young代的空間大小從2048M減少到1844M,整個Java堆的大小從6144M減少到5940M,兩者都是減少了204m。

    無論是young的空間調大還是調小,都需要重新收集垃圾回收信息和重新計算MinorGC的平均時間和頻率,以達到應用的延遲要求,可能需要幾個輪迴來達到這個要求。

    為了説明了增加young代的大小以降低MinorGC的頻率,我們下面舉一個例子。如果系統要求的頻率是5秒一次,這個上面的例子中是2.147秒一次,也就是説它用了2.147秒,填充滿了2048M空間,如果需要5秒一次的頻率,那麼就需要5/2.147倍的空間,即2048*5/2.147等於4700M。因此young代的空間需要調整到4700M。下面是一個示例來説明配置這個:

[html] view plain copy

  1. -Xms8796m -Xmx8796m -Xmn4700m -XX:PermSize=96m -XX:MaxPermSize=96m

    注意是-Xms和-Xmx也同步調整了。

    另外一些調整young代的空間需要注意的事項:

    1、old代的空間一定不能小於活動對象的大小的1.5倍。

    2、young代的空間至少要有Java堆大小的10%,太小的Java空間會導致過於頻繁的MinorGC。

    3、當提高Java堆大小的時候,不要超過JVM可以使用的物理內存大小。如果使用過多的物理內存,會導致使用交換區,這個會嚴重影響性能。

    如果在僅僅是MinorGC導致了延遲的情況下,你無法通過調整young代的空間來滿足系統的需求,那麼你需要重 新修改應用程序、修改JVM部署模型把應用部署到多個JVM上面(通常得要多機器了)或者重新評估系統的需求。

    如果通過調整MinorGC能夠滿足應用的延遲需求,接下來就可以調整old代了,以達到最壞情況下的延遲和延遲頻率的需求。下一節詳細説明這個問題。

       完善old代的大小

    這一節的目標是評估由於Full GC引起的最差暫停時間和頻率。

    同前面一個節“完善young代大小”一樣,垃圾回收的統計信息是必須的,在穩定狀態下,Full GC的時間表明瞭應用最差的延遲,如果發生了多個Full GC,計算多個Full GC的平均消耗時間,更多數據能夠更好的評估。

    計算兩次不同的FullGC之間的時間差,可以提供出FullGC的頻率,下圖用一個列子來説明兩個FullGC:

java 響應大小_java 響應大小_09

       如果沒有FullGC,可以人為的去幹預,前面説過,可以使用VisualVM來觸發FullGC。另外,評估FullGC的頻率需要知道對象的轉移率,這個轉移率説明對象從young代轉移到old代。接下來的介紹如何評估轉移率。

    接下有個幾個MinorGC的例子,他們被用來評估FullGC的頻率。

[html] view plain copy

  1. 2010-12-05T14:40:29.564-0800: [GC  
  2. [PSYoungGen: 2045989K->249795K(2097152K)]  
  3. 3634533K->1838430K(6291456K), 0.0543798 secs]  
  4. [Times: user=0.38 sys=0.01, real=0.05 secs]  

[html] view plain copy

  1. 2010-12-05T14:40:31.949-0800: [GC  
  2. [PSYoungGen: 2047896K->247788K(2097152K)]  
  3. 3655319K->1859216K(6291456K), 0.0539614 secs]  
  4. [Times: user=0.35 sys=0.01, real=0.05 secs]  

[html] view plain copy

  1. 2010-12-05T14:40:34.346-0800 [GC  
  2. [PSYoungGen: 2045889K->248993K(2097152K)]  
  3. 3677202K->1881099K(6291456K), 0.0532377 secs]  
  4. [Times: user=0.39 sys=0.01, real=0.05 secs]  

[html] view plain copy

  1. 2010-12-05T14:40:36.815-0800 [GC  
  2. [PSYoungGen: 2047094K->247765K(2097152K)]  
  3. 3696985K->1900882K(6291456K), 0.0543332 secs]  
  4. [Times: user=0.37 sys=0.01, real=0.05 secs]  

 

    從上面的例子可以看出:

    1、Java堆的大小是6291456K或6144M

    2、young代的大小是2097152K或2048M

    3、old代的大小是6144M-2048M = 4096M

    在這個例子中,活動對象的大小差不多是1370M。那麼old代還有2726M剩餘空間(4096M-1370M=2726M)。

    填充完成2736M空間需要多長時間是由young代向old代的轉移率決定的。這個轉移率的計算通過查看每次MinorGC後old代的佔用空間的增長情況以及MinorGC發生的時間。old代的空間佔用是MinorGC之後Java堆中對象大小減去young代的大小,通過這個公式計算,可以看出在這個例子中每次MinorGC之後,old代的空間佔用情況是:

    1588635K,第一個MinorGC

    1611428K,第二次MinorGC

    1632106K,第三次MinorGC

    1653117K,第四次MinorGC

    每次的增量分別是

    22793K,第一次和第二次的增量

    20678K,第二次和第三次的增量

    21011K,第三次和第四次的增量

    平均每次MinorGC轉移大概201494K或者叫21M。

    如果剩餘的空間都是按照設個轉移率來轉移到old代的話,且知道MinorGC的頻率是每2.147秒一次。因此,這個轉移率是201494K/2.147s差不多10M/s,那麼一共的空間是2736M空間需要273.6s差不多4.5分鐘一次。

    因此,通過前面的案例分析,應用的最差延遲的頻率是4.5分鐘。這個評估可以通過讓應用處於穩定運行狀態超過4.5分鐘來驗證。

    如果評估和觀察的FullGC的頻率高於了應用對最壞延遲頻率的要求,那麼可以提高old代的空間大小。如果改變old代的大小,保持young代的空間恆定,在優化young代的時候也説這個問題,兩者應該獨立優化,以保證有高效。

    如果這步已經達到了你最壞延遲的要求,那麼這一步調優延遲就算已經完成了,就可以進入下一步去調優“吞吐量”了。

       如果你未能達到了應用對最壞延遲時間和頻率的性能要求,由於FullGC的執行時間太長了,然後你可以把垃圾回收器切換CMS(concurrent garbage collection)。CMS有能力讓垃圾回收儘量是多線程的,即讓程序保持在運行狀態。要使用CMS可以通過下面這條命令選項:-XX:+UseConcMarkSweepGC。

        後面詳細説明如何調優CMS。

 

五:優化延遲或者響應時間(2)

優化CMS(concurrent garbage collection)

   使用CMS,old代的垃圾回收執行線程會和應用程序的線程最大程度的併發執行。這個提供了一個機會來減少最壞延遲的頻率和最壞延遲的時間消耗。CMS沒有執行壓縮,所以可以避免old代空間的stop-the-world壓縮(會讓整個應用暫停運行)。

   優化CMS的目標就是避開stop-the-world壓縮垃圾回收,然而,這個説比做起來容易。在一些的部署情況下,這個是不可避免的,尤其是當內存分配受限的時候。

   在一些特殊的情況下,CMS比其他類型的垃圾回收需要更多優化,更需要優化young代的空間,以及潛在的優化該什麼時候初始化old代的垃圾回收循環。

   當從吞吐量垃圾回收器(Throughput)遷移到CMS的時候,有可能會獲得更慢的MinorGC,由於對象從young代轉移到old會更慢 ,由於CMS在old代裏面分配的內存是一個不連續的列表,相反,吞吐量垃圾回收器只是在本地線程的分配緩存裏面指定一個指針。另外,由於old代的垃圾回收線程和應用的線程是儘可能的併發運行的,所以吞吐量會更小一些。然而,最壞的延遲的頻率會少很多,由於在old代的不可獲取的對象能夠在應用運行的過程被垃圾回收,這樣可以避免old代的空間溢出。

   使用CMS,如果old代能夠使用的空間有限,單線程的stop-the-world壓縮垃圾回收會執行。這種情況下,FullGC的時間會比吞吐量垃圾回收器的FullGC時間還要長,導致的結果是,CMS的絕對最差延遲會比吞吐量垃圾回收器的最差延遲嚴重很多。old代的空間溢出以及運行了stop-the-world垃圾回收必須被應用負責人重視,由於在響應上會有更長的中斷。因此,不要讓old代運行得溢出就非常重要了。對於從吞吐量垃圾回收器遷移到CMS的一個比較重要的建議就是提升old代20%到30%的容量。

   在優化CMS的時候有幾個注意點,首先,對象從young代轉移到old代的轉移率。其次,CMS重新分配內存的概率。再次,CMS回收對象時候產生的old代的分隔,這個會在可獲得的對象中間產生一些空隙,從而導致了分隔空間。

   碎片可以被下面的幾種方法尋址。第一辦法是壓縮old代,壓縮old代空間是通過stop-the-world垃圾回收壓縮完成的,就像前面所説的那樣,stop-the-world垃圾回收會執行很長時間,會嚴重影響應用的響應時間,應該避開。第二種辦法是,對碎片編址,提高old代的空間,這個辦法不能完全解決碎片的問題的,但是可以延遲old代壓縮的時間。通常來講,old代越多內存,由於碎片導致需要執行的壓縮的時間久越長。努力把old的空間增大的目標是在應用的生命週期中,避免堆碎片導致stop-the-world壓縮垃圾回收,換句話説,應用GC最大內存原則。另外一種處理碎片的辦法是減少對象從young代移動到old的概率,就是減少MinorGC,應用MinorGC回收原則。

   任期閥值(tenuring threshold)控制了對象該什麼時候從young代移動到old代。任期閥值會在後面詳細的介紹,它是HotSpot VM基於young代的佔用空間來計算的,尤其是survivor(倖存者)空間的佔用量。下面詳細介紹一下survivor空間以及討論任期閥值。

survivor空間

   survivor空間是young代的一部分,如下圖所示。young代被分成了一個eden區域和兩個survivor空間。

java 響應大小_java 響應大小_10

   兩個survivor空間的中一個被標記為“from”,另外一個標記為“to”。新的Java對象被分配到Eden空間。比如説,下面的一條語句:

   [java] view plain copy

  1. Map<String,String> map = new HashMap<String,String>();

   一個新的HashMap對象會被放到eden空間,當eden空間滿了的時候,MinorGC就會執行,任何存活的對象,都從eden空間複製到“to” survivor空間,任何在“from” survivor空間裏面的存活對象也會被複制到“to” survivor。MinorGC結束的時候,eden空間和“from” survivor空間都是空的,“to” survivor空間裏面存儲存活的對象,然後,在下次MinorGC的時候,兩個survivor空間交換他們的標籤,現在是空的“from” survivor標記成為“to”,“to” survivor標記為“from”。因此,在MinorGC結束的時候,eden空間是空的,兩個survivor空間中的一個是空的。

   在MinorGC過程,如果“to” survivor空間不夠大,不能夠存儲所有的從eden空間和from suvivor空間複製過來活動對象,溢出的對象會被複制到old代。溢出遷移到old代,會導致old代的空間快速增長,會導致stop-the-world壓縮垃圾回收,所以,這裏要使用MinorGC回收原則。

   避免survivor空間溢出可以通過指定survivor空間的大小來實現,以使得survivor有足夠的空間來讓對象存活足夠的歲數。高效的歲數控制會導致只有長時間存活的對象轉移到old代空間。

   歲數控制是指一個對象保持在young代裏面直到無法獲取,所以讓old代只是存儲長時間保存的對象。

   survivor的空間可以大小設置可以用HotSpot命令行參數:-XX:SurvivorRatio=<ratio>

   <ratio>必須是以一個大於0的值,-XX:SurvivorRatio=<ratio>表示了每一個survivor的空間和eden空間的比值。下面這個公式可以用來計算survivor空間的大小

 [html] view plain copy

  1. survivor spave size = -Xmn<value>/(-XX:SurvivorRatio=<ratio>+2)  

   這裏有一個+2的理由是有兩個survivor空間,是一個調節參數。ratio設置的越大,survivor的空間越小。為了説明這個問題,假設young代的大小是-Xmn512m而且-XX:SurvivorRatio=6.那麼,young代有兩個survivor空間且空間大小是64M,那麼eden空間的大小是384M。

   同樣假如young代的大小是512M,但是修改-XX:SurvivorRatio=2,這樣的配置會使得每一個survivor空間的大小是128m而eden空間的大小是256M。

   對於一個給定大小young代空間大小,減小ratio參數增加survivor空間的大小而且減少eden空間的大小。反之,增加ratio會導致survivor空間減少而且eden空間增大。減少eden空間會導致MinorGC更加頻繁,相反,增加eden空間的大小會導致更小的MinorGC,越多的MinorGC,對象的歲數增長得越快。

   為了更好的優化survivor空間的大小和完善young代空間的大小,需要監控任期閥值,任期閥值決定了對象會再young代保存多久。怎麼樣來監控和優化任期閥值將在下一節中介紹。

   任期閥值

   “任期”是轉移的代名詞,換句話説,任期閥值意味着對象移動到old代空間裏面。HotSpot VM每次MinorGC的時候都會計算任期,以決定對象是否需要移動到old代去。任期閥值就是對象的歲數。對象的歲數是指他存活過的MinorGC次數。當一個對象被分配的時候,它的歲數是0。在下次MinorGC的時候之後,如果對象還是存活在young代裏面,它的歲數就是1。如果再經歷過一次MinorGC,它的歲數變成2,依此類推。在young代裏面的歲數超過HotSpot VM指定閥值的對象會被移動到old代裏面。換句話説,任期閥值決定對象在young代裏面保存多久。

   任期閥值的計算依賴於young代裏面能夠存放的對象數以及MinorGC之後,“to” servivor的空間佔用。HotSpot VM有一個選項-XX:MaxTenuringThreshold=<n>,可以用來指定當時對象的歲數超過<n>的時候,HotSpot VM會把對象移動到old代去。內部計算的任期閥值一定不會超過指定的最大任期閥值。最大任期閥值在可以被設定為0-15,不過在Java 5 update 5之前可以設置為1-31。

   不推薦把最大任期閥值設定成0或者超過15,這樣會導致GC的低效率。

   如果HotSpot VM它無法保持目標survivor 空間的佔用量,它會使用一個小於最大值的任期閥值來維持目標survivor空間的佔用量,任何比這個任期閥值的大的對象都會被移動到old代。話句話説,當存活對象的量大於目標survivor空間能夠接受的量的時候,溢出發生了,溢出會導致對象快速的移動到old代,導致不期望的FullGC。甚至會導致更頻繁的stop-the-world壓縮垃圾回收。哪些對象會被移動到old代是根據評估對象的歲數和任期閥值來確定的。因此,很有必要監控任期閥值以避免survivor空間溢出,接下來詳細討論。

監控任期閥值

   為了不被內部計算的任期閥值迷惑,我們可以使用命令選項-XX:MaxTenuringThreshod=<n>來指定最大的任期閥值。為了決定出最大的任期閥值,需要監控任期閥值的分佈和對象歲數的分佈,通過使用下面的選項實現

[html] view plain copy

  1. -XX:+PrintTenuringDistribution  

   -XX:+PrintTenuringDistribution的輸出顯示在survivor空間裏面有效的對象的歲數情況。閲讀-XX:+PrintTenuringDistribution輸出的方式是觀察在每一個歲數上面,對象的存活的數量,以及其增減情況,以及HotSpot VM計算的任期閥值是不是等於或者近似於設定的最大任期閥值。

   -XX:+PrintTenuringDistribution在MinorGC的時候產生任期分佈信息。它可以同其他選項一同使用,比如-XX:+PrintGCDateStamps,-XX:+PrintGCTimeStamps以及-XX:+PringGCDetails。當調整survivor空間大小以獲得有效的對象歲數分佈,你應該使用-XX:+PrintTenuringDistribution。在生產環境中,它同樣非常有用,可以用來判斷stop-the-world的垃圾回收是否發生。

   下面是一個輸出的例子:

Desired survivor size 8388608 bytes, new threshold 1 (max 15) 

   - age 1: 16690480 bytes, 16690480 total

Desired survivor size 8388608 bytes表示一個survivor的空間大小。目標survivor的佔有率是指目標survivor和兩個survivor空間總和的比值。怎麼樣指定期望的survivor空間大小在後面會詳細介紹。在第一行下面,會列出一個對象的歲數列表。每行會列出每一個歲數的字節數,在這個例子中,歲數是1的對象有16690480字節,而且每行後面有一個總的字節數,如果有多行輸出的話,總字節數是前面的每行的累加數。後面舉例説明。

   在前面的例子中,由於期望的survivor大小(8388608)比實際總共survivor字節數(16690480)小,也就是説,survivor空間溢出了,這次MinorGC會有一些對象移動到old代。這個就意味着survivor的空間太小了。另外,設定的最大任期閥值是15,但是實際上JVM使用的是1,也表明了survivor的空間太小了。

   如果發現survivor區域太小,就增大survivor的空間,下面詳細介紹如何操作。

   設定survivor空間

   當修改survivor空間的大小的時候,有一點需要記住。當修改survivor空間大小的時候,如果young代的大小不改變,那麼eden空間會減小,進一步會導致更頻繁的MinorGC。因此,增加survivor空間的時候,如果young代的空間大小違背了MinorGC頻率的需求,eden空間的大小同需要需要增加。換句話説,當survivor空間增加的時候,young代的大小需要增加。

   如果有空間來增加MinorGC的頻率,有兩種選擇,一是拿一些eden空間來增加survivor的空間,二是讓young的空間更大一些。常規來講,更好的選擇是如果有可以使用的內存,增加young代的空間會比減少eden的空間更好一些。讓eden空間大小保持恆定,MinorGC的頻率不會改變,即使調整survivor空間的大小。

   使用-XX:+PrintTenuringDistribution選項,對象的總字節數和目標survivor空間佔用可以用來計算survivor空間的大小。重複前面的例子:

Desired survivor size 8388608 bytes, new threshold 1 (max 15) 

   - age 1: 16690480 bytes, 16690480 total

   存活對象的總字節數是1669048,這個併發垃圾回收器(CMS)的目標survivor默認使用50%的survivor空間。通過這個信息,我們可以知道survivor空間至少應該是33380960字節,大概是32M。這個計算讓我們知道對survivor空間的預估值需要計算對象的歲數更高效以及防止溢出。為了更好的預估survivor的可用空間,你應該監控應用穩定運行情況下的任期分佈,並且使用所有的額外總存活對象的字節數來作為survivor空間的大小。

   在這個例子,為了讓應用計算歲數更加有效,survivor空間需要至少提升32M。前面使用的選項是:

[html] view plain copy

  1. -Xmx1536m -Xms1536m -Xmn512m -XX:SurvivorRatio=30

   那麼為了保持MinorGC的頻率不發生變化,然後增加survivor空間的大小到32M,那麼修改後的選項如下:

[html] view plain copy

  1. -Xmx1568m -Xms1568m -Xmn544m -XX:SurvivvorRatio=15

   當時young代空間增加了,eden空間的大小保持大概相同,且survivor的空間大小增減了。需要注意的時候,-Xmx、-Xms、-Xmn都增加了32m。另外,-XX:SurvivvorRatio=15讓每一個survivor空間的大小都是32m (544/(15+2) = 32)。

   如果存在不能增加young代空間大小的限制,那麼增加survivor空間大小需要以減少eden空間的大小為代價。下面是一個增加survivor空間大小,每一個survivor空間從16m增減加到32m,那麼會見減少eden的空間,從480m減少到448m(512-32-32=448,512-16-16=480)。

[html] view plain copy

  1. -Xms1536m -Xms1536m -Xmn1512m -XX:SurvivorRatio=14

   再次強調,減少eden空間大小會增加MinorGC的頻率。但是,對象會在young代裏面保持更長的時間,由於提升survivor的空間。

   假如運行同樣的應用,我們保持eden的空間不變,增加survivor空間的大小,如下面選項:

[html] view plain copy

  1. -Xmx1568m -Xms1568m -Xmn544m -XX:SurvivorRatio=15

   可以產生如下的任期分佈:

   Desired survivor size 16777216 bytes, new threshold 15 (max 15)

- age 1: 6115072 bytes, 6115072 total
- age 2: 286672 bytes, 6401744 total
- age 3: 115704 bytes, 6517448 total
- age 4: 95932 bytes, 6613380 total
- age 5: 89465 bytes, 6702845 total
- age 6: 88322 bytes, 6791167 total
- age 7: 88201 bytes, 6879368 total
- age 8: 88176 bytes, 6967544 total
- age 9: 88176 bytes, 7055720 total
- age 10: 88176 bytes, 7143896 total
- age 11: 88176 bytes, 7232072 total
- age 12: 88176 bytes, 7320248 total

 

   從任期分佈的情況來看,survivor空間沒有溢出,由於存活的總大小是7320248,但是預期的survivor空間大小是16777216以及任期閥值和最大任期閥值是相等的。這個表明,對象的老化速度是高效的,而且survivor空間沒有溢出。

   在這個例子中,由於歲數超過3的對象很少,你可能像把最大任期閥值設置為3來測試一下,即設置選項-XX:MaxTenuringThreshhold=3,那麼整個選項可以設置為:

[html] view plain copy

  1. -Xmx1568m -Xms1658m -Xmn544m -XX:SurvivorRatio=15 -XX:MaxTenuringThreshold=3

   這個選項設置和之前的選項設置的權衡是,後面這個選擇可以避免在MinorGC的時候不必要地把對象從“from” survivor複製到“to” survivor。在應用運行在穩定狀態的情況下,觀察多次MinorGC任期分佈情況,看是否有對象最終移動到old代或者顯示的結果還是和前面的結果類似。如果你觀察得到和前面的任期分佈情況相同,基本沒有對象的歲數達到15,也沒有survivor的空間溢出,你應該自己設置最大任期閥值以代替JVM默認的15。在這個例子中,沒有長時間存活的對象,由於在他們的歲數沒有到達15的時候就被垃圾回收了。這些對象在MinorGC中被回收了,而不是移動到old代裏面。使用併發垃圾回收(CMS)的時候,對象從young代移動到old代最終會導致old的碎片增加,有可能導致stop-the-world壓縮垃圾回收,這些都是不希望出現的。寧可選擇讓對象在“from” survivor和“to” survivor中複製,也不要太快的移動到old代。

   你可能需要重複數次監控任期分佈、修改survivor空間大小或者重新配置young代的空間大小直到你對應用由於MinorGC引起的延遲滿意為止。如果你發現MinorGC的時間太長,你可以通過減少young代的大小直到你滿意為止。儘管,減少young代的大小,會導致更快地移動對象到old代,可能導致更多的碎片,如果CMS的併發垃圾回收能夠跟上對象的轉移率,這種情況就比不能滿足應用的延遲需求更好。如果這步不能滿足應用的MinorGC的延遲和頻率需求,這個時候就有必要重新審視需求以及修改應用程序了。

   如果滿足對MinorGC延遲的需求,包括延遲時間和延遲頻率,你可以進入下一步,優化CMS垃圾回收週期的啓動,下節詳細介紹。

五:優化延遲或者響應時間(3)

CMS垃圾回收器週期   

   一旦young的空間大小(包含eden和survivor空間)已經完善得滿足應用對MinorGC產生延遲要求,注意力可以轉移到優化CMS垃圾回收器,降低最差延遲時間的時間長度以及最小化最差延遲的頻率。目標是保持可用的old代空間和併發垃圾回收,避免stop-the-world壓縮垃圾回收。

 

   stop-the-world壓縮垃圾回收是垃圾回收影響延遲的最差情況,對某些應用來説,恐怕無法完全避免開這些,但是本節提供的優化信息至少可以減少他們的頻率。

 

   成功的優化CMS垃圾回收器需要達到的效果是old代的裏面的垃圾回收的效率要和young代轉移對象到old代的效率相同,沒有能夠完成這個標準可以稱為“比賽失敗”,比賽失敗的結果就是導致stop-the-world壓縮垃圾回收。不比賽中失敗的一個關鍵是讓下面兩個事情結合起來:1、old代有足夠的空間。2、啓動CMS垃圾回收週期開始時機——快到回收對象的速度比較轉移對象來的速度更快。

 

   CMS週期的啓動是基於old代的空間大小的。如果CMS週期開始的太晚,他就會輸掉比賽,沒有能夠快速的回收對象以避免溢出old代空間。如果CMS週期開始得太早,會造成不必要的壓力以及影響應用的吞吐量。但是,通常來講過早的啓動總比過晚的啓動好。

 

   HotSpot VM自動地計算出當佔用是多少時啓動CMS垃圾回收週期。不過在一些場景下,對於避免stop-the-world垃圾回收,他做得並不好。如果觀察到stop-the-world垃圾回收,你可以優化該什麼時候啓動CMS週期。在CMS垃圾回收中,stop-the-world壓縮垃圾回收在垃圾回收日誌中輸出是“concurrent mode failure”,下面一個例子:

 

174.445: [GC 174.446: [ParNew: 66408K->66408K(66416K), 0.0000618

   secs]174.446: [CMS ( concurrent mode failure): 161928K->162118K(175104K),

   4.0975124 secs] 228336K->162118K(241520K)

 

   如果你發現有concurrent mode failure你可以通過下面這個選項來控制什麼時候啓動CMS垃圾回收:

 

   

[html] view plain copy

  1. -XX:CMSInitiatingOccupancyFraction=<percent>  

 

 

   這個值指定了CMS垃圾回收時old代的空間佔用率該是什麼值。舉例説明,如果你希望old代佔用率是65%的時候,啓動CMS垃圾回收,你可以設置-XX:CMSInitiatingOccupancyFraction=65。另外一個可以同時使用的選項是

 

   

[html] view plain copy

  1. -XX:+UseCMSInitiatingOccupancyOnly  

 

 

   -XX:+UseCMSInitiatingOccupancyOnly指定HotSpot VM總是使用-XX:CMSInitiatingOccupancyFraction的值作為old的空間使用率限制來啓動CMS垃圾回收。如果沒有使用-XX:+UseCMSInitiatingOccupancyOnly,那麼HotSpot VM只是利用這個值來啓動第一次CMS垃圾回收,後面都是使用HotSpot VM自動計算出來的值。

 

   -XX:CMSInitiatingOccupancyFraction=<percent>這個指定的值,應該比垃圾回收之後存活對象的佔用率更高,怎麼樣計算存活對象的大小前面在“決定內存佔用”的章節已經説過了。如果<percent>不比存活對象的佔用量大,CMS垃圾回收器會一直運行。通常的建議是-XX:CMSInitiatingOccupancyFraction的值應該是存活對象的佔用率的1.5倍。舉例説明一下,假如用下面的Java堆選項配置:

 

   

[html] view plain copy

  1. -Xmx1536m -Xms1536m -Xmn512m  

 

 

   那麼old代的空間大小是1024M(1536-512 = 1024m)。如果存活對象的大小是350M的話,CMS垃圾回收週期的啓動閥值應該是old代佔用空間是525M,那麼佔用率就應該是51%(525/1024=51%),這個只是初始值,後面還可能根據垃圾回收日誌進行修改。那麼修改後的命令行選項是:

 

   

[html] view plain copy

  1. -Xmx1536m -Xms1536m -Xmn512m  -XX:+UseCMSInitiatingOccupancyOnly   
  2. -XX:CMSInitiatingOccupancyFraction=51

 

 

   該多早或者多遲啓動CMS週期依賴於對象從young代轉移到old代的速率,也就是説,old代空間的增長率。如果old代填充速度比較緩慢,你可以晚一些啓動CMS週期,如果填充速度很快,那麼就需要早一點啓動CMS週期,但是不能小於存活對象的佔用率。如果需要設置得比存活對象的佔用率小,應該是增加old代的空間。

 

   想知道CMS週期是開始的太早還是太晚,可以通過評估垃圾回收信息識別出來。下面是一個CMS週期開始得太晚的例子。為了更好閲讀,稍微修改了輸出內容:

 

[ParNew 742993K->648506K(773376K), 0.1688876 secs]

[ParNew 753466K->659042K(773376K), 0.1695921 secs]

[CMS-initial-mark 661142K(773376K), 0.0861029 secs]

[Full GC 645986K->234335K(655360K), 8.9112629 secs]

[ParNew 339295K->247490K(773376K), 0.0230993 secs]

[ParNew 352450K->259959K(773376K), 0.1933945 secs]

 

   注意FullGC在CMS-inital-mark之後很快就發生了。CMS-initial-mark是報告CMS週期多個字段中的一個。下面的例子會使用到更多的字段。

    下面是一個CMS開始的太早了的情況:

 

[ParNew 390868K->296358K(773376K), 0.1882258 secs]

[CMS-initial-mark 298458K(773376K), 0.0847541 secs]

[ParNew 401318K->306863K(773376K), 0.1933159 secs]

[CMS-concurrent-mark: 0.787/0.981 secs]

[CMS-concurrent-preclean: 0.149/0.152 secs]

[CMS-concurrent-abortable-preclean: 0.105/0.183 secs]

[CMS-remark 374049K(773376K), 0.0353394 secs]

[ParNew 407285K->312829K(773376K), 0.1969370 secs]

[ParNew 405554K->311100K(773376K), 0.1922082 secs]

[ParNew 404913K->310361K(773376K), 0.1909849 secs]

[ParNew 406005K->311878K(773376K), 0.2012884 secs]

[CMS-concurrent-sweep: 2.179/2.963 secs]

[CMS-concurrent-reset: 0.010/0.010 secs]

[ParNew 387767K->292925K(773376K), 0.1843175 secs]

[CMS-initial-mark 295026K(773376K), 0.0865858 secs]

[ParNew 397885K->303822K(773376K), 0.1995878 secs]

 

   CMS-initial-mark表示CMS週期的開始, CMS-initial-sweep和CMS-concurrent-reset表示週期的結束。注意第一個CMS-initial-mark報告堆大小是298458K,然後注意,ParNew MinorGC報告在CMS-initial-mark和CMS-concurrent-reset之間只有很少的佔用量變化,堆的佔用量可以通過ParNew的->的右邊的數值來表示。在這個例子中,CMS週期回收了很少的垃圾,通過在CMS-initial-mark和CMS-concurrent-reset之間只有很少的佔用量變化可看出來。這裏正確的做法是啓動CMS週期用更大的old代空間佔用率,通過使用參數

-XX:+UseCMSInitiatingOccupancyOnly和-XX:CMSInitiatingOccupancyFraction=<percent>。基於初始(CMS-initial-mark)佔用量是298458K以及Java堆的大小是773376K,就是CMS發生的佔用率是35%到40%(298458K/773376K=38.5%),可以使用選項來強制提高佔用率的值。

 

   下面是一個CMS週期回收了大量old代空間的例子,而且沒有經歷stop-the-world壓縮垃圾回收,也就沒有併發錯誤(concurrent mode failure)。同樣的修改輸出格式:

 

[ParNew 640710K->546360K(773376K), 0.1839508 secs]

[CMS-initial-mark 548460K(773376K), 0.0883685 secs]

[ParNew 651320K->556690K(773376K), 0.2052309 secs]

[CMS-concurrent-mark: 0.832/1.038 secs]

[CMS-concurrent-preclean: 0.146/0.151 secs]

[CMS-concurrent-abortable-preclean: 0.181/0.181 secs]

[CMS-remark 623877K(773376K), 0.0328863 secs]

[ParNew 655656K->561336K(773376K), 0.2088224 secs]

[ParNew 648882K->554390K(773376K), 0.2053158 secs]

[ParNew 489586K->395012K(773376K), 0.2050494 secs]

[ParNew 463096K->368901K(773376K), 0.2137257 secs]

[CMS-concurrent-sweep: 4.873/6.745 secs]

[CMS-concurrent-reset: 0.010/0.010 secs]

[ParNew 445124K->350518K(773376K), 0.1800791 secs]

[ParNew 455478K->361141K(773376K), 0.1849950 secs]

 

   在這個例子中,在CMS週期開始的時候,CMS-initial-mark表明佔用量是548460K。在CMS週期開始和結束(CMS-concurrent-reset)之間,ParNew MinorGC報告顯著的減少了對象的佔用量。尤其,在CMS-concurrent-sweep之前,佔用量從561336K降低到了368901K。這個表明在CMS週期中,有190M空間被垃圾回收。需要注意的是,在CMS-concurrent-sweep之後的第一個ParNew MinorGC報告的佔用量是350518K。這個説明超過190M被垃圾回收(561336K-350518K=210818K=205.88M)。

 

   如果你決定優化CMS週期的啓動,多嘗試幾個不同的old代佔用率。監控垃圾回收信息以及分析這些信息可以幫助你做出正確的決定。

 

強制的垃圾回收

   如果你想要觀察通過調用System.gc()來啓動的FullGC,當使用用CMS的時候,有兩種方法來處理這種情況。

 

   1、你可以請求HotSpot VM執行System.gc()的時候使用CMS週期,使用如下命令選項:

 

 

[html] view plain copy

  1. -XX:+ExplicitGCInvokesConcurrent  
  2.   或者  
  3. -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses  

 

 

   第一個選項在Java 6及更新版本中能夠使用,第二選項在從Java 6 Update 4之後才有。如果可以,建議使用後者。

 

   2、你可以請求HotSpot VM選項忽視強制的調用System.gc(),可以使用如下選項:

 

   

[html] view plain copy

  1. -XX:+DisableExplicitGC  

 

 

   這個選項用來讓其他垃圾回收器忽略System.gc()請求。

   

   當關閉的強制垃圾回收需要小心,這樣做可能對Java性能產生很大的影響,關閉這個功能就像使用System.gc()一樣需要明確的理由。

 

   在垃圾回收日誌裏面找出明確的垃圾回收信息是非常容易的。垃圾回收的輸出裏面包含了一段文字來説明FullGC是用於調用System.gc().下面是一個例子:

 

2010-12-16T23:04:39.452-0600: [Full GC (System)

[CMS: 418061K->428608K(16384K), 0.2539726 secs]

418749K->4288608K(31168K),

[CMS Perm : 32428K->32428K(65536K)],0.2540393 secs]

[Times: user=0.12 sys=0.01, real=0.25 secs]

 

   注意Full GC後面的(System)標籤,這個説明是System.gc()引起的FullGC。如果你在垃圾回收日誌裏面觀察到了明確的FullGC,想想為什麼會出現、是否需要關閉、是否需要把應用源代碼裏面的相關代碼刪除掉,對CMS垃圾回收週期是否有意義。

併發的Permanent代垃圾回收

 

    FullGC發生可能是由於permanent空間滿了引起的,監控FullGC垃圾回收信息,然後觀察Permanent代的佔用量,判斷FullGC是否是由於permanent區域滿了引起的。下面是一個由於permanent代滿了引起的FullGC的例子:

 

2010-12-16T17:14:32.533-0600: [Full GC

[CMS: 95401K->287072K(1048576K), 0.5317934 secs]

482111K->287072K(5190464K),

[CMS Perm : 65534K->58281K(65536K)], 0.5319635 secs]

[Times: user=0.53 sys=0.00, real=0.53 secs]

 

    注意permanent代的空間佔用量,通過CMS Perm :標籤識別。permanent代空間大小是括號裏面的值,65536K。在FullGC之前permanent代的佔用量是->左邊的值,65534K,FullGC之後的值是58281K。可以看到的是,在FullGC之前,permanent代的佔用量以及基本上和permanent代的容量非常接近了,這個説明,FullGC是由Permanent代空間溢出導致的。同樣需要注意的是,old代還沒有到溢出空間的時候,而且沒有證據説明CMS週期啓動了。

 

   HotSpot VM默認情況下,CMS不會垃圾回收permanent代空間,儘管垃圾回收日誌裏面有CMS Perm標籤。為讓CMS回收permanent代的空間,可以用過下面這個命令選項來做到:

 

   

[html] view plain copy

  1. -XX:+CMSClassUnloadingEnabled  

 

 

   如果使用Java 6 update 3及之前的版本,你必須指定一個命令選項:

 

 

[html] view plain copy

  1. -XX:+CMSPermGenSweepingEnabled  

 

 

   你可以控制permanent的空間佔用率來啓動CMS permanent代垃圾回收通過下面這個命令選項:

 

   

[html] view plain copy

  1. -XX:CMSInitiatingPermOccupancyFraction=<percent>  

 

 

   這個參數的功能和-XX:CMSInitiatingOccupancyFraction很像,他指的是啓動CMS週期的permanent代的佔用率。這個參數同樣需要和-XX:+CMSClassUnloadingEnabled配合使用。如果你想一直使用-XX:CMSInitiatingPermOccupancyFraction的值作為啓動CMS週期的條件,你必須要指定另外一個選項:

 

   

[html] view plain copy

  1. -XX:+UseCMSInitiatingOccupancyOnly  

 

 

CMS暫停時間優化

 

   在CMS週期裏面,有兩個階段是stop-the-world階段,這個階段所有的應用線程都被阻塞了。這兩階段是“初始標記”階段和“再標記”階段,儘管初始標記解決是單線程的,但是通過不需要花費太長時間,至少比其他垃圾回收的時間短。再標記階段是多線程的,線程數可通過命令選項來控制:

 

   

[html] view plain copy

  1. -XX:ParallelGCThreads=<n>  

 

 

   在Java 6 update 23之後,默認值是通過Runtime.availableProcessors()來確定的,不過是建立在返回值小於等於8的情況下,反之,會使用Runtime.availableProcessors()*5/8作為線程數。如果有多個程序運行在同一個機器上面,建議使用比默認線程數更少的線程數。否則,垃圾回收可能會引起其他應用的性能下降,由於在同一個時刻,垃圾回收器使用太多的線程。

 

   在某些情況下設置下面這個選項可以減少再標記的時間:

 

   

[html] view plain copy

  1. -XX:+CMSScavengeBeforeRemark  

 

 

   這個選項強制HotSpot VM在FullGC之前執行MinorGC,在再標記步驟之前做MinorGC,可以減少再標記的工作量,由於減少了young代的對象數,這些對象能夠在old代獲取到的。

 

   如果應用有大量的引用或者finalizable對象需要處理,指定下面這個選項可以減少垃圾回收的時間:

 

   

[html] view plain copy

  1. -XX:+ParallelRefProcEnabled  

 

   這個選項可以用HotSpot VM的任何一種垃圾回收器上,他會是用多個的引用處理線程,而不是單個線程。這個選項不會啓用多線程運行方法的finalizer。他會使用很多線程去發現需要排隊通知的finalizable對象。

 

下一步

 

   這一步結束,你需要看看應用的延遲需要是否滿足了,無論是使用throughput垃圾回收器或者併發垃圾回收器。如果沒有能夠滿足應用的需要,那麼回頭看看需求是否合理或者修改應用程序。如果滿足了應用的需求,那麼我們就進入下一步——優化吞吐量。

 

 

六:優化吞吐量

如果你已經進行完了前面的步驟了,那麼你應該知道這是最後一步了。在這一步裏面,你需要測試應用的吞吐量和為了更高的吞吐量而優化JVM。

   這一步的輸入就是應用的吞吐量性能要求。應用的吞吐量是在應用層面衡量而不是在JVM層面衡量,因此,應用必須要報告出一些吞吐量指標或者應用的某些操作的吞吐量性能指標。觀察到的吞吐量指標然後用可以用來和應用需要的性能指標進行比較,如果達到或者超過要求,那麼這一步就完成了。如果你需要更好的吞吐量的話,有一些JVM優化可以去做。

 

   這一步的另外一個輸入就是,有多少內存可以供應用使用,就想前面説的GC最大化內存原則,越多可用的內存,性能就更好。這條原則不僅僅適用於吞吐量優化,同樣適用於延遲優化。

 

   應用的吞吐量需求可能是無法滿足的。如果是這種情況,那麼就需要重新審視應用吞吐量的需求,應用就需要修改或者改變部署模型。如果上面的一種或者多種情況發生了,那麼你需要重新進行前面的優化步驟。

 

   在前面的步驟裏面,你可能使用吞吐量垃圾回收器解決了問題(通過-XX:+UseParallelOldGC或者-XX:+UsePrallelGC),或者你調整到併發垃圾回收器(CMS)來解決的問題。如果使用的CMS來解決的問題,下面有一些選項來提升應用的吞吐量,下面詳細介紹。如果是使用的吞吐量垃圾回收器,我們將在CMS之後介紹。

   

   CMS吞吐量優化

 

   能夠用來提升CMS吞吐量的選項數量有限,下面列出一些可以單獨使用或者聯合使用的選項:

 

   1、使用一些額外的命令選項,在後面的“額外的性能命令行選項”中詳細介紹。

   2、增加young代的空間大小,增加young代的空間大小,可以減少MinorGC的頻率,就能夠減少在一段時間裏面MinorGC佔用的時間。

   3、增加old代的空間大小,增加old代的空間,可以減少CMS垃圾回收的頻率,減少潛在的碎片,可以減少

stop-the-world垃圾回收。

   4、進一步優化young代堆大小,已經在前面的“優化延遲和響應時間”裏面説過了,以及如何優化eden空間任務後和survivor空間大小以減少對象從young代移動到old也在前面已經説過了。需要注意的是,當優化eden和survivor空間大小的時候考慮到一些權衡。

   5、優化CMS週期的啓動,也在前面説過了。

   

   任何上面提到的優化,或者組合使用上面的選擇,都是減少垃圾回收器佔用CPU時間,把CPU留給應用計算。前面兩種選擇,提供一種可能性來提升吞吐量,但是會有stop-the-world垃圾回收的風險,會增加延遲。

 

   作為指導,不考慮CMS,MinorGC的次數應該減少10%,你可能只能降低1%-3%。通常來講,如果只能減少3%甚至更少,那麼能夠提升的吞吐量空間恐怕就有限了。

 

   吞吐量垃圾回收器優化

 

   優化吞吐量垃圾回收器的目標是避免FullGC或者理想情況下,避免在穩定狀態下FullGC。這個需要優化對象的歲數,這個可以通過制定survivor空間優化完成。你可以讓eden空間更大,可以減少MinorGC的次數。我知道當對象的任期或者歲數達到一定值的時候就會移動到old代,而這個任期就是對象經歷MinorGC的次數,MinorGC的次數越少,對象任期增長越慢,就有可能被MinorGC回收掉,而不是進入old代。

 

   使用HotSpot VM的吞吐量垃圾回收器,可以通過-XX:+UseParallelOldGC和-XX:+UsePrallelGC,這樣可以提供最好的吞吐量。吞吐量垃圾回收器利用了一種叫做自適應大小的特性,自適應大小是基於對象的分配和存活率來自動改變eden空間和survivor空間大小,目的是優化對象的歲數分佈。自適應大小的企圖是提供易用性,容易優化JVM,以致於提供可靠的吞吐量。自適應大小在大多數應用下,能夠很好的工作,但是關閉自適應大小以及優化eden空間和survivor空間以及old代空間是一個探索提升應用吞吐量的一種辦法。關閉自適應大小會改變應用的程序的靈活性,尤其是在修改應用程序,以及隨着時間的推移應用的數據發生了變化。

 

   關閉自適應大小可以使用選項:

 

[html] view plain copy

  1. -XX:-UseAdaptiveSizePolicy  

 

   注意在“-XX”後面的“-”表明關閉UseAdapivieSizePolicy提供的特性。只有吞吐量垃圾回收器支持這個選項。在其他的垃圾回收器上使用這個選項是無用的。

 

-XX:+PrintGCDateStamps, -XX:PrintGCDetails, -XX:-UseAdaptiveSizePolicy (關閉自適應大小), 以及-XX:+PrintAdaptiveSizePolicy:

 

   2010-12-16T21:44:11.444-0600:

[GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:

survived: 224408984

promoted: 10904856

overflow: false

[PSYoungGen: 6515579K->219149K(9437184K)]

8946490K->2660709K(13631488K), 0.0725945 secs]

[Times: user=0.56 sys=0.00, real=0.07 secs]

 

   和以前不同的是,以GCAdaptiveSizePolicy開頭的一些額外信息輸出來了,survived標籤表明“to” survivor空間的對象字節數。在這個例子中,survivor空間佔用量是224408984字節,但是移動到old代的字節數卻有10904856字節。overflow表明young代是否有對象溢出到old代,換句話説,就是表明了“to” survivor是否有足夠的空間來容納從eden空間和“from”survivor空間移動而來的對象。為了更好的吞吐量,期望在應用處於穩定運行狀態下,survivor空間不要溢出。

 

   為了開始優化,你應該關閉自適應大小以及獲取在垃圾回收器日誌裏面額外的survivor空間統計信息,使用這兩個選項-XX:-UseAdaptiveSizePolicy以及-XX:+PrintAdaptiveSizePolicy。這樣提供了一些初始化的信息,以幫助做出優化決定。假如之前使用下面的命令行選項:

 

 

[html] view plain copy

  1. -Xmx13g -Xms13g -Xmn4g -XX:SurvivorRatio=6
  2. XX:+PrintGCDetails  

 

 

  那麼,就應該如下一組命令行選項,來關閉自適應大小和捕獲survivor空間統計信息:

 

 

[html] view plain copy

  1. -XX:SurvivorRatio=6
  2. -XX:+UseParallelOldGC -XX:PrintGCDateStamps -XX:+PrintGCDetails  
  3. -XX:-UseAdaptiveSizePolicy -XX:+PrintAdaptiveSizePolicy  

 

 

   首先在應用穩定運行狀態下尋找FullGC信息,包括日期和時間戳可以用來識別出應用是否從啓動狀態進入了穩定狀態。舉例,如果你知道應用啓動需要30秒時間,那麼在應用啓動30秒之後才觀察垃圾回收。

 

   觀察FullGC信息,你可能會發現有一些短存活時間的對象移動到了old代空間,如果FullGC發生了,首先要確定是old代的空間是FullGC之後存活對象的1.5倍。如果有需要,增加old代的空間來保持1.5倍的指標,這樣,可以保證old代有足夠的空間來處理不在預期內的轉移率(導致短的存活時間的對象移動到old代)或者一些未知的情況——導致了對象的轉移過快,擁有這樣的額外空間,可以延遲甚至可能能夠阻止FullGC的發生。

 

   在確定了old代有足夠的空間之後,就需要觀察MinorGC的狀況。首先需要觀察survivor空間是否溢出,如果survivor空間溢出了,那麼overflow標籤會是true,否則,overload字段會是false。下面是一個survivor空間溢出的例子:

 

   2010-12-18T10:12:33.322-0600:

[GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:

survived: 446113911

promoted: 10904856

overflow: true

[PSYoungGen: 6493788K->233888K(9437184K)]

7959281K->2662511K(13631488K), 0.0797732 secs]

[Times: user=0.59 sys=0.00, real=0.08 secs]

 

   如果survivor空間溢出,對象會再達到任期閥值或者消亡之前被移動到old代。換句話説,對象過快的移動到old代。頻繁的survivor空間溢出會導致FullGC,下面説如何優化survivor。

   

優化survivor空間

 

   優化survivor空間的目標是保持或者老化短時間存活動的對象在young代中,一直到不得不移動到old代中。開始查看每一個MinorGC,尤其是存活的對象字節數。需要注意一點的是,為了避免應用啓動的時候對象對後面分析的干擾,可以考慮放棄應用剛進入穩定狀態的前面5到10個MinorGC信息。

 

   每次MinorGC之後的存活對象數量可以通過-XX:+PrintAdaptiveSizePolicy來查看。在下面的例子中,survivor對象的字節數是224408984。

 

2010-12-16T21:44:11.444-0600:    

[GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:

survived: 224408984

promoted: 10904856

overflow: false

[PSYoungGen: 6515579K->219149K(9437184K)]

8946490K->2660709K(13631488K), 0.0725945 secs]

[Times: user=0.56 sys=0.00, real=0.07 secs]

[GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:

survived: 224408984

promoted: 10904856

overflow: false

[PSYoungGen: 6515579K->219149K(9437184K)]

8946490K->2660709K(13631488K), 0.0725945 secs]

[Times: user=0.56 sys=0.00, real=0.07 secs]

 

   使用最大存活對象數量以及知道目標survivor空間的佔用量,你可以決定出最差survivor空間大小,以使得讓對象老化得更加高效。如果目標survivor空間的佔用率沒有通過-XX:TargetSurvivorRatio=<percent>指定,那麼目標survivor空間的佔用率是50%。

 

   首先為最差的場景優化survivor空間,這個需要找出在MinorGC之後最大的存活對象數量,注意可以忽略應用進入穩定狀態前面的5到10個MinorGC。可以通過awk或者perl腳本來完成這項工作。

 

   調整survivor空間的大小,不是僅僅修改survivor空間的大小以使得比存活的對象字節數更大那麼簡單。需要記住的是,如果不增加young代的空間大小,而增加survivor空間的大小,會減少eden空間的大小,這樣會導致頻繁的MinorGC,從而是的對象的老化速度加快,更快的進入old代,又會導致FullGC。所以,需要同步增加young代的空間大小。如果不增加old的空間,那麼就有可能造成頻繁的FullGC甚至內存溢出錯誤。因此,如果有可以獲取的空間,需要同步增加Java堆的空間。

 

   同樣建議,HotSpot Vm使用默認的目標survivor空間佔用率(50%),如果使用了-XX:TargetSurvivorRatio=<percent>,會使用<percent>作為MinorGC之後目標survivor空間佔用率。如果survivor空間的佔用率可能超過這個目標值,會在對象達到最大歲數之前把對象移動到old代去。

 

   通過一個例子詳細説明,考慮用下面的命令選項:

[html] view plain copy

  1. -Xmx13g -Xms13g -Xmn4g -XX:SurvivorRatio=6
  2. -XX:+UseParallelOldGC -XX:-UseAdaptiveSizePolicy  
  3. -XX:PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintAdaptiveSizePolicy  

 

   總共的Java堆空間是13g,young代是4g,old代是9g,survivor空間的大小是4g/(6+2)=512M。假如一個應用的存活對象是470M,由於沒有明確指定-XX:TargetSurvivorRatio=<percent>,那麼默認的目標survivor空間佔用率是50%,那麼最小的survivor空間應該是940M,也就是最壞的情況,需要設置940M的survivor空間。

   從上面的例子來看,4g的young代空間被分隔成兩個512M的survivor空間和一個3g的eden空間。剛才分析的最壞情況分配給survivor的空間是940M,差不多和1g相當。為了保持對象老化速率,即保持MinorGC的頻率,eden空間需要保持在3g。因此,young代需要給每一個survivor空間1g內存以及3g的eden空間,那麼young代需要增加到5g,也就是説young代需要增加1g空間,需要把-Xmn4g選項改成-Xmn5g選項。比較的理想的情況是,同步把Java堆的空間也增加1g。但是如果內存不夠用,需要保證old代空間大小至少是存活對象的1.5倍。

 

   假設應用的內存需求滿足,增加survivor空間佔用後的命令選項是:

 

    

[html] view plain copy

  1. -XX:SurvivorRatio=3
  2. -XX:+UseParallelOldGC -XX:-UseAdaptiveSizePolicy  
  3. -XX:PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintAdaptiveSizePolicy  

 

 

   old空間還是9g,young代的空間是5g,比之前大了1g,eden還是3g,每一個survivor空間是1g。

 

   你可能需要重複多次設定大小,直到滿足內存佔用的條件下到達吞吐量的峯值。吞吐的峯值一般都是在對象最有效的老化的時候的達到的。

 

   通常的建議是,吞吐量垃圾回收器的垃圾回收的開銷應該小於5%。如果你只能把這個開銷降低1%甚至更少,你可能需要使用除本章描述之外的特別努力和很大的開銷來優化JVM。

  

優化Parallel GC線程

 

   吞吐量垃圾回收器的線程數的優化同樣基於有多少應用運行在同一個系統裏面以及硬件平台。就像前面的“優化CMS”裏面提到的,如果是多個應用運行在同一個系統上面,建議使用比垃圾回收器默認使用的線程數更少的線程數,使用選項是-XX:ParallelGCThreads=<n>.

 

  另外,由於大量的垃圾回收線程同時執行,垃圾回收可能會嚴重影響其他應用的性能。由於Java 6 Update 23之後,默認的垃圾回收線程是執行Runtime.availableProcessors()獲得的,如果這個方法的返回值小於等於8,那麼就用這個返回值,如果比8更大,那麼就取這個值的5/8。如果運行多個應用,可以根據應用的情況來分配線程數,如果應用的消耗是相當的,那麼就用CPU的內核數除以應用數得到每一個應用可以分配的線程。如果應用的load不相當,那麼就可以根據應用的實際情況來衡量和分配。

   

下一步

   如果你到這一步都還沒有能夠達到吞吐量的要求,那麼可以嘗試後面的“額外的性能選項”,如果還是無法達到,就只能修改應用或者JVM部署結構了。如果進行了修改應用或者修改了部署結構,你需要重新做前面的各個步驟。

   可能會用到的一些邊緣場景,下面一節介紹。

 

七:其他

邊緣問題

   在某些場景下,按照前面的一步步優化指導無法產生效果。這一節説明一下這些情況。

 

   一些應用分配了一些少量的非常大的長時間存活的對象。這樣的場景需要需要young代的空間比old代更大。

 

   一些應用會經歷很少的對象轉移。這樣的場景可能需要old代的空間遠遠大於存活對象的大小,由於old的佔用量增長率很小。

 

   一些應用有小延遲需求,會使用CMS垃圾回收器,而且使用小young代空間(以致於MinorGC時間更短),以及大的old代空間。在這種配置下,對象會快速的從young代移動到old代,替代了高效老化對象。另外,CMS垃圾回收移動後的對象,碎片的可能性通過大的old代空間來解決。

 

   下一節介紹一些其他的HotSpot VM選項來提升應用的性能。

 

其他一些的性能命令行選項

 

   幾個可選的前面有提到的命令選項可以用來提升Java應用的延遲和吞吐量性能,這些選項是通過JIT編譯器代碼優化以及其他的HotSpot VM優化能力。下面介紹這些特性以及相適應的命令選項。

 

   最新和最大優化

   當新的性能優化集成到HotSpot VM中之後,可以通過-XX:+AggressiveOpts選項來啓用。

 

   通過選項來引入新的優化,可以把最新及最大的優化和以及經過長時間使用證明是穩定的優化分離開。應用通常更希望獲得更好的穩定性,畢竟最新的優化可能會導致未知的問題。但是如果應用需要提升任何可以提升的性能優化的時候,可以使用命令選項來啓用這些優化。

 

   當新的優化被證明是穩定的之後,他們會被默認使用,也許需要升級幾個版本之後才會變成默認。

 

   使用-XX:+AggressiveOpts命令選項之後,需要考慮到性能的提升,同樣也需要考慮到性能提升所帶來的不穩定風險。

 

   逃避分析

 

   逃避分析是一個種分析Java對象範圍的技術,在特殊情況下,一個線程分配的對象可能被另外一個線程使用,這個對象就叫着“逃避”。如果對象沒有逃避,額外的優化技術可以應用,因此,這種優化技術叫做逃避分析。

 

   在HotSpot VM裏面的逃避分析優化可以通過命令行選項:

 

[html] view plain copy

  1. -XX:+DoEscapeAnalysis  

   

   這是在Java 6 update 14中引入的,而且自動啓用通過-XX:+AggressiveOpts。在Java 6 update 23中是默認開啓的。

 

   通過逃避分析,HotSpot VM JIT編譯器,可應用下面的優化技術:

 

   1、對象爆炸:對象爆炸是一種對象的屬性存儲在Java堆以外而且可能潛在的消失。比如説,對象屬性可以直接被放置到內存的寄存器裏面或者對象被分配棧裏面而不是堆裏面。

 

   分等級替換:分等級替換是一種用來減少內存使用的優化技術,考慮下面的Java類,表現為保存長方形的長和寬:

public class Rectangle {

  int length;

  int width;

}

   HotSpot VM可以優化內存分配和使用非逃避的Rectangle類的實例通過把長和寬都直接存儲到CPU的寄存器而不是分配Rectangle對象,結果是當時需要使用長和寬屬性的時候,不需要再複製到CPU的寄存器。這個可以減少內存的讀取。

 

   2、線程棧分配:顧名思義,線程棧分配是一種把對象分配到線程棧中,而不是Java堆裏面的優化技術。一個對象永遠不逃避,就可以放置到線程棧框架裏面,由於沒有其他線程需要看到這個對象。線程棧分配可以減少對象分配到Java堆,可以減少GC的頻率。

 

   3、消滅同步:如果線程分配的對象從來不會逃避,而且這個線程鎖定了這個對象,這個鎖可能會被JIT編譯器消滅,畢竟沒有其他線程會使用這個對象。

 

   4、消滅垃圾回收讀寫障礙:如果線程分配的對象從來不會逃避,只會被當前線程使用,所以在其他對象裏面存儲它的地址不需要障礙。讀或者寫障礙只有在對象會被其他線程使用的時候才有需要。

 

有偏見的鎖

 

   有偏見的鎖是使得鎖更偏愛上次使用到它線程。在非競爭鎖的場景下,即只有一個線程會鎖定對象,可以實現近乎無鎖的開銷。

 

   有偏見的鎖,是在Java 5 update 6引入的。通過HotSpot VM的命令選項-XX:+UseBiasedLocking啓用。

 

   Java 5 HotSpot JDK需要明確的命令來啓用這個特性,在使用-XX:+AggressiveOpts選項,有偏見的鎖會Java 5中會被自動啓用。在Java 6中是默認啓用的。

 

   各種經歷告訴我們這個特性對大多數應用還是非常有用的。然後,有一些應用使用這個屬性不一定能夠表現的很好,比如,鎖被通常不被上次使用它的同一個線程使用。對於Java應用來説,由於stop-the-world安全點操作需要取消偏見,這樣可以通過使用-XX:-UseBiaseLocking來獲得好處。如果你不清楚你的應用是什麼情況,可以通過分別設置這兩個選項來測試。

 

大頁面

 

   在計算機系統中,內存被分為固定大小的區塊,這個區塊就叫做頁(page)。內存的存取是通過程序把虛擬內存地址轉換成物理內存地址實現的。虛擬到物理地址是在一個塊表裏面映射的。為了減少每次存取內存的時候使用頁表的消耗,通常會使用一種快速的虛擬到物理地址轉換的緩存。這個緩存叫做轉換後備緩衝區(translation lookaside buffer),簡稱TLB。

 

   通過TLB來滿足虛擬到物理地址的映射請求,會比遍歷頁表來找到映射關係快很多,一個TLB通常包含指定數量的條目。一個TLB條目是一個基於頁大小虛擬到物理地址映射,因此,更大的頁大小允許一個條目或者一個TLB有更大的內存地址範圍。在TLB中有更廣泛的地址,更少的地址轉換請求在TLB中不命中,就可以減少遍歷頁表(page table)操作。使用大頁的目的就是減少TLB的不命中。

 

   Oracle solariz,Linux 以及Windows都支持HotSpot VM使用大頁。通常處理器可以支持幾種頁大小,不過不同的處理器各不相同。另外,操作系統配置需要使用大頁。

 

   下面説説怎麼樣在Linux下使用大頁(Large Page)

 

Linux下的大頁面

  在寫作本書的時候,在Linux下使用大頁,除使用-XX:+UseLargePages命令選項以外,需要修改操作系統配置。Linux的修改操作具體和發行版本以及內核有關係。為了合理的啓用Linux下的大頁,可以徵詢Linux管理員的意見或者閲讀Linux發行文檔。一旦使用了Linux操作系統配置已經修改,-XX:+UseLargePage命令行選項就必須要使用了。比如:

[html] view plain copy

  1. $ java -server -Xmx1024m -Xms1024m -Xmn256m -XX:+UseLargePages ...  

-XX:+UseLargePages是一個有效的選項,不過會報告無法獲取大頁,而且會退回操作系統的默認頁大小。

 

 

PS:打完收工,其實翻譯挺無聊和挺累的