Tomcat/JVM常見問題排除及性能優化

        最近一個SSH2項目升級了框架,部署後發現執行一段時間就會沒法訪問(Tomcat及其下其它Web能夠正常訪問)。html

        MyEclipse中進行「壓力測試」時報錯:前端

Exception in thread "com.mchange.v2.async.ThreadPoolAsynchronousRunner$PoolThread-#0" 
java.lang.OutOfMemoryError: PermGen space

        以後的現象跟測試機上部署的同樣,初步判斷就是這個緣由。結合這個Exception,究其根本的緣由,多是框架第三方Jar包,class文件佔有量變大;另外一方面,有些模塊請求歷史數據時隨着時間累積會返回較多的JSON數據,存放在Action的成員String變量中,經過Struts框架返回給前端,而前面說的這些,正好是存放在永久代(PermGen)裏的。還有一個是測試部署的時候出現:java

Exception loading sessions from persistent storage
java.io.WriteAbortedException: writing aborted; java.io.NotSerializableException: ...

        當時的解決方法是相應的Bean繼承Serializable接口,同時把work/Catalina/localhost/的項目文件夾給刪了。總所周知,這個是jsp編譯後的文件存放的目錄,而PermGen OutOfMemoryError也容易發生在web服務器對JSP進行pre compile的時候。綜上所述,那麼就先看看PermGen的分配使用狀況吧。linux


jstat內存監控(http://docs.oracle.com/javase/1.5.0/docs/tooldocs/share/jstat.html):web

        例如查看PermGen:jstat -gcpermcapacity pid數據庫

        Windows上查看進程:tasklist | findstr javaw.exeexpress


調用jstat出現pid not found:tomcat

        在服務器上對部署的項目查看PermGen時,jstat出現pid not found的問題。jstat的基本原理是會生成一個hsperfdata_username的目錄(裏面存有pid文件,記錄進程的信息),默認是在java.io.tmpdir目錄下(linux上默認是/tmp下),可是我執行jstat的時候這個目錄下沒有pid文件。而網上資料說jdk1.6.0.23/24版本兼容性問題,而我正好使用了0.23,踩坑了。服務器

        While it's true that 6u23/24 introduce this issue, it's not a bug in jps. Rather a change in behavior of the VM itself. On GNU/Linux Jps and the likes seem to only look at /tmp but not necessarily your CATALINA_TMPDIR. If set or not, try to export CATALINA_TMPDIR=/tmp which translates to "-Djava.io.tmpdir=/tmp" and after restarting the Tomcat process you should see Tomcat's data as "/tmp/hsperfdata_/" and Jps will most likely work again as well.session

        解決辦法是修改VM的配置文件,在tomcat/bin/catalina.sh中,找到CATALINA_TMPDIR,改成CATALINA_TMPDIR=/tmp,在重啓tomcat就能夠了。


        繼續在本地進行「壓力測試」,結果出現問題時,PermGen的PGC/PC已被消耗光,YGC/FGC次數不斷攀升,同時FGCT/GCT也持續增長,很明顯,JVM嘗試不斷進行GC試圖釋放PermGen,可是彷佛力不從心,致使程序一直被阻塞。那麼,改PermGen參數咯。


更改Tomcat的JVM PermGen參數:

        由於是本地MyEclipse以debug方式啓動tomcat的,在按照網上所說的方法在%CATALINA_HOME%/bin/catalina.bat中Execute The Requested Command後增長set JAVA_OPTS=%JAVA_OPTS%-server -XX:PermSize=128m -XX:MaxPermSize=256m後,debug啓動無效,其實這個時候tomcat是被調用tomcat7.exe啓動,因此沒法使得catalina.bat中的參數生效。

        正確的方式是在MyEclipse -> Preferences -> Servers -> Tomcat -> Tomcat x.x -> JDK的Optional Java VM arguments中增長一下參數,再重啓便可。

-XX:PermSize=128M

-XX:MaxPermSize=256M

        原本PermSize設置爲64M,Web項目啓動執行過程當中,若是不夠用時它會按照某種方式增量分配,好比FGC一下,PGC/PC增到140M,接下來還會適時地FGC,又減到135M、130M之類。這樣多累啊,不如初始分配時多一點,分配128M咯,而後再測試,執行時觀察過程當中並無發生FGC。

        就這樣?其實我也想從代碼上進行優化,以提升內存利用率,排除可能存在的內存泄露之類的問題。可是老代碼太多(問題是升級過程當中,大部分代碼並未動過,只是新增了部分小模塊),很懷疑是SSH框架(Spring3.2.一、Struts2.3.四、Hibernate3.3.2)的問題。這方面,你們有經驗的請多指教。日常關注業務,都沒時間去挖掘技術細節了。。。


jstack(Java Stack Trace)查看Java堆棧信息:

        某天,也就是今天,又出現項目Hung現象,看來問題尚未完全排除,痛定思痛,因而乎用jstack查看了堆棧信息:

        使用方法:jstack pid

"http-bio-8080-exec-24" daemon prio=10 tid=0x00007ff028005000 nid=0x99c in Object.wait() [0x00007ff0f9c56000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	at com.mchange.v2.resourcepool.BasicResourcePool.awaitAvailable(BasicResourcePool.java:1315)
	at com.mchange.v2.resourcepool.BasicResourcePool.prelimCheckoutResource(BasicResourcePool.java:557)
	- locked <0x000000041752e940> (a com.mchange.v2.resourcepool.BasicResourcePool)
	at com.mchange.v2.resourcepool.BasicResourcePool.checkoutResource(BasicResourcePool.java:477)
	at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutPooledConnection(C3P0PooledConnectionPool.java:525)
	at com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource.getConnection(AbstractPoolBackedDataSource.java:128)

        百度了下Object.wait()的大體含義,結合C3p0鏈接池的錯誤信息,猜想多是數據庫鏈接池阻塞致使了項目沒法響應。回顧了下,本身確實調整過com.mchange.v2.c3p0.ComboPooledDataSource的配置。因而調整了相應參數再部署,初步「壓力」測試下來一切正常。若是還有問題,那就繼續寫博客咯。


c3p0鏈接池沒法釋放:

        果真,意料之中的事情仍是發生了,依舊出現了上述問題,請求無響應。直接用jstack查看日誌,跟上一段的相似:

"http-bio-8080-exec-13" daemon prio=10 tid=0x00007f4d4c1a2800 nid=0x5cd7 in Object.wait() [0x00007f4e25ee2000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	at com.mchange.v2.resourcepool.BasicResourcePool.awaitAvailable(BasicResourcePool.java:1315)
	at com.mchange.v2.resourcepool.BasicResourcePool.prelimCheckoutResource(BasicResourcePool.java:557)
	- locked <0x00000004178cb420> (a com.mchange.v2.resourcepool.BasicResourcePool)
	at com.mchange.v2.resourcepool.BasicResourcePool.checkoutResource(BasicResourcePool.java:477)
	at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutPooledConnection(C3P0PooledConnectionPool.java:525)
	at com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource.getConnection(AbstractPoolBackedDataSource.java:128)

        可能鏈接池又爆了……寫了端代碼實時查看鏈接池使用的狀況:

        try {
            DataSource ds = dataSource;
            if (ds instanceof PooledDataSource) {
                PooledDataSource pds = (PooledDataSource) ds;
                debugMap.put("NumBusyConnections", pds.getNumBusyConnections());
                debugMap.put("NumBusyConnectionsAllUsers", pds.getNumBusyConnectionsAllUsers());
                debugMap.put("NumBusyConnectionsDefaultUser", pds.getNumBusyConnectionsDefaultUser());
                debugMap.put("NumConnections", pds.getNumConnections());
                debugMap.put("NumConnectionsAllUsers", pds.getNumConnectionsAllUsers());
                debugMap.put("NumConnectionsDefaultUser", pds.getNumConnectionsDefaultUser());
                debugMap.put("NumIdleConnections", pds.getNumIdleConnections());
                debugMap.put("NumIdleConnectionsAllUsers", pds.getNumIdleConnectionsAllUsers());
                debugMap.put("NumIdleConnectionsDefaultUser", pds.getNumIdleConnectionsDefaultUser());
            } else
                System.err.println("Not a c3p0 PooledDataSource!");
        } catch (SQLException e1) {
            e1.printStackTrace();
        }

        果真,請求一次,當前使用的鏈接多一次,很快100個容量就被用完。因而排查鏈接沒法釋放的緣由,由於使用的SSH,數據庫鏈接池都在配置文件中,而dataSource最終是由Hibernate接管的,看了下最有可能出問題的即是事務,事務配置不合理致使hibernate沒有正常關閉鏈接。在aop的poincut中,個人expression是execution(* com.xxx.manager.service.*.*(..)),但不只是service層,dao層也有相應的鏈接請求,因而,一律而論,把表達式改爲了execution(* com.xxx.manager.*.*.*(..)),測試後,果真好了。

 一、execution(): 表達式主體。

 二、第一個*號:表示返回類型,*號表示全部的類型。

 三、包名:表示須要攔截的包名,後面的兩個句點表示當前包和當前包的全部子包,com.sample.service.impl包、子孫包下全部類的方法。

 四、第二個*號:表示類名,*號表示全部的類。

 五、*(..):最後這個星號表示方法名,*號表示全部的方法,後面括弧裏面表示方法的參數,兩個句點表示任何參數

相關文章
相關標籤/搜索