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-uHeap 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

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

-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:使用併發收集器時,開啓對年老代的壓縮。

-XX:CMSFullGCsBeforeCompaction=0:上面配置開啓的情況下,這裏設置多少次Full GC後,對年老代進行壓縮。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章