天天看點

JVM調優知識

   一、Java應用伺服器

Tomcat、Nginx、Resin、等多種應用伺服器,雖然JVM做為容器,提供的是一個Java Web的運作時環境,以支援Servlet/JSP等等這些内容的運作但是我們都很清楚,其本質上是還是一個Java應用程式。現在有哪些java應用伺服器呢?商業的有BEA Weblogic Server、IBM Websphere Application Server、Oracle Application Server、Sybase EAServer。免費開源的java應用伺服器有Tomcat、jboss、resin(百度、人人網、搜狗)、Geronimo等。

二、學習JVM調優及JVM區域

運維人員需要會一些JVM調優,為什麼運維要了解一些jvm調優的知識,為以後的的運維工作更輕松。學習JVM的調優,主要有兩個方面考慮:記憶體大小配置和垃圾回收算法選擇。當然,确切的說,這兩點并不互相獨立,記憶體的大小配置也會影響垃圾回收的執行效率。為什麼要學習JVM調優? 每次對于容器(這裡容器值得是java應用伺服器,如:resin、weblogic)的啟動運作,都是把這個Java程式跑起來,來實作Web容器的能力。做為一類“特殊”的Java應用程式,和任務其他的java應用一樣,需要使用到JVM,會有堆,會使用到垃圾回收,會涉及到不同的堆分區比例... 是以在對Web容器的調優中必不可少的是對于JVM的調優。

JVM區域總體分兩類,heap區和非heap區。 heap區又分為: Eden Space(伊甸園)、 Survivor Space(幸存者區)、 Old Gen(老年代)。非heap區又分: Code Cache(代碼緩存區)、Perm Gen(永久代)、Jvm Stack(Java虛拟機棧)、Local Method Statck(本地方法棧)。

Eden Space字面意思是伊甸園,對象被建立的時候首先放到這個區域,進行垃圾回收後,不能被回收的對象被放入到空的survivor區域。

Survivor Space幸存者區,用于儲存在eden space記憶體區域中經過垃圾回收後沒有被回收的對象。Survivor有兩個,分别為To Survivor、 From Survivor,這個兩個區域的空間大小是一樣的。執行垃圾回收的時候Eden區域不能被回收的對象被放入到空的survivor(也就是To Survivor,同時Eden區域的記憶體會在垃圾回收的過程中全部釋放),另一個survivor(即From Survivor)裡不能被回收的對象也會被放入這個survivor(即To Survivor),然後To Survivor 和 From Survivor的标記會互換,始終保證一個survivor是空的。 

Eden Space和Survivor Space都屬于新生代,新生代中執行的垃圾回收被稱之為Minor GC(因為是對新生代進行垃圾回收,是以又被稱為Young GC),每一次Young GC後留下來的對象age加1。

注:GC為Garbage Collection,垃圾回收。

Old Gen老年代,用于存放新生代中經過多次垃圾回收仍然存活的對象,也有可能是新生代配置設定不了記憶體的大對象會直接進入老年代。經過多次垃圾回收都沒有被回收的對象,這些對象的年代已經足夠old了,就會放入到老年代。

當老年代被放滿的之後,虛拟機會進行垃圾回收,稱之為Major GC。由于Major GC除并發GC外均需對整個堆進行掃描和回收,是以又稱為Full GC。

heap區即堆記憶體,整個堆大小=年輕代大小 + 老年代大小。堆記憶體預設為實體記憶體的1/64(<1GB);預設空餘堆記憶體小于40%時,JVM就會增大堆直到-Xmx的最大限制,可以通過MinHeapFreeRatio參數進行調整;預設空餘堆記憶體大于70%時,JVM會減少堆直到-Xms的最小限制,可以通過MaxHeapFreeRatio參數進行調整。

下面我們來認識下非堆記憶體(非heap區) 

Code Cache代碼緩存區,它主要用于存放JIT所編譯的代碼。CodeCache代碼緩沖區的大小在client模式下預設最大是32m,在server模式下預設是48m,這個值也是可以設定的,它所對應的JVM參數為ReservedCodeCacheSize 和 InitialCodeCacheSize,可以通過如下的方式來為Java程式設定。

-XX:ReservedCodeCacheSize=128m

CodeCache緩存區是可能被充滿的,當CodeCache滿時,背景會收到CodeCache is full的警告資訊,如下所示: 

“CompilerThread0” java.lang.OutOfMemoryError: requested 2854248 bytes for Chunk::new. Out of swap space?

注:JIT編譯器是在程式運作期間,将Java位元組碼編譯成平台相關的二進制代碼。正因為此編譯行為發生在程式運作期間,是以該編譯器被稱為Just-In-Time編譯器。

Perm Gen全稱是Permanent Generation space,是指記憶體的永久儲存區域,因而稱之為永久代。這個記憶體區域用于存放Class和Meta的資訊,Class在被 Load的時候被放入這個區域。因為Perm裡存儲的東西永遠不會被JVM垃圾回收的,是以如果你的應用程式LOAD很多CLASS的話,就很可能出現PermGen space錯誤。預設大小為實體記憶體的1/64。

 三、記憶體大小配置

    記憶體大小配置,最主要做的有:确定記憶體占用的總大小和确定記憶體中各個代的大小劃分。

    所謂記憶體大小的占用,是指應用程式啟動後穩定運作一小段時間時,觀察到的記憶體占用情況。以 HotSpot 虛拟機為例,Java 堆主要有三個空間:新生代、老年代和永久代。

    根據不同應用的特别,觀察應用對于記憶體的占用,如果有大量的臨時對象,不會重複使用,則可以調整 New Gen, 這樣這些臨時對象就在新生代建立完成,并在 Minor GC 産生時被回收,這樣較短生存活的對象不會晉升到老年代,進而可以避免垃圾堆集産生 Full GC。 理想狀态下,短期存活的對象都隻在新生代完成生命周期,被費時勁少的。Minor GC  回收完成, 而長期存活,将會多次使用的在多次回收之後晉升到老年代, 最終經過 Full GC 完成生命周期。這裡涉及到關于記憶體大小的調整參數有:

-Xms

-Xmx

    這兩個參數用于配置 heap 的起始大小和最大值。這裡需要經過觀察,找一個合适的值,設定太大會導緻記憶體浪費,同時也會導緻垃圾回收耗時太長。對于 Tomcat 來說,一般都會将初始值和最大值設定為相同值,這樣就避免在初始記憶體不足時觸發 Full GC 來進行擴充記憶體。

設定 heap 大小之後,要根據對象生命周期的特征,來調整新生代與老年代的大小比例。涉及到的參數有:

-XX:NewSize

-XX:NewRatio

-XX:MaxNewSize

-Xmn

    第一個是直接設定新生代初始大小,第二個是設定比例(Ratio)。太高或太低都會導緻 GC 不能高效的工作。畢竟 Minor GC 也是要耗時的。最後一個設定新生代的初始值和最大值相同,堆空間的變化不影響其值。

對于使用了大量第三方類庫的應用來說,會加載許多架構依賴的類,使用過程中可能會遇到因為Perm Gen 不足産生的 OOM,這種情況可以通過觀察穩定狀态下 Perm 區的占用,再通過參數設定。

-XX:PermSize

-XX:MaxPermSize

-XX:MaxMetaspaceSize

    一個會設定Perm區的初始大小,第二個用于設定Perm 區的最大值。在Java 8的時候, Perm 區被移除,改為Metaspace,不過如果遇到類似的OOM,依然可以調整其大小。

此外,對于使用大量線程的應用,也可以配置 -Xss,主要用于設定單個線程的stack 大小。注意,是單個的大小,是以設定值越大,會占用越大,可用的線程數也就越少。

這裡的配置一般對于-X開始的可以直接在後面用數字加機關,而-XX的則需要等号後數字再加機關,例如:

java -Xms100m -Xmx200m -XX:PermSize=300m

    這裡數字後的單可以是m,g,k代表計算機中的不同機關。

那我們前面一直在說根據不同的應用,觀察分析設定堆的大小,堆的各個代的大小,那具體觀察什麼呢?我們一般在JVM的配置中增加一些列印 GC 日志的選項,配置方式和上面的類似,這樣在 GC 産生時,會列印出各個代占用的大小,具體觸發時間等。推薦的配置有以下幾個:

-XX:+PrintGCTimeStamps

-XX:+PrintGCDetails

    -Xloggc:<檔案名>

-XX:PrintGCDateStamps  

    第一個和第四個選項可以任選一個,第一個列印自JVM啟動以來的時間,一般也稱為uptime, 第四個列印的是系統目前日期和時間。

四、垃圾回收算法

     垃圾回收的瓶頸

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

  分代垃圾回收方式确實也考慮了實時性要求而提供了并發回收器,支援最大暫停時間的設定,但是受限于分代垃圾回收的記憶體劃分模型,其效果也不是很理想。

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

         增量收集的演進

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

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

        Garbage Firest(G1)

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

         目标

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

支援很大的堆

         高吞吐量

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

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

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

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

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

算法詳解

JVM調優知識

圖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算法進行調優了。

五、常見配置彙總

堆設定

-Xms:初始堆大小

-Xmx:最大堆大小

-XX:NewSize=n:設定年輕代大小

-XX:NewRatio=n:設定年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,表示Eden:Survivor=3:2,一個Survivor區占整個年輕代的1/5。

-XX:MaxPermSize=n:設定持久代大小

收集器設定

-XX:+UseSerialGC:設定串行收集器

-XX:+UseParallelGC:設定并行收集器

-XX:+UseParalledlOldGC:設定并行年老代收集器

-XX:+UseConcMarkSweepGC:設定并發收集器

垃圾回收統計資訊

-XX:+PrintGC

-Xloggc:filename

并行收集器設定

-XX:ParallelGCThreads=n:設定并行收集器收集時使用的CPU數。并行收集線程數。

-XX:MaxGCPauseMillis=n:設定并行收集最大暫停時間

-XX:GCTimeRatio=n:設定垃圾回收時間占程式運作時間的百分比。公式為1/(1+N)

并發收集器設定

-XX:+CMSIncrementalMode:設定為增量模式。适用于單CPU情況。

-XX:+ParallelGCThreads=n:設定并發收集器年輕代收集方式為并行收集時,使用的CPU數。并行收集線程數。

六、JVM調優案例

     堆大小設定

     年輕代的設定很關鍵

    JVM中最大堆大小有三方面限制:相關作業系統的資料模型(32-bit 還是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。整個堆大小=年輕代大小+年老代大小+持久代大小。持久代一般固定大小為64m,是以增大年輕代後,将會減小年老代大小。此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8。

-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區進行多次複制,這樣可以增加對象在年輕代的存活時間,增加在年輕代被回收的機率。

    回收器選擇

    JVM給了三種選擇:串行收集器、并行收集器、并發收集器,但是串行收集器隻适用于小資料量的情況,是以這裡的選擇主要針對并行收集器和并發收集器。預設情況下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在啟動的時候加入相應參數。JDK5.0以後,JVM會根據目前系統配置進行判斷。

    吞吐量優先的并行收集器

    如上文所述,并行收集器主要以到達一定的吞吐量為目标,适用于科學計算和背景處理等。

    典型配置:

    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區比例,以達到目标系統規定的最低響應時間或者收集頻率等,此值建議使用并行收集器時,一直打開。 

    響應時間優先的并發收集器

    如上文所述,并發收集器主要是保證系統的響應時間,減少垃圾收集典型配置:

    java  -Xmx3550m  -Xms時的停頓時間。适用于應用伺服器、電信領域等。

    3550  -Xmn2g  -Xss128k  -XX:ParallelGCThreads=20  -XX:+UseConcMarkSweepGC  -XX:+UseParNewGC

-XX:+UseConcMarkSweepGC:設定年老代為并發收集。測試中配置這個以後,-XX:NewRatio=4的配置失效了,原因不明。是以,此時年輕代大小最好用-Xmn設定。

-XX:+UseParNewGC:設定年輕代為并行收集。可與CMS收集同時使用。JDK5.0以上,JVM會根據系統配置自行設定,是以無需再設定此值。     

    java  -Xmx3550m  -Xms3550  -Xmn2g  -Xss128k  -XX:+UseConcMarkSweepGC  -XX:CMSFullGCsBeforeCompaction=5  -XX:+UseCMSCompactAtFullCollection 

-XX:CMSFullGCsBeforeCompaction:由于并發收集器不對記憶體空間進行壓縮、整理,是以運作一段時間後會産生“碎片”,使得運作效率降低。此值設定運作多少次GC以後對記憶體空間進行壓縮、整理。

-XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮。可能會影響性能,但是可以消除碎片。

七、調優的方法

    JVM調優工具

    Jconsole、JProfile、VisuaIVM

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

    JProfile:商業軟體,需要付費。功能強大。

    VisuaIVM:jdk自帶,功能強大,與JProfiler類似。推薦

    如何調優

    觀察記憶體釋放情況、集合類檢查、對象樹

堆資訊檢視

JVM調優知識

圖2 檢視堆資訊可檢視堆空間大小配置設定(年輕代、年老代、持久代配置設定)。

JVM調優工具

https://my.oschina.net/feichexia/blog/196575

http://blog.csdn.net/zhoudaxia/article/details/26956831

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

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

JVM調優知識

圖3 堆内類和對象資訊

  檢視堆内類、對象資訊檢視:數量、類型等。

JVM調優知識

圖4 對象引用情況

    對象引用情況檢視。 

  有了堆資訊檢視方面的功能,我們一般可以順利解決以下問題:

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

-- 記憶體洩漏

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

 線程監控

JVM調優知識

圖5 線程監控資訊

  線程資訊監控:系統線程數量。

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

JVM調優知識

圖6 線程轉儲資訊

Dump線程詳細資訊:檢視線程内部運作情況。

死鎖檢查 。

 熱點分析

JVM調優知識

圖7 熱點分析

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

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

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

    快照

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

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

         記憶體洩漏檢

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

  記憶體洩漏一般可以了解為系統資源(各方面的資源,堆、棧、線程等)在錯誤使用的情況下,導緻使用完畢的資源無法回收(或沒有回收),進而導緻新的資源配置設定請求無法完成,引起系統錯誤。

  記憶體洩漏對系統危害比較大,因為他可以直接導緻系統的崩潰。

  需要差別一下,記憶體洩漏和系統超負荷兩者是有差別的,雖然可能導緻的最終結果是一樣的。記憶體洩漏是用完的資源沒有回收引起錯誤,而系統超負荷則是系統确實沒有那麼多資源可以配置設定了(其他的資源都在使用)。

    年老代堆空間被占滿

  異常: java.lang.OutOfMemoryError: Javaheap space

  說明:

JVM調優知識

圖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減小單個線程大小。以便能生産更多的線程。

八、JVM調優經驗總結

年輕代大小選擇

響應時間優先的應用:盡可能設大,直到接近系統的最低響應時間限制(根據實際情況選擇)。在此種情況下,年輕代收集發生的頻率也是最小的。同時,減少到達年老代的對象。

吞吐量優先的應用:盡可能的設定大,可能到達Gbit的程度。因為對響應時間沒有要求,垃圾收集可以并行進行,一般适合8CPU以上的應用。

年老代大小選擇

響應時間優先的應用:年老代使用并發收集器,是以其大小需要小心設定,一般要考慮并發會話率和會話持續時間等一些參數。如果堆設定小了,可以會造成記憶體碎片、高回收頻率以及應用暫停而使用傳統的标記清除方式;如果堆大了,則需要較長的收集時間。最優化的方案,一般需要參考以下資料獲得:

1.并發垃圾收集資訊

2.持久代并發收集次數

3.傳統GC資訊

4.花在年輕代和年老代回收上的時間比例減少年輕代和年老代花費的時間,一般會提高應用的效率

吞吐量優先的應用:一般吞吐量優先的應用都有一個很大的年輕代和一個較小的年老代。原因是,這樣可以盡可能回收掉大部分短期對象,減少中期的對象,而年老代盡存放長期存活對象。

較小堆引起的碎片問題

因為年老代的并發收集器使用标記、清除算法,是以不會對堆進行壓縮。當收集器回收時,他會把相鄰的空間進行合并,這樣可以配置設定給較大的對象。但是,當堆空間較小時,運作一段時間以後,就會出現“碎片”,如果并發收集器找不到足夠的空間,那麼并發收集器将會停止,然後使用傳統的标記、清除方式進行回收。如果出現“碎片”,可能需要進行如下配置:

-XX:+UseCMSCompactAtFullCollection:使用并發收集器時,開啟對年老代的壓縮。