JVM內存調優

調優設置


  1. 堆大小設置
    JVM 中最大堆大小有三方面限制:相關操做系統的數據模型(32-bt仍是64-bit)限制;系統的可用虛擬內存限制;系統的可用物理內存限制。32位系統下,通常限制在1.5G~2G;64爲操做系統對內存無限制。我在Windows Server 2003 系統,3.5G物理內存,JDK5.0下測試,最大可設置爲1478m。
    典型設置:
    • java -Xmx3550m -Xms3550m -Xmn2g-Xss128k
      -
      Xmx3550m:設置JVM最大可用堆內存爲3550M。
      -Xms3550m:設置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/6
      -XX:MaxPermSize=16m:設置持久代大小爲16m。
      -XX:MaxTenuringThreshold=0:設置垃圾最大年齡。若是設置爲0的話,則年輕代對象不通過Survivor區,直接進入年老代。對於年老代比較多的應用,能夠提升效率。若是將此值設置爲一個較大值,則年輕代對象會在Survivor區進行屢次複製,這樣能夠增長對象在年輕代的存活時間,增長在年輕代被回收的機率。
  2. 回收器選擇
    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:打開對年老代的壓縮。可能會影響性能,可是能夠消除碎片。
  3. 輔助信息
    JVM提供了大量命令行參數,打印信息,供調試使用。主要有如下一些:
    • -XX:+PrintGC
      輸出形式[GC 118250K->113543K(130112K), 0.0094143 secs]

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

    • -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]java

    • -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:與上面幾個配合使用,把相關日誌信息記錄到文件以便分析。
  4. 常見配置彙總
    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暫停所帶來的請求堆積和請求失敗是沒法接受的。這類應用可能要求請求的返回時間在幾百甚至幾十毫秒之內,若是分代垃圾回收方式要達到這個指標,只能把最大堆的設置限制在一個相對較小範圍內,可是這樣有限制了應用自己的處理能力,一樣也是不可接受的。mysql

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

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

  

增量收集的演進

 

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

  固然,傳統分代收集方式也提供了併發收集,可是他有一個很致命的地方,就是把整個堆作爲一個內存塊,這樣一方面會形成碎片(沒法壓縮),另外一方面他的每次收集都是對整個堆的收集,沒法進行選擇,在暫停時間的控制上仍是很弱。而增量方式,經過內存空間的分塊,偏偏能夠解決上面問題。數據庫

 

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實現之後,在這方面會有一個質的飛躍。

 

 

參考資料

 

 

  能整理出上面一些東西,也是由於站在巨人的肩上。下面是一些參考資料,供你們學習,你們有更好的,能夠繼續完善:)

· Java 理論與實踐: 垃圾收集簡史

· Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning

· Improving Java Application Performance and Scalability by Reducing Garbage Collection Times and Sizing Memory Using JDK 1.4.1

· Hotspot memory management whitepaper 

· Java Tuning White Paper 

· Diagnosing a Garbage Collection problem

· Java HotSpot VM Options

· A Collection of JVM Options

· Garbage-First Garbage Collection

· Frequently Asked Questions about Garbage Collection in the HotspotTM JavaTM Virtual Machine

· JProfiler試用手記

· Java6 JVM參數選項大全

· 《深刻Java虛擬機》。雖然過去了不少年,但這本書依舊是經典。

 

本文轉自:http://pengjiaheng.iteye.com/blog/538582

相關文章
相關標籤/搜索