天天看點

Resin 常見問題及解決方法(FAQ)                        Resin 常見問題及解決方法(FAQ)

                        Resin 常見問題及解決方法(FAQ)

轉載:http://blog.sina.com.cn/s/blog_9fd5b6df01010nf1.html

1. 症狀

1.1. Resin停止響應

1.2. Resin不停重新開機動

1.3. java.lang.OutOfMemoryError錯誤,應用程式記憶體溢出

1.4. 運作一會兒,伺服器開始變得非常慢

1.5. CPU尖峰,高的CPU使用率

1.6. 會話(sessions)變成null,會話丢失

1.6.1. 調試日志

1.6.2. Resin會話配置

1.6.3. 應用程式重載

1.6.4. 浏覽器cookie的局限

1.6.5. cookie域名的問題

1.6.6. cookie名稱沖突

1.6.7. URL重寫

1.7. J2EE規範,javax.servlet包規範1.3和Resin不相容

1.8. Unsupported major.minor version 48.0

1.9. 讀取POST資料的問題

2. 技巧方法

2.1. 啟用調試日志

2.1.1. 伺服器和所有應用程式的完全調試日志

2.1.2. 一個web應用程式的完全調試日志

2.2. 線程轉儲

2.2.1. 使用JDK5工具轉儲線程

2.2.2. 通過發送一個信号轉儲線程

2.2.3. 如果發送信号無效時的線程轉儲

2.2.4. 了解線程轉儲

2.3. 記憶體溢出和垃圾收集

2.3.1. -verbosegc

2.3.2. 使用堆轉儲檢查記憶體使用

2.3.3. 了解 java.hprof.txt 檔案中的棧資訊

2.3.4. 了解 java.hprof.txt 檔案中的CPU資訊

2.3.5. 監視垃圾回收

2.3.6. 增加堆記憶體

2.4. 清空classpath

2.5. 監視HTTP傳輸

2.6. 使用一個外部編譯器

2.7. 調整棧記憶體避免線程限制

2.8. 使用作業系統的 netstat 指令獲得目前 TCP/IP 端口的使用

2.8.1. 連接配接狀态

2.8.2. 端口使用

1. 症狀

1.1. Resin停止響應

●可能是一個線程死鎖的問題,應該進行線程轉儲。

●啟用完全調試日志模式,檢查日志最後的紀錄看看發生了什麼。

1.2. Resin不停重新開機動

●啟用完全調試日志模式,檢查記錄看看Resin為什麼不停的重新開機它。

1.3. java.lang.OutOfMemoryError錯誤,應用程式記憶體溢出

●使用JVM啟動參數增加堆(heap)記憶體。

●轉儲堆,看看那個對象無法被垃圾回收器無法回收。

●轉儲線程,檢查占用着對象的不能釋放的線程

一個OutOfMemoryError錯誤通常意味着堆(heap)記憶體被用盡。一般是應用程式代碼保持了對不在使用的對象的引用,垃圾回收器無法對其進行回收。轉儲堆,能夠查到什麼代碼和什麼種類的對象被占用了。如果對轉儲或者其它監視工具顯示伺服器和你的程式實際沒有超出堆記憶體,那麼OutOfMemoryError意味着JVM超出了虛拟記憶體,也就是底層的malloc()調用失敗。通常這種情況,通過使用作業系統工具顯示記憶體使用,JVM自己能夠顯示其自己的堆記憶體,但是作業系統工具确顯示程序占用了大量的記憶體。在Windows下使用任務管理器,Unix下使用top或者ps指令。

 JVM無法進行堆記憶體配置設定可能有如下原因:

●線程,特别是線程堆占用虛拟記憶體。

●JNI庫可能調用malloc或者nmap占用虛拟記憶體。這包括很多資料庫驅動,也包含一些Resin使用的JNI代碼。

●對于.jar/.zip檔案,JDK要配置設定虛拟記憶體。如果你打開了大量的jar檔案,你可能會遇到問題。可以想到用于打開jar的getResourceAsStream沒有關閉将會耗盡.jar記憶體。

1.4. 運作一會兒,伺服器開始變得非常慢

● 這可能是一個垃圾回收問題。如果你的記憶體缺乏,然後又建立了大量的對象,這導緻垃圾回收器耗盡CPU。如果你記憶體溢出,JVM将會慢慢停止(連續地進行垃圾收集)直到它死亡。

  ○ 監視垃圾收集。

  ○ 轉儲堆,看看是否是有對象無法被回收。

  ○ 參看JVM垃圾回收參數調整的文檔獲得更多垃圾回收的資訊。

● 可能有一個死循環的線程或者一個請求耗盡資源。回應一個請求的線程如果不能傳回,Resin就沒法再次利用它,那麼可用來服務的線程就會越來越少。

  ○ 進行線程轉儲,檢查可能占用對象的無法釋放的線程。

1.5. CPU尖峰,高的CPU使用率

● 轉儲線程,檢查那些線程在無限循環。

● 檢查垃圾收集的部分。

1.6. 會話(sessions)變成null,會話丢失

1.6.1. 調試日志

首先啟用調試日志。特别是浏覽器請求送出的頭資訊能夠顯示一個用戶端的JSESSIONID狀态,日志也能說明Resin什麼時候識别、建立和失效一個會話。

1.6.2. Resin會話配置

另一個可能是session-max設定過低,導緻目前使用者建立會話的數量大于你設定的這個值。另一個可能是會話逾時,你可以通過session-timeout标簽來配置它。

<web-app id='/'>

  ...

  <session-config>

    <!-- timeout after 120 minutes -->

    <session-timeout>120</session-timeout>

    <!-- up to 4096 sessions at once -->

    <session-max>4096</session-max>

  </session-config>

  ...

</web-app>

1.6.3. 應用程式重載

無論何時,一個java源檔案、web.xml或者resin.xml改變,Resin都會重新開機應用程式。如果這個情況發生,你目前的會話就會丢失,除非你配置了一個持久性會話存儲。

1.6.4. 浏覽器cookie的局限

一些使用者報告,如果他們的應用程式使用大量的cookie,浏覽器将會丢棄舊的cookie為新的騰出空間。這就會出現浏覽器丢失了Resin用來跟蹤會話的cookie。IE浏覽器使用者特别容易遇到這個問題。如果你的應用程式使用大量的cookie,最好的解決方案就是減少cookie數量和cookie資料的大小。Resin使用一個單一的cookie其存儲相對很少的資料用拉跟蹤使用者的會話ID。應用程式存儲在cookie中的資訊可以使用HttpSession對象來存儲。作為最後的手段,你可以配置Resin總是使用URL重寫,這需要把enable-cookies設定成false。由于安全的原因URL重寫式不推薦的,因為重寫URL增加了重寫某些頁面丢失調用的高可能性。

<web-app id='/'>

  ...

  <session-config>

    <enable-cookies>false</enable-cookies>

    <enable-url-rewriting>true</enable-url-rewriting>

  </session-config>

  ...

</web-app>

1.6.5. cookie域名的問題

如果你的cookie域名不相容也可能丢失會話。例如,如果你有一個伺服器使用cookie域名"hogwarts.com",另一個使用 "qa.hogwarts.com",在浏覽器中"hogwarts.com"的cookie會幹擾在"qa.hogwarts.com"上的會話。方法是改變cookie域名"hogwarts.com"為"www.hogwarts.com"。 你可以在session-config标簽中設定cookie域名。

1.6.6. cookie名稱沖突

如果你使用Resin和另一個應用伺服器(例如Tomcat),你可能遇到這個沖突,因為它們使用相同的cookie名稱(他通常是JSESSIONID) 來跟蹤會話。Resin提供session-cookie 和 ssl-session-cookie讓你可以改變Resin使用的cookie名稱。

改變用來跟蹤會話的cookie名稱的片斷:

  <cluster>

    ...

    <session-cookie>RJESSESSIONID</session-cookie>

1.6.7. URL重寫

如果你忘記了重寫一個URL,一個需要重寫的使用者當通路到這個URL時将丢失他們的會話。Resin在一個使用者浏覽器和一個會話(session)之間建立一個關聯,是通過為每一個新請求傳回一個惟一的id。這可通過兩種方式之一來完成:使用cookie或者URL重寫。Resin首先嘗試向使用者浏覽器發送一個包含惟一會話ID的cookie來跟蹤一個使用者的會話。有時Resin不能建立cookie,不是因為使用者在其浏覽器禁用了cookies就是因為某些浏覽器不支援它們(例如一些HDML和WML浏覽器)。如果cookie不能建立那麼就會使用URL重寫。在這種情況下,Resin重寫每一個它送出給使用者的URL,讓其包含一個名稱為_jsessionid的參數。然後為每一個新來的請求做的第一件事就是查找這個參數,如果這個參數存在那麼就知道一個會話已經建立,它移出參數并使用它來查找使用者會話對象。URL重寫需要開發者的協同合作。開發者必須編碼每一個URL引用讓Resin有一個合适的機會放置_jsessionid參數。

使用JSTL實作URL重寫

<%@ taglib prefix='c' uri='http://java.sun.com/jstl/core' %>

Time to go <a href="<c:url _fcksavedurl=" target="_blank" rel="external nofollow" <c:url _fcksavedurl="<c:url _fcksavedurl="<c:url value='home.jsp' />">Home</a>!

使用Java scriptlet實作URL重寫

<%

String homeUrl = response.encodeURL("home.jsp");

%>

<%-- the presentation --%>

Time to go <a href="<%= homeUrl %>" target="_blank" rel="external nofollow" >Home</a>!

1.7. J2EE規範,javax.servlet包規範1.3和Resin不相容

參看清除classpath環境變量。

1.8. Unsupported major.minor version 48.0

這個錯誤經常在發現一個沖突的jar時發生,參看清除classpath環境變量。

如果環境變量classpath被完全清除,然而一個JDK或者舊Resin的一個jar或者一些其它元件出現在的什麼地方,如果你已經在那些地方添加了,在你的JAVA_HOME樹裡的一些jar可能有一個問題,那裡可有一個和你的web程式WEB-INF/lib/目錄下沖突的jar。另一種可能是你還沒設定JAVA_HOME,或者你使用了一個沖突的JDK的一些元件。

如果在Windows上,檢查JAVA_HOME之外的java.exe的拷貝,例如C:/WINDOWS/java.exe或者在你PATH路徑裡其它地方的java.exe。

1.9. 讀取POST資料的問題

首先啟用調試日志。調試日志會顯示發送到Resin的請求,提供一些Resin如何處理這些資料的資訊。最重要的是確定在讀取POST參數之前編碼設定正确。浏覽器總是發回和輸出頁面編碼相同的參數。因為請求不包含編碼,應用程式代碼需要確定編碼比對。是以第一件事就是确定發送到浏覽器的表單的編碼。你的應用程式總應該指定它。一旦你指定了它,你就知道浏覽器POST使用編碼。(這裡UTF-8是個自然的編碼選擇,我不能确信你為什麼使用其它的編碼)。在讀取POST參數之前確定設定了正确的編碼,你可以調用request.setCharacterEncoding(encoding)來設定編碼。

2. 技巧方法

2.1. 啟用調試日志

Resin使用JDK日志工具提供了大量的診斷資訊。通過使用一個空名稱(比對所有名字)可以啟用完全調試日志,調試級别為“全部”。因為将會産生大量資訊,把這些資訊放在一個單獨的檔案中比較好。

2.1.1. 伺服器和所有應用程式的完全調試日志

下面的配置每天建立一個日志,當一個問題出現時用來查找問題出現在什麼地方。因為日志配置在resin.xml中,日志的捕捉是伺服器和其上所有應用程式的。日志輸出的資訊在 $RESIN_HOME/log/debug.log。

<!-- resin.xml -->

<resin xmlns="http://caucho.com/ns/resin">

  <log-handler name="" level="all" path="log/debug.log"

                  timestamp="[%H:%M:%S.%s] {%{thread}} " />

  <logger name="" level="finer" />

</resin>

有其它一些的日志配置選項,請參看Resin日志文檔。

2.1.2. 一個web應用程式的完全調試日志

通常你一般僅需要一個程式輸出的調試日志。日志配置記錄放在<web-app-root>/WEB-INF/web.xml中,那麼僅這個web應用程式的日志資訊被輸出到日志檔案中。下面的配置每天建立一個調試日志,位置<web-app-root>/WEB-INF/work/debug.log。

<!-- <web-app-root>/WEB-INF/web.xml -->

<web-app>

  ...

  <log name="" path="WEB-INF/work/debug.log" timestamp="[%H:%M:%S.%s] {%{thread}} " />

  <logger name="" level="finer" />

  ...

</web-app>

2.2. 線程轉儲

如果應用程式好像有問題或者超出資源洩露,線程轉儲能夠顯示伺服器的狀态。對于伺服器調試Java的縣城轉儲是一個重要的工具。因為Servlet是多線程的,沒有處理好的話很可能出現死鎖,或者出現死循環和導緻記憶體溢出錯誤。特别是你使用了第三方軟體例如資料庫、EJB和Corba ORBs。

2.2.1. 使用JDK5工具轉儲線程

在JDK5裡可以使用jps和jstack,一個快捷的指令行方法獲得目前所有線程的堆棧跟蹤資訊。

# jps

12903 Jps

20087 Resin

# jstack 20087

Attaching to process ID 20087, please wait...

Debugger attached successfully.

Client compiler detected.

JVM version is 1.5.0-beta2-b51

Thread 12691: (state = BLOCKED)

 - java.lang.Object.wait(long) (Compiled frame; information may be imprecise)

 - com.caucho.util.ThreadPool.runTasks() @bci=111, line=474 (Compiled frame)

 - com.caucho.util.ThreadPool.run() @bci=85, line=423 (Interpreted frame)

 - java.lang.Thread.run() @bci=11, line=595 (Interpreted frame)

Thread 12689: (state = BLOCKED)

 - java.lang.Object.wait(long) (Compiled frame; information may be imprecise)

 - com.caucho.util.ThreadPool.runTasks() @bci=111, line=474 (Compiled frame)

 - com.caucho.util.ThreadPool.run() @bci=85, line=423 (Interpreted frame)

 - java.lang.Thread.run() @bci=11, line=595 (Interpreted frame)

...

2.2.2. 通過發送一個信号轉儲線程

在 Windows, ctrl-break會産生線程轉儲。

在Unix, "kill -QUIT" 會産生線程轉儲。

2.2.3. 如果發送信号無效時的線程轉儲

你可以在啟動JVM時指定附加的參數允許附加一個調試器而不是發送信号來轉儲線程。你然後在任何時候附加調試器來得到線程轉儲。 這種方法在所有的作業系統上得到支援。

下面是是逐漸的指導:

   1. 使用附加的參數啟動Resin來允許一個調試器附加:

      resin.xml for debugging

      <resin xmlns="http://caucho.com/ns/resin">

      <cluster id="">

        <server-default>

          <jvm-arg>-Xdebug</jvm-arg>

          <jvm-arg>-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5432</jvm-arg>

        </server-default>

        <server id="" address="127.0.0.1" port="6800" />

      </cluster>

      </resin>

   2. 等待,直到你認為應用程式出現了死鎖或者失去控制。

   3. 打開另一個終端 (window), 使用jdb連接配接正在運作的Resin執行個體:

      $JAVA_HOME/bin/jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=5432

      jdb會顯示類似如下資訊:

      Set uncaught java.lang.Throwable

      Set deferred uncaught java.lang.Throwable

      Initializing jdb ...

      >

   4. 使用 "suspend" 指令, 然後 "where all"指令獲得一個線程轉儲:

      例子: jdbc suspend

      > suspend

      All threads suspended.

      > where all

      tcpConnection-6862-3:

       [1] java.lang.Object.wait (native method)

       [2] com.caucho.server.TcpServer.accept (TcpServer.java:650)

       [3] com.caucho.server.TcpConnection.accept

      (TcpConnection.java:208)

       [4] com.caucho.server.TcpConnection.run (TcpConnection.java:131)

       [5] java.lang.Thread.run (Thread.java:536)

      tcpConnection-543-2:

       [1] java.lang.Object.wait (native method)

       [2] com.caucho.server.TcpServer.accept (TcpServer.java:650)

       [3] com.caucho.server.TcpConnection.accept

      (TcpConnection.java:208)

       [4] com.caucho.server.TcpConnection.run (TcpConnection.java:131)

       [5] java.lang.Thread.run (Thread.java:536)

      ..

   5. 使用 "resume" 指令來恢複程序

      > resume

Unix 使用者(和Windows上的Cygwin使用者)可以使用一個腳本:

resin-thread-dump.sh

#!/bin/sh

echo -e "suspend\nwhere all\nresume\nquit" | $JAVA_HOME/bin/jdb -connect \

  com.sun.jdi.SocketAttach:hostname=localhost,port=5432

雖然沒有進行過嚴格基準測試,好像使用線程轉儲參數啟動的JVM在性能上影響不大。

2.2.4. 了解線程轉儲

在任何情況下,你會最終得到類似如下的跟蹤調試資訊(不同的JDK有稍微的差别):

Full thread dump:

"tcpConnection-8080-2" daemon waiting on monitor [0xbddff000..0xbddff8c4]

        at java.lang.Object.wait(Native Method)

        at com.caucho.server.TcpServer.accept(TcpServer.java:525)

        at com.caucho.server.TcpConnection.accept(TcpConnection.java:190)

        at com.caucho.server.TcpConnection.run(TcpConnection.java:136)

        at java.lang.Thread.run(Thread.java:484)

"tcpConnection-8080-1" daemon waiting on monitor [0xbdfff000..0xbdfff8c4]

        at java.lang.Object.wait(Native Method)

        at com.caucho.server.TcpServer.accept(TcpServer.java:525)

        at com.caucho.server.TcpConnection.accept(TcpConnection.java:190)

        at com.caucho.server.TcpConnection.run(TcpConnection.java:136)

        at java.lang.Thread.run(Thread.java:484)

"tcpConnection-8080-0" daemon waiting on monitor [0xbe1ff000..0xbe1ff8c4]

        at java.lang.Object.wait(Native Method)

        at com.caucho.server.TcpServer.accept(TcpServer.java:525)

        at com.caucho.server.TcpConnection.accept(TcpConnection.java:190)

        at com.caucho.server.TcpConnection.run(TcpConnection.java:136)

        at java.lang.Thread.run(Thread.java:484)

"tcp-accept-8080" runnable [0xbe7ff000..0xbe7ff8c4]

        at java.net.PlainSocketImpl.socketAccept(Native Method)

        at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:413)

        at java.net.ServerSocket.implAccept(ServerSocket.java:243)

        at java.net.ServerSocket.accept(ServerSocket.java:222)

        at com.caucho.server.TcpServer.run(TcpServer.java:415)

        at java.lang.Thread.run(Thread.java:484)

"resin-cron" daemon waiting on monitor [0xbe9ff000..0xbe9ff8c4]

        at java.lang.Thread.sleep(Native Method)

        at com.caucho.util.Cron$CronThread.run(Cron.java:195)

"resin-alarm" daemon waiting on monitor [0xbebff000..0xbebff8c4]

        at java.lang.Thread.sleep(Native Method)

        at com.caucho.util.Alarm$AlarmThread.run(Alarm.java:268)

"Signal Dispatcher" runnable [0..0]

"Finalizer" daemon waiting on monitor [0xbf3ff000..0xbf3ff8c4]

        at java.lang.Object.wait(Native Method)

        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:108)

        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:123)

        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:162)

"Reference Handler" daemon waiting on monitor [0xbf5ff000..0xbf5ff8c4]

        at java.lang.Object.wait(Native Method)

        at java.lang.Object.wait(Object.java:420)

        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:110)

"main" waiting on monitor [0xbfffd000..0xbfffd210]

        at java.lang.Thread.sleep(Native Method)

        at com.caucho.server.http.ResinServer.waitForExit(ResinServer.java:674)

        at com.caucho.server.http.ResinServer.main(ResinServer.java:821)

        at com.caucho.server.http.HttpServer.main(HttpServer.java:95)

每個線程都被命名了。這裡有一些通用的名稱:

線程名稱 描述

tcp-accept-8080 在8080端口監聽新連接配接的線程

tcpConnection-8080-3 處理從8080端口連接配接的servlet線程

tcp-cron Resin的run-at線程

tcp-alarm Resin的警告線程

Resin為每一個<http>和<srun>開啟一個 tcp-accept-xxx  線程,tcp-accept-xxx 線程總是處于socketAccept狀态。應該有一些tcpConnection-xxx-n線程,每一個都是一個servlet線程。在一個忙碌的伺服器上,這些能在你代碼裡任何地方出現。如果幾個出現在一個位置,你可能有某種死鎖或者至少一個慢鎖。空閑線程不是tcpAccept就是httpRequest 或者runnerRequest。對于死鎖,你應檢視"waiting on monitor"線程和很多線程阻塞在同一位置的任一執行個體。

2.3. 記憶體溢出和垃圾收集

大部分記憶體問題時應用程式設計上的記憶體漏洞。例如,一個緩存或者vector填充了過期的資料,或者一個singleton或者靜态變量不能适當地偵測到web-app重新開機。大部分怪異的記憶體問題是堆記憶體或者虛拟記憶體溢出,當使用了大量的線程(〉256)。

追蹤捕獲記憶體問題的步驟是:

   1. 使用 resin.sh start or resin.exe -install啟用 -J-verbosegc。 -verbosegc标志記錄堆的垃圾收集,讓你知道你是否堆記憶體溢出了(大部分情況是這樣的)。

   2. 獲得一個heap profiler或者在JVM中使用堆轉儲。JProfiler是一個價格便宜的商業的heap profiler.雖然JVM的堆轉儲不是很使用者友好,但是它也是可用的。你應該使用一個heap profiler作為你開發過程的一部分,在任一産品投入使用前應該使用一個。

   3.  使用heap profiler, 找到2-3個過量消耗記憶體的使用者并修正這些記憶體漏洞。

   4. 一般應用程式錯誤包括:

          ○ 在每一個請求(request)結束ThreadLocal變量沒有正常清除。

          ○ 單一模式(Singleton) 或者靜态散列影射和緩存,web-app重新開機要清除。

          ○ web-app重新開機後衍生出來的線程不能被停止。

          ○ web-app 變量 (像 "application" 變量), 被存儲在一個靜态變量中。

   5.如果堆沒問題,例如 -verbosegc顯示了一個穩定的堆,你應該看看非堆記憶體:

          ○ 線程棧的使用(-Xss2m). 每一個線程的消耗一些非堆記憶體。一些系統預設是8M。在一些32位系統上虛拟記憶體的限制大約是2G,256個線程,每個消耗8M,就能耗盡虛拟記憶體。你可以減小棧記憶體通過使用 -Xss指令。

          ○ Java 本地接口記憶體(JNI memory)。如果你使用JNI庫或者使用了利用JNI的驅動,JNI配置設定了比可用記憶體更多的記憶體是可能的。

          ○ fork/exec 和 OS 限制. 如果作業系統沒有足夠的交換空間可用,例如作業系統可能拒絕一個"jikes"編譯。

          ○ NIO, 記憶體影射, 和 .jar 檔案. JDK在記憶體中影射jar檔案。在某些情況下,大量的jar檔案能夠耗盡虛拟記憶體。這種情況也出現在NIO記憶體映射中。

   6. 如果所有這些情況都被排除,它可能是一個Resin的BUG。然而你應該在報告Resin BUG之前找到了這個記憶體漏洞,例如在報告BUG之前你已經進行了上面所有的檢查。關于記憶體溢出的BUG報告,如果沒有得到一個JDK記憶體轉儲,它更有可能是一個程式上的錯誤。你必須在報告任一潛在的Resin記憶體問題時提供一個堆轉儲。

2.3.1. -verbosegc

-verbosegc是一個JVM的調試輸出。對于檢查基本的記憶體使用和垃圾收集時間它是一個非常友善的工具。對于任一産品系統使用 -verbosegc 是一個好主意。當啟動Resin時,你可以使用-J-verbosegc。

特定的輸出依賴于JVM,一些内容看起來如下:

-verbosegc output

[GC 9176K->8647K(9768K), 0.0014790 secs]

[GC 9287K->8668K(9768K), 0.0011120 secs]

[GC 9308K->8668K(9768K), 0.0007810 secs]

"(9768K)"是非常重要的資料,表示最大可配置設定的堆大約是10M。其它數值顯示了實際的堆使用在垃圾收集前後。

2.3.2. 使用堆轉儲檢查記憶體使用

如果一個應用程式過分地消耗記憶體直到抛出記憶體溢出錯誤,或者好像在垃圾收集上消耗了大量的時間,一個堆轉儲能夠幫助你找到問題的根源。真正需要你去做的是有一個CPU和堆調試程式(profile)。JDK自帶一個簡單的(界面不是很使用者友好),是以不必一定需要買一個profile。jvmstat就是一個簡單的堆螢幕。它是一個标準的java參數,是以"java -Xrunhprof:help"會告訴你如何啟動它。例如你可以如下啟動Resin

> resin.sh -J-Xrunhprof:heap=sites,cpu=samples

(在Unix上, Resin啟動腳本有個 -cpuprof-ascii 參數被自動設定.)

 運作一個負載一定的時間(你可以運作類似Apache "ab"工具10分鐘時間),然後正常停止伺服器,你不應使用crtl-C殺死它,你需要一個正常的退出。它會轉儲一個 java.hprof.txt 檔案。在這個檔案的尾部檢視跟蹤資訊。

2.3.3. 了解 java.hprof.txt 檔案中的棧資訊

假設你采用廉價方案,使用JDK的堆調試器而不是購買一個,你就需要幫助來解釋它。下面是一個運作中的Resin堆轉儲的一個例子。在這個例子中你要跳到"SITES BEGIN" 開始的段落。對于這大部分資訊,你僅需要注意上面的20行。别的其它的都是雜亂資訊,忽略它。

SITES BEGIN (ordered by live bytes) Tue Jan  9 17:44:33 2001

          percent         live       alloc'ed  stack class

 rank   self  accum    bytes objs   bytes objs trace name

    1 11.87% 11.87%   983520  120 1393320  170  2553 [B

    2  9.89% 21.76%   819600  100 1286772  157  4070 [B

    3  9.09% 30.85%   753756   23 3539376  108  4970 [L<Unknown>;

    4  5.83% 36.68%   483564   59  778620   95  7180 [B

    5  5.74% 42.42%   475368   58  745836   91  7178 [B

    6  4.35% 46.77%   360624   44  696660   85  7182 [B

    7  2.97% 49.74%   245880   30  450780   55  7176 [B

    8  2.37% 52.11%   196704   24  352428   43  7254 [B

    9  1.88% 53.99%   155724   19  262272   32  7174 [B

   10  1.78% 55.77%   147528   18  245880   30  7137 [B

   11  1.53% 57.30%   126988 1063 16973092 129113  3271 [C

   12  1.34% 58.64%   110684 3953 20362832 727244  1213 sun/io/CharToByteISO8859_1

   13  1.25% 59.88%   103320  738  141820 1013  5942 java/lang/Class

   14  1.21% 61.10%   100548   49  221616  108  5003 [L<Unknown>;

   15  1.21% 62.31%   100548   49  221616  108  5005 [L<Unknown>;

   16  1.07% 63.38%    89080 1532 18393580 317347  1340 [B

   17  0.79% 64.18%    65568    8   81960   10  8408 [B

   18  0.79% 64.97%    65552    4   65552    4 27630 [C

   19  0.70% 65.67%    58232   24 1110128  386  5038 [C

   20  0.68% 66.35%    56200  450  116816  980  7186 [C

有兩個需要查找的。首先,如果任何一個類在"live objs"列數值大,你需要分析它。 那可能有記憶體漏洞。第二,如果一些類在"alloc'ed objs"列數值大,這可能浪費了大量的垃圾收集時間,你可以使用緩存來解決它。

在類名稱中的 [C 意味着一個字元數組。要知道它到底表示什麼,你需要檢視棧跟蹤 (3271):

TRACE 3271:

java/lang/String.<init>(String.java:244)

com/caucho/util/CharBuffer.close(CharBuffer.java:714)

com/caucho/vfs/FilesystemPath.normalizePath(FilesystemPath.java:162)

com/caucho/vfs/FilesystemPath.schemeWalk(FilesystemPath.java:127)

那是 Resin的VFS代碼部分。也許在将來會盡力減少它。你使用 "-prof-depth 10"參數能得到更長的資訊。(或者在-Xrunhprof指定相應的深度)。那會通常會給出更多的資訊。

2.3.4. 了解 java.hprof.txt 檔案中的CPU資訊

CPU資訊比較容易了解。在一些JDK,你需要禁用JIT來運作它。

CPU SAMPLES BEGIN (total = 424614) Tue Jan  9 17:44:33 2001

rank   self  accum   count trace method

   1 21.36% 21.36%   90704  7266 com/caucho/server/http/VirtualHost.logAccess

   2 10.84% 32.20%   46041  7269 java/net/SocketInputStream.socketRead

   3  5.99% 38.19%   25428  1213 java/lang/Class.newInstance0

   4  5.11% 43.31%   21715  7896 com/caucho/util/CharBuffer.toString

   5  4.82% 48.13%   20463  1286 sun/io/CharToByteISO8859_1.convert

   6  3.54% 51.66%   15018  1242 sun/io/CharToByteConverter.<init>

   7  2.68% 54.35%   11388  7241 java/io/PrintWriter.<init>

   8  2.47% 56.82%   10508  7748 com/caucho/server/http/Request.fillCookies

   9  2.27% 59.09%    9650  1214 sun/io/ByteToCharConverter.<init>

  10  1.85% 60.94%    7857  5097 java/lang/String.<init>

  11  1.59% 62.53%    6754  1341 java/lang/String.substring

  12  1.57% 64.10%    6650  1340 java/lang/String.getBytes

  13  0.92% 65.02%    3907  7897 java/lang/String.<init>

  14  0.76% 65.78%    3227  3259 com/caucho/vfs/FilePath.fsWalk

  15  0.75% 66.53%    3195  7895 com/caucho/server/http/Request.fillCookie

  16  0.71% 67.25%    3031  7321 java/lang/String.getBytes

  17  0.71% 67.95%    2996  3270 com/caucho/util/CharBuffer.close

  18  0.68% 68.63%    2892  3271 java/lang/String.<init>

  19  0.66% 69.29%    2782  7318 com/caucho/vfs/FilePath.openWriteImpl

  20  0.61% 69.90%    2604  7320 java/io/FileOutputStream.<init>

你僅需要注意頂部的20行。你可能需要忽略頂部10行的一些資訊,因為它們僅僅是等待一個使用者的回應。SocketInputStream.socketRead是一個例子。你可使用跟蹤号萊調用跟蹤資訊:

TRACE 7266:

com/caucho/server/http/VirtualHost.logAccess(VirtualHost.java:487)

com/caucho/server/http/Application.logAccess(Application.java:1846)

com/caucho/server/http/Response.finish(Response.java:1345)

com/caucho/server/http/Request.finish(Request.java:416)

2.3.5. 監視垃圾回收

使用附加參數-Xloggc:gc.log 運作Resin, "gc.log" 是日志檔案的名稱,其将會在Resin根目錄建立,例如 /resin/gc.log。一旦伺服器在一定負載下運作一定時間,或者開始出現了問題,檢視gc.log檔案,并搜尋"Full"。開始它出現的不是很頻繁,往底部檢視,将會變得越來越頻繁知道連續出現。注意在第一列的"timestamp"是程序已運作的秒數。垃圾收集日志會對性能有輕微的影響,但是它對診斷與垃圾收集的相關問題是很重要的。過多的垃圾收集的可能原因是記憶體洩露和不充足的堆記憶體。

2.3.6. 增加堆記憶體

參看JVM微調中有關記憶體部分的内容。

2.4. 清空classpath

舊的或者不相容的類版本經常引起沖突。摒除這些類的第一個步驟是使用一個空的CLASSPATH環境變量來啟動Resin。

win> set CLASSPATH=

win> bin/resin.exe

unix.sh> export CLASSPATH=""

unix.sh> bin/resin.sh

如果你已經在$RESIN_HOME/lib目錄或者你的JDK目錄放置了一些jar檔案,也同樣可能引起沖突。

如果在一個舊版本的Resin上安裝一個新的Resin(例如安裝在相同目錄),一些舊的jar可能殘留。最好給每一版本獨立的目錄。

如果RESIN_HOME環境變量沒有設定,Resin可能采用一個舊版本的。

你可以使用 -verbose 選項運作resin.sh/resin.exe來檢視當Resin啟動時使用的CLASSPATH。

2.5. 監視HTTP傳輸

要監視HTTP頭資訊,在$RESIN_HOME/resin.xml檔案中啟用如下調試日志:

<resin xmlns="http://caucho.com/ns/resin">

  ...

  <log-handler name='com.caucho.server.http' level='finer'

                  path='log/http.log' />

  <log-handler name='com.caucho.server.connection' level='finer'

                  path='log/http.log' />

  ...

</resin>

偵聽和監視一個web浏覽器和Resin之間傳遞的原始資料能夠提供很有價值的資訊。這個原始資料包括浏覽器送出的資訊頭和内容,及Resin傳回給浏覽器的資訊頭和内容。 Apache Axis jar包含了一個工具"tcpmon",它可以用來偵聽和監視浏覽器和Resin之間的傳輸。使用tcpmon, 你要指定一個"listen port" 、一個 "target host" 、一個"target port"。例如,如果你通常運作Resin在8080端口上,你可以啟動tcpmon使用"listen port"9090端口,一個localhost的目标主機和一個目标端口8080。現在你可以在浏覽器中使用一個url http://localhost:9090。這時浏覽器就會使用tcpmon。tcpmon會紀錄發送的請求,同時轉發内容到8080端口上的Resin,也會紀錄Resin傳回的資料并把它也發送回浏覽器。

2.6. 使用一個外部編譯器

Resin預設使用内部(internal)編譯器,因為它是很容易使用的。有時内部編譯器會導緻錯誤,抛出錯誤或者簡單挂起和占用一個線程。解決方法是在resin.xml中改變編譯器為"javac"。

  <javac compiler="javac" args="" />

當然也可以使用Jikes等編譯器。

2.7. 調整棧記憶體避免線程限制

每一個線程配置設定了一個棧,如果棧尺寸太大,當線程數量增大時可能記憶體溢出。請參看JVM參數調整的文章。

2.8. 使用作業系統的 netstat 指令獲得目前 TCP/IP 端口的使用

netstat指令可用來擷取目前系統的網絡狀态。

unix$ netstat -anp

win> netstat -an

-a 訓示偵聽的和非偵聽的套接字都顯示。-n 訓示顯示端口号而不是端口名(例 http)。-p 顯示正在使用套接字的程序。因為Windows下的netstat指令和UNIX下的有些不同,-p選項在Winodws系統上無效。

2.8.1. 連接配接狀态

連接配接狀态可能是最重要的資訊。可檢視netstat指令幫助獲得相關狀态的較長的描述。

"LISTEN" or "LISTENING" 表示,程序在套接字上等待連接配接。

"TIME_WAIT"表示包處理結束之後套接字仍在等待的狀态。連接配接關閉之後,套接字會被作業系統保持在打開狀态一個短期的時間。即使連接配接完全關閉了,在網絡上也可能有些偏離的包需要連接配接。TIME_WAIT就是保持套接字足夠長的打開時間來捕捉這些偏離的包,以至于這些偏離的包不會傳輸到在同一個套接字上的新連接配接上。

2.8.2. 端口使用

如果Resin顯示不能綁定到一個端口,這意味着可能令一個其它程序在使用這個端口,netstat可以查出那個程式在使用這個端口。因為netstat産生了很多資訊,應該濾掉那些沒用的資訊。下面的例子是查找使用80端口的程式:

unix$ netstat -anp | grep ":80" | less