【J2EE性能分析篇】JVM參數對J2EE性能優化的影響

一切J2EE應用都是基於JVM的,那麼對於JVM的設置和監控,成爲J2EE應用程序性能分析和性能優化的必然手段。今天Sincky和大家交流該話題。這裏以Tomcat環境爲例,其它WEB服務器如Jboss、Weblogic、Websphere完全一致。

【認識JVM】

    首先我們來看一張圖,這是目前JDK1.6版本自帶的JVM性能監控工具VisualVM的一個插件VisualGC的顯示情況。讓我們先來了解JVM的內存堆Heap管理模式,要調整JVM,自然要知道它的內部結構和運作,此乃“知己知彼,百戰不殆”!

    JVM的Heap包括三部分,分別是Permanent Generation(簡稱PermGen)、New Generation和Tenured Generation(又叫Old )。

    PermGen是JVM自用的區域,是內存的永久保存區,用於存放反射代理和Class,Class在被裝載時就會被放到PermGen中;所以如果WEB服務器裏應用的Class相當多時,就可以考慮將這一塊區域放大一些。

    New和Old是Java應用的Heap區,用來存放類的實例Instance的;其中New Generation的目標就是儘可能快速的收集掉那些生命週期短的對象;New Generation又分爲Eden Space,From Space和To Space三塊,Eden Space用於存放新創建的對象,From區和To區都是救助空間Survivor Space(圖中的S0和S1);當Eden區滿時,JVM執行垃圾回收GC(Garbage Collection),垃圾收集器暫停應用程序,並會將Eden Space還存活的對象複製到當前的From救助空間,一旦當前的From救助空間充滿,此區的存活對象將被複制到另外一個To區,當To區也滿了的時候,從From區複製過來並且依然存活的對象複製到Old區,從而From和To救助空間互換角色,維持活動的對象將在救助空間不斷複製,直到最終轉入Old域。需要注意,Survivor的兩個區是對稱的,沒先後關係,所以同一個區中可能同時存在從Eden複製過來對象,和從前一個Survivor複製過來的對象,而且,Survivor區總有一個是空的。同時,根據程序需要,Survivor區是可以配置爲多個的(多於兩個),這樣可以增加對象在年輕代中的存在時間,減少被放到年老代的可能。每一次垃圾回收後,Eden Space都會被清空。

    Old區用於存放長壽的對象,在New區中經歷了N次垃圾回收後仍然存活的對象,就會被放到Old區中;如那些與業務信息相關的對象,包括Http請求中的Session對象、線程、Socket連接,這類對象跟業務直接掛鉤,因此生命週期比較長。當任何一個空間不夠用時,都會促使JVM執行垃圾回收,而垃圾回收也需要消耗一定的時間,從而造成應用程序卡住;因此,合理的設置各個區的大小,可以進行快速GC即Mimor GC,避免頻繁的Full GC。

    所謂Minor GC,一般情況下,當新對象生成,並且在Eden申請空間失敗時,就會觸發Minor GC,對Eden區域進行GC,清除非存活對象,並且把尚且存活的對象移動到Survivor區,然後整理Survivor的兩個區。這種方式的GC是對New區的Eden區進行,不會影響到Old區。因爲大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的Minor GC會頻繁進行。因而,一般在這裏需要使用速度快、效率高的算法,使Eden去能儘快空閒出來。

    而Full GC要對整個Heap區進行回收,包括New、Old和PermGen,所以比Minor GC要慢,因此應該儘可能減少Full GC的次數。在對JVM性能調優的過程中,很大一部分工作就是對於Full GC的調節。有如下原因可能導致Full GC:
·Tenured區被寫滿
·PermGen區被寫滿
·System.gc()被顯示調用
·上一次GC之後Heap的各域分配策略動態變化

【JVM參數實例一】JVM PermGen溢出:

java.lang.OutOfMemoryError  PermGen space

    由於GC不會在主程序運行期對PermGen進行清理,所以如果你的應用中有很多CLASS的話,就很可能出現PermGen space錯誤,這種錯誤常見在WEB服務器對JSP進行預編譯的時候發生。如果你的WEB APP下都用了大量的第三方JAR包, 其大小超過了JVM默認的大小(4M)那麼就會產生此錯誤信息了。

    解決方法: 手動設置MaxPermSize大小。修改TOMCAT_HOME/bin/catalina.bat,在echo Using CATALINA_base:%CATALINA_base%上面加入以下行:

JAVA_OPTS=%JAVA_OPTS% -server -XX:PermSize=64M -XX:MaxPermSize=128m

建議:將相同的第三方jar文件移置到tomcat/shared/lib目錄下,這樣可以達到減少jar 文檔重複佔用內存的目的。

【JVM參數實例二】JVM Heap溢出:

java.lang.OutOfMemoryError: Java heap space

     JVM Heap堆的設置是指java程序運行過程中JVM可以調配使用的內存空間。JVM在啓動的時候會自動設置Heap size的值,其初始空間(即-Xms)是物理內存的1/64,最大空間(-Xmx)是物理內存的1/4。可以利用JVM提供的-Xmn -Xms -Xmx等選項可進行設置。提示:在JVM中如果98%的時間是用於GC且可用的Heap大小不足2%的時候將拋出此異常信息。Heap最大不要超過可用物理內存的80%,一般的要將-Xms和-Xmx選項設置爲相同,而-Xmn爲1/4的-Xmx值。 
    解決方法:手動設置Heap size,例如:

JAVA_OPTS=%JAVA_OPTS% -server -Xms800m -Xmx800m

    但是有的時候可能這樣的設置還會不生效,例如當JVM加載大量Class類時,永久域中的對象急劇增加,從而

使JVM不斷調整永久域大小。爲了避免自動調整,可以使用前面的參數結合設置,如:

JAVA_OPTS=%JAVA_OPTS% -server -Xms800m -Xmx800m -XX:PermSize=64m -XX:MaxPermSize=128m

    基於Sun 的Java的垃圾回收機制,允許分隔內存池以包含不同時效的對象,垃圾回收循環根據時效收集與其他對象彼此獨立的對象。使用其他參數,您可以單獨設置內存池的大小。爲了實現更好的性能,可以對包含短期存活對象的池的大小進行設置,以使該池中的對象的存活時間不會超過一個垃圾回收循環。新生成的池的大小由 NewSize和MaxNewSize參數確定。如:

JAVA_OPTS=%JAVA_OPTS% -server -Xms800m -Xmx800m  -XX:PermSize=64m -XX:MaxPermSize=128m -XX:NewSize=8m -XX:MaxNewSize=16m

【JVM參數實例三】垃圾回收時promotion failed:

    這個問題一般由兩種原因引起,第一個原因是Survivor空間不夠,裏面的對象還不應該被移動到Old區,但New區又有很多對象需要放入Survivor;第二個原因是Old區沒有足夠的空間接納來自New區的對象;這兩種情況都會轉向Full GC,造成系統卡住時間較長。第一個原因可以通過去掉救助空間解決,設置-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0即可,第二個原因可以設置CMSInitiatingOccupancyFraction爲某個值(假設70),這樣Old區的70%時就開始執行CMS,Old區有足夠的空間接納來自New的對象。但是不管怎樣,PermGen還是會逐漸變滿,所以隔三差五重起java服務器是必要的。需要注意的是,Old區用的是併發回收,可以設置New區小一點,Old區大些,可以減少系統的卡住。

    解決方法:

$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xms6000M -Xmx6000M -Xmn500M -XX:PermSize=500M -XX:MaxPermSize=500M -XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0 -Xnoclassgc -XX:+DisableExplicitGC -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:-CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=90 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:log/gc.log"

-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0 就是去掉了救助空間
-Xnoclassgc 禁用類垃圾回收,性能會高一點
-XX:+DisableExplicitGC 禁止System.gc(),免得程序員誤調用gc方法影響性能
-XX:+UseParNewGC 對New採用多線程並行回收,這樣收得快
帶CMS參數的都是和併發回收相關的:
CMSInitiatingOccupancyFraction,這個參數設置有很大技巧,基本上滿足(Xmx-Xmn)*(100-CMSInitiatingOccupancyFraction)/100>=Xmn 就不會出現promotion failed。在這裏Xmx是6000,Xmn是500,那麼Xmx-Xmn是5500m,也就是Old有5500m,CMSInitiatingOccupancyFraction=90說明Old到90%滿的時候開始執行對Old區的併發垃圾回收(CMS),這時還剩10%的空間是5500*10%=550m,所以即使Xmn(也就是New共500m)裏所有對象都搬到Old裏,550兆的空間也足夠了,所以只要滿足上面的公式,就不會出現垃圾回收時的promotion failed。
SoftRefLRUPolicyMSPerMB這個參數是softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap。

    但是,這種設置方法因爲沒有用到Survivor,所以Old區容易滿,CMS執行會比較頻繁。建議還是用Survivor並把加大,這樣也不會有promotion failed錯誤。配置上32位和64位不一樣,64位系統只要配置MaxTenuringThreshold參數,CMS還是有暫停。爲了解決暫停問題和promotion failed問題,最後我設置-XX:SurvivorRatio=1 ,並把MaxTenuringThreshold去掉,這樣即沒有暫停又不會有promotoin failed,而且更重要的是,Old區和PermGen上升非常慢(因爲好多對象到不了年老代就被回收了),所以CMS執行頻率非常低,好幾個小時才執行一次,這樣,服務器都不用重啓了。下面是64位的配置,系統8G內存。

-Xmx4000M -Xms4000M -Xmn600M -XX:PermSize=500M -XX:MaxPermSize=500M -Xss256K -XX:+DisableExplicitGC -XX:SurvivorRatio=1 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:LargePageSizeInBytes=128M -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:log/gc.log

【JVM啓動參數介紹】

-Xmn  -Eden Generation的Heap大小,一般設置爲Xmx的1/3或1/4

-Xmx  -設置JVM Heap大小最大值,這裏的heap = New Generation + Old Generation,但不包括PermGen

-Xms  -設置JVM Heap大小初始值

-XX:NewRatio -New/Old的大小比率

-XX:NewSize -New Generation Heap的大小

-XX:MaxNewSize -可以通過NewRatio和-Xmx計算得到

-XX:SurvivorRatio -Eden/Survivor Space大小比率

-XX:PermSize -PermGen的初始值

-XX:MaxPermSize -PermGen最大值

-Xss: -設置每個線程的Stack大小

-XX:+UseParNewGC  -表示多CPU下縮短Minor GC的時間

-XX:+UseParallelGC  -設置後可以使用並行清除收集器【多CPU】

-XX:+ParallelGCThreads  -可用來增加並行度【多CPU】

-XX:+AggressiveOpts  -是否激活最近的試驗性性能調整

-XX:-Xnoclassgc  -是否允許類垃圾收集,默認設置是允許類 GC

-XX:+UseLargePages  -是否支持大頁面堆

-XX:+UseFastAccessorMethods  -在指定了這個參數後,JDK會將所有的get/set方法都轉爲本地代碼

-XX:+UseConcMarkSweepGC  -縮短major收集的時間,此選項在Heap比較大而且Full GC時間較長的情況下使用更合適

    另外,JVM的一些參數可以輸出有效的日誌文件:

-verbose:gc  -輸出一些gc信息

-XX:+PrintGCDetails  -輸出gc詳細信息

-XX:+PrintGCTimeStamps  -包含時間戳信息

-XX:+PrintHeapAtGC  -包括gc前後Heap狀況

-XX:+PrintTenuringDistribution  -輸出對象存活時間和Tenured Generation的其他信息

-XX:+PrintHeapUsageOverTime  -以時間戳輸出heap利用率和容量信息

-Xloggc:filename  -輸出gc信息到日誌文件
    例如:

set JAVA_OPTS=%JAVA_OPTS% -verbose:gc -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC

     如果輸出的日誌文件非常大,可以後續採用其它工具分析;或者乾脆使用Jstat或Jconsole這些成熟的工具直接監控JVM的GC問題。當然,前邊圖示的VisualVM更是一款漂亮的、即時監控JVM GC的工具。

    下面的命令把整個堆設置成128m,New/Old比率設置成3,即New與Old比例爲1:3,New Space爲Heap的1/4即32M, 缺省值爲NewSize=2m MaxNewSize=32m SurvivorRatio=2

java –Xms128m –Xmx128m –XX:NewRatio=3

    但是,如果JVM的堆Heap大小大於1GB,則應該使用值:-XX:newSize=640m -XX:MaxNewSize=640m -XX:SurvivorRatio=16,或者將堆的總大小的50%到60%分配給新生成的池。

【JVM性能瓶頸】

    在JAVA應用程序中,雖然不太容易出現內存泄漏的問題,因爲JVM會不定期的進行垃圾回收。但是因爲程序的不合理寫法,也會導致一些數據不能被收集。典型的狀況是在HashMap中放置大量不用的數據,而沒有及時的清理。在web應用中,很多人喜歡在Session放放置狀態數據,而沒有清理,也是內存泄漏的一個原因。在Session中存放數據還好,因爲Session終究會有過期時間,但是如果在Class的Static變量中放置數據,那就怎麼樣也沒辦法了。診斷應用中是否存在內存泄漏也有一些方法,通過分析JVM GC Log就是一個直觀的方式。通過分析GC After Heap的變化趨勢,如果GC After Heap穩步上升,立刻進行Full GC後,仍然不能降下來,通常就意味着存在內存泄漏了。當然也有情況是,的確有一些數據是應用程序的需求,我們要確認除了這些數據,是否還存在一些異常數據一直佔據內存。可以通過JProbe的Memory Views來觀察Class的對象數,在一段請求過後,如果還存在一些Class的Instance數目相當多,就可以判斷這個Class可能會是問題的根源。

原文:

http://sinckyzhang.blog.sohu.com/149067215.html

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