1. 程式人生 > >JVM調優(二)經驗參數設置

JVM調優(二)經驗參數設置

too 語言 相關 jdk5 nta 回收算法 from 情況 根據

調優設置具體解析

  堆大小設置

    JVM 中最大堆大小有三方面限制:相關操作系統的數據模型(32-bt還是64-bit)限制;系統的可用虛擬內存限制;系統的可用物理內存限制。32位系統下,一般限制在1.5G~2G;64為操作系統對內存無限制。

    在Windows Server 2003 系統,3.5G物理內存,JDK5.0下測試,最大可設置為1478m。
    典型設置:
    • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
    • -Xms3550m:設置JVM最大可用堆內存為3550M,設置JVM初始堆內存為3550m。此值可以設置與-Xmx相同,以避免每次垃圾回收完成後JVM重新分配內存。
    • -Xmn2g:設置年輕代大小為2G。整個JVM內存大小=年輕代大小 + 年老代大小 + 持久代大小。持久代一般固定大小為64m,所以增大年輕代後,將會減小年老代大小。此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8。(註意:從Java 8開始,HotSpot虛擬機中刪除了“持久代”)。
    • -Xss128k:設置每個線程的堆棧大小。JDK5.0以後每個線程堆棧大小為1M,以前每個線程堆棧大小為256K。更具應用的線程所需內存大小進行調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。
    • java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
    • -XX:NewRatio=4 設置年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代),設置為4,則年輕代與年老代所占比值為1:4,年輕代占整個堆棧的1/5。
    • -XX:SurvivorRatio=4:設置年輕代中Eden區與Survivor區的大小比值,設置為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區占整個年輕代的1/2 * 1/3 = 1/6
    • -XX:MaxPermSize=16m:設置持久代大小為16m。
    • -XX:MaxTenuringThreshold=0:設置垃圾最大年齡。如果設置為0的話,則年輕代對象不經過Survivor區,直接進入年老代
      。對於年老代比較多的應用,可以提高效率。如果將此值設置為一個較大值,則年輕代對象會在Survivor區進行多次復制,這樣可以增加對象在年輕代的存活時間,增加在年輕代被回收的概率。
  1. 回收器選擇

    JVM給了三種選擇:串行收集器、並行收集器、並發收集器,但是串行收集器只適用於小數據量的情況,所以這裏的選擇主要針對並行收集器和並發收集器。默認情況下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在啟動時加入相應參數。JDK5.0以後,JVM會根據當前系統配置行判斷。
    1. 吞吐量優先的並行收集器
      如上文所述,並行收集器主要以到達一定的吞吐量為目標,適用於科學技術和後臺處理等。
      典型配置:
      • java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
        -XX:+UseParallelGC:選擇垃圾收集器為並行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用並發收集,而年老代仍舊使用串行收集。
        -XX:ParallelGCThreads=20:配置並行收集器的線程數,即:同時多少個線程一起進行垃圾回收。此值最好配置與處理器數目相等。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
        -XX:+UseParallelOldGC:配置年老代垃圾收集方式為並行收集。JDK6.0支持對年老代並行收集。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
        -XX:MaxGCPauseMillis=100:設置每次年輕代垃圾回收的最長時間,如果無法滿足此時間,JVM會自動調整年輕代大小,以滿足此值。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
        -XX:+UseAdaptiveSizePolicy:設置此選項後,並行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目標系統規定的最低響應時間或者收集頻率等,此值建議使用並行收集器時,一直打開
    2. 響應時間優先的並發收集器
      如上文所述,並發收集器主要是保證系統的響應時間,減少垃圾收集時的停頓時間。適用於應用服務器、電信領域等。
      典型配置:
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
        -XX:+UseConcMarkSweepGC:設置年老代為並發收集。測試中配置這個以後,-XX:NewRatio=4的配置失效了,原因不明。所以,此時年輕代大小最好用-Xmn設置
        -XX:+UseParNewGC:設置年輕代為並行收集。可與CMS收集同時使用。JDK5.0以上,JVM會根據系統配置自行設置,所以無需再設置此值。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
        -XX:CMSFullGCsBeforeCompaction:由於並發收集器不對內存空間進行壓縮、整理,所以運行一段時間以後會產生“碎片”,使得運行效率降低。此值設置運行多少次GC以後對內存空間進行壓縮、整理。
        -XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮。可能會影響性能,但是可以消除碎片。
  2. 輔助信息

    JVM提供了大量命令行參數,打印信息,供調試使用。主要有以下一些:
    • -XX:+PrintGC
      輸出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]

      [Full GC 121376K->10414K(130112K), 0.0650971 secs]

    • -XX:+PrintGCDetails
      輸出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]

      [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

    • -XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可與上面兩個混合使用。
      輸出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
    • -XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中斷的執行時間。可與上面混合使用。
      輸出形式:Application time: 0.5291524 seconds
    • -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期間程序暫停的時間。可與上面混合使用。
      輸出形式:Total time for which application threads were stopped: 0.0468229 seconds
    • -XX:PrintHeapAtGC:打印GC前後的詳細堆棧信息。
      輸出形式:
      34.702: [GC {Heap before gc invocations=7:
      def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)
      eden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)
      from space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000)
      to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000)
      tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)
      the space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)
      compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
      the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
      ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
      rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
      34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:
      def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)
      eden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)
      from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)
      to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000)
      tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)
      the space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)
      compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
      the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
      ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
      rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
      }
      , 0.0757599 secs]
    • -Xloggc:filename:與上面幾個配合使用,把相關日誌信息記錄到文件以便分析。
  3. 常見配置匯總
    1. 堆設置
      • -Xms:初始堆大小
      • -Xmx:最大堆大小
      • -XX:NewSize=n:設置年輕代大小
      • -XX:NewRatio=n:設置年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4
      • -XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。註意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區占整個年輕代的1/5
      • -XX:MaxPermSize=n:設置持久代大小
    2. 收集器設置
      • -XX:+UseSerialGC:設置串行收集器
      • -XX:+UseParallelGC:設置並行收集器
      • -XX:+UseParalledlOldGC:設置並行年老代收集器
      • -XX:+UseConcMarkSweepGC:設置並發收集器
    3. 垃圾回收統計信息
      • -XX:+PrintGC
      • -XX:+PrintGCDetails
      • -XX:+PrintGCTimeStamps
      • -Xloggc:filename
    4. 並行收集器設置
      • -XX:ParallelGCThreads=n:設置並行收集器收集時使用的CPU數。並行收集線程數。
      • -XX:MaxGCPauseMillis=n:設置並行收集最大暫停時間
      • -XX:GCTimeRatio=n:設置垃圾回收時間占程序運行時間的百分比。公式為1/(1+n)
    5. 並發收集器設置
      • -XX:+CMSIncrementalMode:設置為增量模式。適用於單CPU情況。
      • -XX:ParallelGCThreads=n:設置並發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集線程數。

調優總結

  1. 年輕代大小選擇
    • 響應時間優先的應用:盡可能設大,直到接近系統的最低響應時間限制(根據實際情況選擇)。在此種情況下,年輕代收集發生的頻率也是最小的。同時,減少到達年老代的對象。
    • 吞吐量優先的應用:盡可能的設置大,可能到達Gbit的程度。因為對響應時間沒有要求,垃圾收集可以並行進行,一般適合8CPU以上的應用。
  2. 年老代大小選擇
    • 響應時間優先的應用:年老代使用並發收集器,所以其大小需要小心設置,一般要考慮並發會話率和會話持續時間等一些參數。如果堆設置小了,可以會造成內存碎片、高回收頻率以及應用暫停而使用傳統的標記清除方式;如果堆大了,則需要較長的收集時間。最優化的方案,一般需要參考以下數據獲得:
      • 並發垃圾收集信息
      • 持久代並發收集次數
      • 傳統GC信息
      • 花在年輕代和年老代回收上的時間比例
      減少年輕代和年老代花費的時間,一般會提高應用的效率
    • 吞吐量優先的應用:一般吞吐量優先的應用都有一個很大的年輕代和一個較小的年老代。原因是,這樣可以盡可能回收掉大部分短期對象,減少中期的對象,而年老代盡存放長期存活對象。
  3. 較小堆引起的碎片問題
    因為年老代的並發收集器使用標記、清除算法,所以不會對堆進行壓縮。當收集器回收時,他會把相鄰的空間進行合並,這樣可以分配給較大的對象。但是,當堆空間較小時,運行一段時間以後,就會出現“碎片”,如果並發收集器找不到足夠的空間,那麽並發收集器將會停止,然後使用傳統的標記、清除方式進行回收。如果出現“碎片”,可能需要進行如下配置:
    • -XX:+UseCMSCompactAtFullCollection:使用並發收集器時,開啟對年老代的壓縮。
    • -XX:CMSFullGCsBeforeCompaction=0:上面配置開啟的情況下,這裏設置多少次Full GC後,對年老代進行壓縮

垃圾回收的瓶頸

  傳統分代垃圾回收方式,已經在一定程度上把垃圾回收給應用帶來的負擔降到了最小,把應用的吞吐量推到了一個極限。但是他無法解決的一個問題,就是Full GC所帶來的應用暫停。在一些對實時性要求很高的應用場景下,GC暫停所帶來的請求堆積和請求失敗是無法接受的。這類應用可能要求請求的返回時間在幾百甚至幾十毫秒以內,如果分代垃圾回收方式要達到這個指標,只能把最大堆的設置限制在一個相對較小範圍內,但是這樣有限制了應用本身的處理能力,同樣也是不可接受的。

  分代垃圾回收方式確實也考慮了實時性要求而提供了並發回收器,支持最大暫停時間的設置,但是受限於分代垃圾回收的內存劃分模型,其效果也不是很理想。

  為了達到實時性的要求(其實Java語言最初的設計也是在嵌入式系統上的),一種新垃圾回收方式呼之欲出,它既支持短的暫停時間,又支持大的內存空間分配。可以很好的解決傳統分代方式帶來的問題。

增量收集的演進

  增量收集的方式在理論上可以解決傳統分代方式帶來的問題。增量收集把對堆空間劃分成一系列內存塊,使用時,先使用其中一部分(不會全部用完),垃圾收集時把之前用掉的部分中的存活對象再放到後面沒有用的空間中,這樣可以實現一直邊使用邊收集的效果,避免了傳統分代方式整個使用完了再暫停的回收的情況。

  當然,傳統分代收集方式也提供了並發收集,但是他有一個很致命的地方,就是把整個堆做為一個內存塊,這樣一方面會造成碎片(無法壓縮),另一方面他的每次收集都是對整個堆的收集,無法進行選擇,在暫停時間的控制上還是很弱。而增量方式,通過內存空間的分塊,恰恰可以解決上面問題。

Garbage Firest(G1)

  這部分的內容主要參考這裏,這篇文章算是對G1算法論文的解讀。我也沒加什麽東西了。

目標

  從設計目標看G1完全是為了大型應用而準備的。

支持很大的堆

高吞吐量

-- 支持多CPU和垃圾回收線程

-- 在主線程暫停的情況下,使用並行收集

-- 在主線程運行的情況下,使用並發收集

實時目標:可配置在N毫秒內最多只占用M毫秒的時間進行垃圾回收

  當然G1要達到實時性的要求,相對傳統的分代回收算法,在性能上會有一些損失。

算法詳解

技術分享圖片

圖1 G1收集器

  G1可謂博采眾家之長,力求到達一種完美。他吸取了增量收集優點,把整個堆劃分為一個一個等大小的區域(region)。內存的回收和劃分都以region為單位;同時,他也吸取了CMS的特點,把這個垃圾回收過程分為幾個階段,分散一個垃圾回收過程;而且,G1也認同分代垃圾回收的思想,認為不同對象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。為了達到對回收時間的可預計性,G1在掃描了region以後,對其中的活躍對象的大小進行排序,首先會收集那些活躍對象小的region,以便快速回收空間(要復制的活躍對象少了),因為活躍對象小,裏面可以認為多數都是垃圾,所以這種方式被稱為Garbage First(G1)的垃圾回收算法,即:垃圾優先的回收。

  回收步驟:

初始標記(Initial Marking)

  G1對於每個region都保存了兩個標識用的bitmap,一個為previous marking bitmap,一個為next marking bitmap,bitmap中包含了一個bit的地址信息來指向對象的起始點。

  開始Initial Marking之前,首先並發的清空next marking bitmap,然後停止所有應用線程,並掃描標識出每個region中root可直接訪問到的對象,將region中top的值放入next top at mark start(TAMS)中,之後恢復所有應用線程。

  觸發這個步驟執行的條件為:

 G1定義了一個JVM Heap大小的百分比的閥值,稱為h,另外還有一個H,H的值為(1-h)*Heap Size,目前這個h的值是固定的,後續G1也許會將其改為動態的,根據jvm的運行情況來動態的調整,在分代方式下,G1還定義了一個u以及soft limit,soft limit的值為H-u*Heap Size,當Heap中使用的內存超過了soft limit值時,就會在一次clean up執行完畢後在應用允許的GC暫停時間範圍內盡快的執行此步驟;

 在pure方式下,G1將marking與clean up組成一個環,以便clean up能充分的使用marking的信息,當clean up開始回收時,首先回收能夠帶來最多內存空間的regions,當經過多次的clean up,回收到沒多少空間的regions時,G1重新初始化一個新的marking與clean up構成的環。

並發標記(Concurrent Marking)

  按照之前Initial Marking掃描到的對象進行遍歷,以識別這些對象的下層對象的活躍狀態,對於在此期間應用線程並發修改的對象的以來關系則記錄到remembered set logs中,新創建的對象則放入比top值更高的地址區間中,這些新創建的對象默認狀態即為活躍的,同時修改top值。

最終標記暫停(Final Marking Pause)

  當應用線程的remembered set logs未滿時,是不會放入filled RS buffers中的,在這樣的情況下,這些remebered set logs中記錄的card的修改就會被更新了,因此需要這一步,這一步要做的就是把應用線程中存在的remembered set logs的內容進行處理,並相應的修改remembered sets,這一步需要暫停應用,並行的運行。

存活對象計算及清除(Live Data Counting and Cleanup)

  值得註意的是,在G1中,並不是說Final Marking Pause執行完了,就肯定執行Cleanup這步的,由於這步需要暫停應用,G1為了能夠達到準實時的要求,需要根據用戶指定的最大的GC造成的暫停時間來合理的規劃什麽時候執行Cleanup,另外還有幾種情況也是會觸發這個步驟的執行的:

G1采用的是復制方法來進行收集,必須保證每次的”to space”的空間都是夠的,因此G1采取的策略是當已經使用的內存空間達到了H時,就執行Cleanup這個步驟;

對於full-young和partially-young的分代模式的G1而言,則還有情況會觸發Cleanup的執行,full-young模式下,G1根據應用可接受的暫停時間、回收young regions需要消耗的時間來估算出一個yound regions的數量值,當JVM中分配對象的young regions的數量達到此值時,Cleanup就會執行;partially-young模式下,則會盡量頻繁的在應用可接受的暫停時間範圍內執行Cleanup,並最大限度的去執行non-young regions的Cleanup。

展望

  以後JVM的調優或許跟多需要針對G1算法進行調優了。

JVM調優工具

  主要有Jconsole,jProfile,VisualVM。

  Jconsole : jdk自帶,功能簡單,但是可以在系統有一定負荷的情況下使用。對垃圾回收算法有很詳細的跟蹤。詳細說明參考這裏

  JProfiler:商業軟件,需要付費。功能強大。詳細說明參考這裏

  VisualVM:JDK自帶,功能強大,與JProfiler類似。推薦。

如何調優

  觀察內存釋放情況、集合類檢查、對象樹

  上面這些調優工具都提供了強大的功能,但是總的來說一般分為以下幾類功能

堆信息查看

技術分享圖片

圖2 查看堆信息

  可查看堆空間大小分配(年輕代、年老代、持久代分配)。

  提供即時的垃圾回收功能。

  垃圾監控(長時間監控回收情況)。

技術分享圖片
圖3 堆內類和對象信息

  查看堆內類、對象信息查看:數量、類型等。

技術分享圖片

圖4 對象引用情況

  對象引用情況查看。
  有了堆信息查看方面的功能,我們一般可以順利解決以下問題:

  -- 年老代年輕代大小劃分是否合理

  -- 內存泄漏

  -- 垃圾回收算法設置是否合理

線程監控

技術分享圖片

圖5 線程監控信息

  線程信息監控:系統線程數量。

  線程狀態監控:各個線程都處在什麽樣的狀態下。

技術分享圖片

圖6 線程轉儲信息

Dump線程詳細信息:查看線程內部運行情況。

死鎖檢查 。

熱點分析

技術分享圖片

圖7 熱點分析

  CPU熱點:檢查系統哪些方法占用的大量CPU時間。

  內存熱點:檢查哪些對象在系統中數量最大(一定時間內存活對象和銷毀對象一起統計)。

  這兩個東西對於系統優化很有幫助。我們可以根據找到的熱點,有針對性的進行系統的瓶頸查找和進行系統優化,而不是漫無目的的進行所有代碼的優化。

快照

  快照是系統運行到某一時刻的一個定格。在我們進行調優的時候,不可能用眼睛去跟蹤所有系統變化,依賴快照功能,我們就可以進行系統兩個不同運行時刻,對象(或類、線程等)的不同,以便快速找到問題。

  舉例說,我要檢查系統進行垃圾回收以後,是否還有該收回的對象被遺漏下來的了。那麽,我可以在進行垃圾回收前後,分別進行一次堆情況的快照,然後對比兩次快照的對象情況。

內存泄漏檢查

  內存泄漏是比較常見的問題,而且解決方法也比較通用,這裏可以重點說一下,而線程、熱點方面的問題則是具體問題具體分析了。

  內存泄漏一般可以理解為系統資源(各方面的資源,堆、棧、線程等)在錯誤使用的情況下,導致使用完畢的資源無法回收(或沒有回收),從而導致新的資源分配請求無法完成,引起系統錯誤。

  內存泄漏對系統危害比較大,因為他可以直接導致系統的崩潰。

  需要區別一下,內存泄漏和系統超負荷兩者是有區別的,雖然可能導致的最終結果是一樣的。內存泄漏是用完的資源沒有回收引起錯誤,而系統超負荷則是系統確實沒有那麽多資源可以分配了(其他的資源都在使用)。

年老代堆空間被占滿

  異常: java.lang.OutOfMemoryError: Java heap space

  說明:

技術分享圖片

圖8 堆空間慢慢消耗盡

  這是最典型的內存泄漏方式,簡單說就是所有堆空間都被無法回收的垃圾對象占滿,虛擬機無法再在分配新空間。

  如上圖所示,這是非常典型的內存泄漏的垃圾回收情況圖。所有峰值部分都是一次垃圾回收點,所有谷底部分表示是一次垃圾回收後剩余的內存。連接所有谷底的點,可以發現一條由底到高的線,這說明,隨時間的推移,系統的堆空間被不斷占滿,最終會占滿整個堆空間。因此可以初步認為系統內部可能有內存泄漏。(上面的圖僅供示例,在實際情況下收集數據的時間需要更長,比如幾個小時或者幾天)

  解決:

  這種方式解決起來也比較容易,一般就是根據垃圾回收前後情況對比,同時根據對象引用情況(常見的集合對象引用)分析,基本都可以找到泄漏點。

持久代被占滿

  異常:java.lang.OutOfMemoryError: PermGen space

  說明:

  Perm空間被占滿。無法為新的class分配存儲空間而引發的異常。這個異常以前是沒有的,但是在Java反射大量使用的今天這個異常比較常見了。主要原因就是大量動態反射生成的類不斷被加載,最終導致Perm區被占滿。

  更可怕的是,不同的classLoader即便使用了相同的類,但是都會對其進行加載,相當於同一個東西,如果有N個classLoader那麽他將會被加載N次。因此,某些情況下,這個問題基本視為無解。當然,存在大量classLoader和大量反射類的情況其實也不多。

  解決:

  1. -XX:MaxPermSize=16m

  2. 換用JDK。比如JRocket。

堆棧溢出

  異常:java.lang.StackOverflowError

  說明:這個就不多說了,一般就是遞歸沒返回,或者循環調用造成

線程堆棧滿

  異常:Fatal: Stack size too small

  說明:java中一個線程的空間大小是有限制的。JDK5.0以後這個值是1M。與這個線程相關的數據將會保存在其中。但是當線程空間滿了以後,將會出現上面異常。

  解決:增加線程棧大小。-Xss2m。但這個配置無法解決根本問題,還要看代碼部分是否有造成泄漏的部分。

系統內存被占滿

  異常:java.lang.OutOfMemoryError: unable to create new native thread

  說明:

  這個異常是由於操作系統沒有足夠的資源來產生這個線程造成的。系統創建線程時,除了要在Java堆中分配內存外,操作系統本身也需要分配資源來創建線程。因此,當線程數量大到一定程度以後,堆中或許還有空間,但是操作系統分配不出資源來了,就出現這個異常了。

  分配給Java虛擬機的內存愈多,系統剩余的資源就越少,因此,當系統內存固定時,分配給Java虛擬機的內存越多,那麽,系統總共能夠產生的線程也就越少,兩者成反比的關系。同時,可以通過修改-Xss來減少分配給單個線程的空間,也可以增加系統總共內生產的線程數。

  解決:

  1. 重新設計系統減少線程數量。

  2. 線程數量不能減少的情況下,通過-Xss減小單個線程大小。以便能生產更多的線程。

垃圾回收的悖論

  所謂“成也蕭何敗蕭何”。Java的垃圾回收確實帶來了很多好處,為開發帶來了便利。但是在一些高性能、高並發的情況下,垃圾回收確成為了制約Java應用的瓶頸。目前JDK的垃圾回收算法,始終無法解決垃圾回收時的暫停問題,因為這個暫停嚴重影響了程序的相應時間,造成擁塞或堆積。這也是後續JDK增加G1算法的一個重要原因。

  當然,上面是從技術角度出發解決垃圾回收帶來的問題,但是從系統設計方面我們就需要問一下了:

1. 我們需要分配如此大的內存空間給應用嗎?

2. 我們是否能夠通過有效使用內存而不是通過擴大內存的方式來設計我們的系統呢?

我們的內存中都放了什麽

  內存中需要放什麽呢?個人認為,內存中需要放的是你的應用需要在不久的將來再次用到到的東西。想想看,如果你在將來不用這些東西,何必放內存呢?放文件、數據庫不是更好?這些東西一般包括:

1. 系統運行時業務相關的數據。比如web應用中的session、即時消息的session等。這些數據一般在一個用戶訪問周期或者一個使用過程中都需要存在。

2. 緩存。緩存就比較多了,你所要快速訪問的都可以放這裏面。其實上面的業務數據也可以理解為一種緩存。

3. 線程。

  因此,我們是不是可以這麽認為,如果我們不把業務數據和緩存放在JVM中,或者把他們獨立出來,那麽Java應用使用時所需的內存將會大大減少,同時垃圾回收時間也會相應減少。

  我認為這是可能的。

解決之道

數據庫、文件系統

  把所有數據都放入數據庫或者文件系統,這是一種最為簡單的方式。在這種方式下,Java應用的內存基本上等於處理一次峰值並發請求所需的內存。數據的獲取都在每次請求時從數據庫和文件系統中獲取。也可以理解為,一次業務訪問以後,所有對象都可以進行回收了。

  這是一種內存使用最有效的方式,但是從應用角度來說,這種方式很低效。

內存-硬盤映射

  上面的問題是因為我們使用了文件系統帶來了低效。但是如果我們不是讀寫硬盤,而是寫內存的話效率將會提高很多。

  數據庫和文件系統都是實實在在進行了持久化,但是當我們並不需要這樣持久化的時候,我們可以做一些變通——把內存當硬盤使。

  內存-硬盤映射很好很強大,既用了緩存又對Java應用的內存使用又沒有影響。Java應用還是Java應用,他只知道讀寫的還是文件,但是實際上是內存。

  這種方式兼得的Java應用與緩存兩方面的好處。memcached的廣泛使用也正是這一類的代表。

同一機器部署多個JVM

  這也是一種很好的方式,可以分為縱拆和橫拆。縱拆可以理解為把Java應用劃分為不同模塊,各個模塊使用一個獨立的Java進程。而橫拆則是同樣功能的應用部署多個JVM。

  通過部署多個JVM,可以把每個JVM的一個垃圾回收控制在可以忍受的範圍內即可。但是這相當於進行了分布式的處理,其額外帶來的復雜性也是需要評估的。另外,也有支持分布式的這種JVM可以考慮,不要要錢哦:)

程序控制的對象生命周期

  這種方式是理想當中的方式,目前的虛擬機還沒有,純屬假設。即:考慮由編程方式配置哪些對象在垃圾收集過程中可以直接跳過,減少垃圾回收線程遍歷標記的時間。

  這種方式相當於在編程的時候告訴虛擬機某些對象你可以在某個時間後再進行收集或者由代碼標識可以收集了(類似C、C++),在這之前你即便去遍歷他也是沒有效果的,他肯定是還在被引用的。

  這種方式如果JVM可以實現,個人認為將是一個飛躍,Java即有了垃圾回收的優勢,又有了C、C++對內存的可控性。

線程分配

  Java的阻塞式的線程模型基本上可以拋棄了,目前成熟的NIO框架也比較多了。阻塞式IO帶來的問題是線程數量的線性增長,而NIO則可以轉換成為常數線程。因此,對於服務端的應用而言,NIO還是唯一選擇。不過,JDK7中為我們帶來的AIO是否能讓人眼前一亮呢?我們拭目以待。

其他的JDK

  本文說的都是Sun的JDK,目前常見的JDK還有JRocket和IBM的JDK。其中JRocket在IO方面比Sun的高很多,不過Sun JDK6.0以後提高也很大。而且JRocket在垃圾回收方面,也具有優勢,其可設置垃圾回收的最大暫停時間也是很吸引人的。不過,Sun的G1實現以後,在這方面會有一個質的飛躍。

JVM調優(二)經驗參數設置