1. 程式人生 > 實用技巧 >JVM效能調優經驗總結

JVM效能調優經驗總結

本文轉載自JVM效能調優經驗總結

說明

調優是一個循序漸進的過程,必然需要經歷多次迭代,最終才能換取一個較好的折中方案。

在JVM調優這個領域,沒有任何一種調優方案是適用於所有應用場景的,同時,切勿極端才能夠達到JVM效能調優的真正目的和意義。

調優策略

核心目標

  • GC的時間足夠的小

  • GC的次數足夠的少

  • 發生Full GC的週期足夠的長

    前兩個目前是相悖的,要想GC時間小必須要一個更小的堆,要保證GC次數足夠少,必須保證一個更大的堆,我們只能取其平衡。

    更大的年輕代必然導致更小的年老代,大的年輕代會延長普通GC的週期,但會增加每次GC的時間;小的年老代會導致更頻繁的Full GC

    更小的年輕代必然導致更大年老代,小的年輕代會導致普通GC很頻繁,但每次的GC時間會更短;大的年老代會減少Full GC的頻率

    如何選擇應該依賴應用程式物件生命週期的分佈情況:如果應用存在大量的臨時物件,應該選擇更大的年輕代;如果存在相對較多的持久物件,年老代應該適當增大。

    但很多應用都沒有這樣明顯的特性,在抉擇時應該根據以下兩點:a、本著Full GC儘量少的原則,讓年老代儘量快取常用物件,JVM的預設比例3:8也是這個道理(B)通過觀察應用一段時間,看其他在峰值時年老代會佔多少記憶體,在不影響FullGC的前提下,根據實際情況加大年輕代,比如可以把比例控制在1:1。但應該給年老代至少預留1/3的增長空間

將新物件預留在年輕代

眾所周知,由於 Full GC 的成本遠遠高於 Minor GC,因此某些情況下需要儘可能將物件分配在年輕代,這在很多情況下是一個明智的選擇。雖然在大部分情況下,JVM 會嘗試在 Eden 區分配物件,但是由於空間緊張等問題,很可能不得不將部分年輕物件提前向年老代壓縮。因此,在 JVM 引數調優時可以為應用程式分配一個合理的年輕代空間,以最大限度避免新物件直接進入年老代的情況發生。

通過設定一個較大的年輕代預留新物件,設定合理的 Survivor 區並且提供 Survivor 區的使用率,可以將年輕物件儲存在年輕代。一般來說,Survivor 區的空間不夠,或者佔用量達到 50%時,就會使物件進入年老代(不管它的年齡有多大)

我們可以嘗試加上

-XX:TargetSurvivorRatio=90

引數,這樣可以提高 from 區的利用率,使 from 區使用到 90%時,再將物件送入年老代

讓大物件進入年老代

我們在大部分情況下都會選擇將物件分配在年輕代。但是,對於佔用記憶體較多的大物件而言,它的選擇可能就不是這樣的。因為大物件出現在年輕代很可能擾亂年輕代 GC,並破壞年輕代原有的物件結構。因為嘗試在年輕代分配大物件,很可能導致空間不足,為了有足夠的空間容納大物件,JVM 不得不將年輕代中的年輕物件挪到年老代。因為大物件佔用空間多,所以可能需要移動大量小的年輕物件進入年老代,這對 GC 相當不利。

基於以上原因,可以將大物件直接分配到年老代,保持年輕代物件結構的完整性,這樣可以提高 GC 的效率。如果一個大物件同時又是一個短命的物件,假設這種情況出現很頻繁,那對於 GC 來說會是一場災難。原本應該用於存放永久物件的年老代,被短命的物件塞滿,這也意味著對堆空間進行了洗牌,擾亂了分代記憶體回收的基本思路。因此,在軟體開發過程中,應該儘可能避免使用短命的大物件。

可以使用引數

-XX:PetenureSizeThreshold

設定大物件直接進入年老代的閾值。當物件的大小超過這個值時,將直接在年老代分配。引數-XX:PetenureSizeThreshold 只對序列收集器和年輕代並行收集器有效,並行回收收集器不識別這個引數。

設定物件進入年老代的年齡

如何設定物件進入年老代的年齡
堆中的每一個物件都有自己的年齡。一般情況下,年輕物件存放在年輕代,年老物件存放在年老代。為了做到這點,虛擬機器為每個物件都維護一個年齡。如果物件在 Eden 區,經過一次 GC 後依然存活,則被移動到 Survivor 區中,物件年齡加 1。以後,如果物件每經過一次 GC 依然存活,則年齡再加 1。當物件年齡達到閾值時,就移入年老代,成為老年物件。

這個閾值的最大值可以通過引數

-XX:MaxTenuringThreshold

來設定,預設值是 15。雖然-XX:MaxTenuringThreshold 的值可能是 15 或者更大,但這不意味著新物件非要達到這個年齡才能進入年老代。事實上,物件實際進入年老代的年齡是虛擬機器在執行時根據記憶體使用情況動態計算的,這個引數指定的是閾值年齡的最大值。即,實際晉升年老代年齡等於動態計算所得的年齡與-XX:MaxTenuringThreshold 中較小的那個。

穩定的 Java 堆 VS 動盪的 Java 堆

一般來說,穩定的堆大小對垃圾回收是有利的。獲得一個穩定的堆大小的方法是使-Xms 和-Xmx 的大小一致,即最大堆和最小堆 (初始堆)一樣

如果這樣設定,系統在執行時堆大小理論上是恆定的,穩定的堆空間可以減少 GC 的次數。因此,很多服務端應用都會將最大堆和最小堆設定為相同的數值。但是,一個不穩定的堆並非毫無用處。穩定的堆大小雖然可以減少 GC 次數,但同時也增加了每次GC的時間。讓堆大小在一個區間中震盪,在系統不需要使用大記憶體時,壓縮堆空間,使 GC 應對一個較小的堆,可以加快單次 GC 的速度。基於這樣的考慮,JVM 還提供了兩個引數用於壓縮和擴充套件堆空間。

-XX:MinHeapFreeRatio

引數用來設定堆空間最小空閒比例,預設值是 40。當堆空間的空閒記憶體小於這個數值時,JVM 便會擴充套件堆空間。

-XX:MaxHeapFreeRatio

引數用來設定堆空間最大空閒比例,預設值是 70。當堆空間的空閒記憶體大於這個數值時,便會壓縮堆空間,得到一個較小的堆。

當-Xmx 和-Xms 相等時,-XX:MinHeapFreeRatio 和-XX:MaxHeapFreeRatio 兩個引數無效。

增大吞吐量提升系統性能

吞吐量優先的方案將會盡可能減少系統執行垃圾回收的總時間,故可以考慮關注系統吞吐量的並行回收收集器。在擁有高效能的計算機上,進行吞吐量優先優化,可以使用引數:

java –Xmx3800m –Xms3800m –Xmn2G –Xss128k –XX:+UseParallelGC 
   –XX:ParallelGC-Threads=20 –XX:+UseParallelOldGC

–Xmx380m –Xms3800m:設定 Java 堆的最大值和初始值。一般情況下,為了避免堆記憶體的頻繁震盪,導致系統性能下降,我們的做法是設定最大堆等於最小堆。假設這裡把最小堆減少為最大堆的一半,即 1900m,那麼 JVM 會盡可能在 1900MB 堆空間中執行,如果這樣,發生 GC 的可能性就會比較高;

-Xss128k:減少執行緒棧的大小,這樣可以使剩餘的系統記憶體支援更多的執行緒;

-Xmn2g:設定年輕代區域大小為 2GB;

–XX:+UseParallelGC:年輕代使用並行垃圾回收收集器。這是一個關注吞吐量的收集器,可以儘可能地減少 GC 時間。

–XX:ParallelGC-Threads:設定用於垃圾回收的執行緒數,通常情況下,可以設定和 CPU 數量相等。但在 CPU 數量比較多的情況下,設定相對較小的數值也是合理的;

–XX:+UseParallelOldGC:設定年老代使用並行回收收集器。

嘗試使用大的記憶體分頁

CPU 是通過定址來訪問記憶體的。32 位 CPU 的定址寬度是 0~0xFFFFFFFF ,計算後得到的大小是 4G,也就是說可支援的實體記憶體最大是 4G。但在實踐過程中,碰到了這樣的問題,程式需要使用 4G 記憶體,而可用實體記憶體小於 4G,導致程式不得不降低記憶體佔用。為了解決此類問題,現代 CPU 引入了 MMU(Memory Management Unit 記憶體管理單元)。MMU 的核心思想是利用虛擬地址替代實體地址,即 CPU 定址時使用虛址,由 MMU 負責將虛址對映為實體地址。MMU 的引入,解決了對實體記憶體的限制,對程式來說,就像自己在使用 4G 記憶體一樣。記憶體分頁 (Paging) 是在使用 MMU 的基礎上,提出的一種記憶體管理機制。它將虛擬地址和實體地址按固定大小(4K)分割成頁 (page) 和頁幀 (page frame),並保證頁與頁幀的大小相同。這種機制,從資料結構上,保證了訪問記憶體的高效,並使 OS 能支援非連續性的記憶體分配。在程式記憶體不夠用時,還可以將不常用的實體記憶體頁轉移到其他儲存裝置上,比如磁碟,這就是大家耳熟能詳的虛擬記憶體。

在 Solaris 系統中,JVM 可以支援 Large Page Size 的使用。使用大的記憶體分頁可以增強 CPU 的記憶體定址能力,從而提升系統的效能。

java –Xmx2506m –Xms2506m –Xmn1536m –Xss128k –XX:++UseParallelGC
 –XX:ParallelGCThreads=20 –XX:+UseParallelOldGC –XX:+LargePageSizeInBytes=256m

–XX:+LargePageSizeInBytes:設定大頁的大小。

過大的記憶體分頁會導致 JVM 在計算 Heap 內部分割槽(perm, new, old)記憶體佔用比例時,會出現超出正常值的劃分,最壞情況下某個區會多佔用一個頁的大小。

使用非佔有的垃圾回收器

為降低應用軟體的垃圾回收時的停頓,首先考慮的是使用關注系統停頓的 CMS 回收器,其次,為了減少 Full GC 次數,應儘可能將物件預留在年輕代,因為年輕代 Minor GC 的成本遠遠小於年老代的 Full GC。

java –Xmx3550m –Xms3550m –Xmn2g –Xss128k –XX:ParallelGCThreads=20
 –XX:+UseConcMarkSweepGC –XX:+UseParNewGC –XX:+SurvivorRatio=8 –XX:TargetSurvivorRatio=90
 –XX:MaxTenuringThreshold=31

–XX:ParallelGCThreads=20:設定 20 個執行緒進行垃圾回收;

–XX:+UseParNewGC:年輕代使用並行回收器;

–XX:+UseConcMarkSweepGC:年老代使用 CMS 收集器降低停頓;

–XX:+SurvivorRatio:設定 Eden 區和 Survivor 區的比例為 8:1。稍大的 Survivor 空間可以提高在年輕代回收生命週期較短的物件的可能性,如果 Survivor 不夠大,一些短命的物件可能直接進入年老代,這對系統來說是不利的。

–XX:TargetSurvivorRatio=90:設定 Survivor 區的可使用率。這裡設定為 90%,則允許 90%的 Survivor 空間被使用。預設值是 50%。故該設定提高了 Survivor 區的使用率。當存放的物件超過這個百分比,則物件會向年老代壓縮。因此,這個選項更有助於將物件留在年輕代。

–XX:MaxTenuringThreshold:設定年輕物件晉升到年老代的年齡。預設值是 15 次,即物件經過 15 次 Minor GC 依然存活,則進入年老代。這裡設定為 31,目的是讓物件儘可能地儲存在年輕代區域。

一些監測JVM的命令

  • jps

列出正在執行的虛擬機器程序

  • jstat

監視虛擬機器執行狀態資訊

  • jmap

生成堆儲存快照

  • jstack

生成虛擬機器當前時刻的執行緒快照,幫助定位執行緒出現長時間停頓的原因

一些JVM引數

設定堆記憶體大小

  1. -Xms:啟動JVM時的堆記憶體空間。
  2. -Xmx:堆記憶體最大限制。
  3. -Xmn:設定年輕代大小整個堆大小=年輕代大小 + 年老代大小 + 持久代大小,Sun官方推薦配置為整個堆的3/8
  4. -XX:PermSize=128M設定持久代大小
  5. -XX:MaxPermSize=128M設定持久代最大值,此值可以設定與-XX:PermSize相同,防止持久代記憶體伸縮,持久代設定很重要,一般預留其使用空間的1/3.

設定新生代大小。

  1. -XX:NewRatio:新生代和老年代的佔比。
  2. -XX:NewSize:新生代空間。
  3. -XX:SurvivorRatio:伊甸園空間和倖存者空間的佔比。
  4. -XX:MaxTenuringThreshold:物件進入老年代的年齡閾值。

設定垃圾回收器

  1. -XX:+UseSerialGC 開啟序列收集器
  2. -XX:+UseParallelGC開啟年輕代並行收集器,JDK5.0以上,JVM會根據系統配置自行設定,所以無需再設定此值
  3. -XX:+UseParallelOldGC開啟老年代並行收集器
  4. -XX:+UseConcMarkSweepGC開啟老年代併發收集器(簡稱CMS),可以和UseParallelGC一起使用
  5. -XX:CMSInitiatingOccupancyFraction=70老年代記憶體使用比例到多少啟用CMS收集器,這個數值的設定有很大技巧基本上滿足(Xmx-Xmn)*(100-CMSInitiatingOccupancyFraction)/100>=Xmn否則會出現“Concurrent Mode Failure”,promotionfailed,官方建議數值為68
  6. -XX:+UseCMSCompactAtFullCollection:使用併發收集器時,開啟對年老代的壓縮

其他

  1. -Xss: 設定每個執行緒的堆疊大小,設定每個執行緒的堆疊大小。JDK5.0以後每個執行緒堆疊大小為1M,以前每個執行緒堆疊大小為256K。更具應用的執行緒所需記憶體大小進行調整。在相同實體記憶體下,減小這個值能生成更多的執行緒。但是作業系統對一個程序內的執行緒數還是有限制的,不能無限生成,經驗值在3000~5000左右,這個引數對效能的影響比較大的
  2. -XX:MaxTenuringThreshold=0:設定垃圾最大年齡。如果設定為0的話,則年輕代物件不經過Survivor區,直接進入年老代。對於年老代比較多的應用,可以提高效率。如果將此值設定為一個較大值,則年輕代物件會在Survivor區進行多次複製,這樣可以增加物件再年輕代的存活時間,增加在年輕代即被回收的概論,linux64的java6預設值是15
  3. -XX:ParallelGCThreads=設定並行垃圾回收的執行緒數。此值可以設定與機器處理器數量相等(邏輯cpu數),這個不確定是物理、還是邏輯使用預設就好
  4. -XX:MaxGCPauseMillis=指定垃圾回收時的最長暫停時間,單位毫秒,如果指定了此值的話,堆大小和垃圾回收相關引數會進行調整以達到指定值,設定此值可能會減少應用的吞吐量
  5. -XX:GCTimeRatio=設定吞吐量為垃圾回收時間與非垃圾回收時間的比值,公式為1/(1+N)。例如,-XX:GCTimeRatio=19時,表示5%的時間用於垃圾回收。預設情況為99,即1%的時間用於垃圾回收
  6. -XX:+UseAdaptiveSizePolicy:設定此選項後,並行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目標系統規定的最低相應時間或者收集頻率等,此值建議使用並行收集器時,一直開啟
  7. -XX:+DisableExplicitGC:禁止 java 程式中的 full gc, 如System.gc() 的呼叫. 最好加上, 防止程式在程式碼裡誤用了對效能造成衝擊
  8. -XX:+PrintGCDetails 打應垃圾收集的情況
  9. -XX:+PrintGCTimeStamps 打應垃圾收集的情況
  10. -XX:+PrintGCApplicationConcurrentTime:列印每次垃圾回收前,程式未中斷的執行時間。可與上面混合使用
  11. -XX:+PrintGCApplicationStoppedTime 打應垃圾收集時 , 系統的停頓時間
  12. -XX:+PrintGC 列印GC情況
  13. -XX:PrintHeapAtGC:列印GC前後的詳細堆疊資訊

(本文完)

參考: