JAVAEE 常見性能問題解決手冊 內存溢出錯誤 最常見的折磨著企業級應用程序的錯誤是讓人恐懼的outofmemoryError(內存溢出錯誤) 這個錯誤引起下面這些典型的癥狀: ----應用服務器崩潰 ----性能下降 ----一個看起來好像無法結束的死循環在重復不斷的執行垃圾收集,它會導致程序停止運行,并且經常導致應用服務器崩潰 不管癥狀是什么,如果你想讓程序恢復正常運行,你一般都需要重新啟動應用服務器。 引發out-of-memory 錯誤的原因 在你打算解決out-of-memory 錯誤之前,首先了解為什么會引發這個錯誤對你有很大的幫助。如果JVM里運行的程序, 它的內存堆和持久存儲區域的都滿了,這個時候程序還想創建對象實例的話,垃圾收集器就會啟動,試圖釋放足夠的內存來創建這個對象。這個時候如果垃圾收集器沒有能力釋放出足夠的內存,它就會拋出OutOfMemoryError內存溢出錯誤。 Out-of-memory錯誤一般是JAVA內存泄漏引起的。回憶上面所討論的內容,內存泄漏的原因是一個對象雖然不被使用了,但是依然還有對象引用他。當一個對象不再被使用時,但是依然有一個或多個對象引用這個對象,因此垃圾收集器就不會釋放它所占據的內存。這塊內存就被占用了,堆中也就少了塊可用的空間。在WEB REQUESTS中這種類型的的內存泄漏很典型,一兩個內存對象的泄漏可能不會導致程序服務器的崩潰,但是10000或者20000個就可能會導致這個惡果。而且,大多數這些泄漏的對象并不是象DOUBLE或者INTEGER這樣的簡單對象,而可能是存在于堆中一系列相關的對象。例如,你可能在不經意間引用了一個Person對象,但是這個對象包含一個Profile對象,此對象還包含了許多擁有一系列數據的PerformanceReview對象。這樣不只是丟失了那個Person對象所占據的100 bytes的內存,你丟失了這一系列相關對象所占據的內存空間,可能是高達500KB甚至更多。 為了尋找這個問題的真正根源,你需要判斷是內存泄漏還是以OutOfMemoryError形式出現的其他一些故障。我使用以下2種方法來判斷: ----深入分析內存數據 ----觀察堆的增長方式 不同JVM(JAVA虛擬機)的調整程序的運作方式是不相同的,例如SUN和IBM的JVM,但都有相同的的地方。 SUN JVM的內存管理方式 SUN的JVM是類似人類家族,也就是在一個地方創建對象,在它長期占據空間之前給它多次死亡的機會。 SUN JVM會劃分為: 1 年輕的一代(Young generation),包括EDEN和2個幸存者空間(出發地和目的地the From space and the To space) 2 老一代(Old generation) 3 永久的一代(Permanent generation) 圖1 解釋了SUN 堆的家族和空間的詳細分類 對象在EDEN出生就是被創建,當EDEN滿了的時候,垃圾收集器就把所有在EDEN中的對象掃描一次,把所有有效的對象拷貝到第一個幸存者空間,同時把無效的對象所占用的空間釋放。當EDEN再次變滿了的時候,就啟動移動程序把EDEN中有效的對象拷貝到第二個幸存者空間,同時,也將第一個幸存者空間中的有效對象拷貝到第二個幸存者空間。如果填充到第二個生存者空間中的有效對象被第一個生存者空間或EDEN中的對象引用,那么這些對象就是長期存在的(也就是說,他們被拷貝到老一代)。若垃圾收集器依據這種小幅度的調整收集(minor collection)不能找出足夠的空間,就是象這樣的拷貝收集(copy collection),就運行大幅度的收集,就是讓所有的東西停止(stop-the-world collection)。運行這個大幅度的調整收集時,垃圾收集器就停止所有在堆中運行的線程并執行清除動作(mark-and-sweep collection),把新一代空間釋放空并準備重啟程序。 圖2和圖3展示的是了小幅度收集如何運行 圖2。對象在EDEN被創建一直到這個空間變滿。 圖3。處理的順序十分重要:垃圾收集器首先掃描EDEN和生存者空間,這就保證了占據空間的對象有足夠的機會證明自己是有效的。 圖4展示了一個小幅度調整是如何運行的 圖4:當垃圾收集器釋放所有的無效的對象并把有效的對象移動到一個更緊湊整齊的新空間,它將EDEN和生存者空間清空。 以上就是SUN實現的垃圾收集器機制,你可以看出在老一代中的對象會被大幅度調整器收集清除。長生命周期的對象的清除花費的代價很高,因此如果你希望生命周期短的對象在占據空間前及時的死亡,就需要一個主垃圾收集器去回收他們的內存。 上面所講解的東西是為了更好的幫助我們識別出內存泄漏。當JAVA中的一個對象包含了一個并不想要的一個指向其他對象的引用的時候,內存就會泄漏,這個引用阻止了垃圾收集器去回收它所占據的內存。采用這種機制的SUN 虛擬機,對象不會被丟棄,而是利用自己特有的方法把他們從樂園和幸存者空間移動到老一代地區。因此,在一個基于多用戶的WEB環境,如果許多請求造成了泄漏,你就會發現老一代的增長。 圖5顯示了那些潛在可能造成泄漏的對象:主收集器收集后遺留下來占據空間的對象會越來越多。不是所有的占據空間的對象都造成內存泄漏,但是造成內存泄漏的對象最終都占據者空間。如果內存泄漏的確存在,這些造成泄漏的對象就會不斷的占據空間,直至造成內存溢出。 因此,我們需要去跟蹤垃圾收集器在處理老一代中的運行:每次垃圾收集器大幅度收集運行時,有多少內存被釋放?老一代內容是不是按一定的原理來增長? 圖5。陰影表示在經過大幅度的收集后幸存下來的對象,這些對象是潛在可能引發內存泄漏的對象 一部分這些相關的信息是可以通過跟蹤API得到,更詳細的信息通過詳細的垃圾收集器的日志也可以看到。和所有的跟蹤技術一樣,日值記錄詳細的程度影響著JVM的性能,你想得到的信息越詳細,付出的代價也就越高。為了能夠判斷內存是否泄漏,我使用了能夠顯示輩分之間所有的不同的較權威的技術來顯示他們的區別,并以此來得到結果。SUN 的日志報告提供的信息比這個詳細的程度超過5%,我的很多客戶都一直使用那些設置來保證他們管理和調整垃圾收集器。下面的這個設置能夠給你提供足夠的分析數據: –verbose:gc –xloggc:gc.log –XX:+PrintGCDetails –XX:+PrintGCTimeStamps 明確發現在整個堆中存在有潛在可能泄漏內存的情況,用老一代增長的速率才比較有說服力。切記調查不能決定些什么:為了能夠最終確定你內存泄漏,你需要離線在內存模擬器中運行你的應用程序。 IBM JVM內存管理模式 IBM的JVM的機制有一點不同。它不是運行在一個巨大的繼承HEAP中,它僅在一個單一的地區維護了所有的對象同時隨著堆的增長來釋放內存。這個堆是這樣運行的:在一開始運行的時候,它會很小,隨著對象實例不斷的填充,在需要執行垃圾收集的地方清除掉無效的對象同時把所有有效的對象緊湊的放置到堆的底部。因此你可能猜測到了,如果想尋找可能發生的內存泄漏應該觀察堆中所有的動作,堆的使用率是在提高? 如何分析內存泄漏 內存泄漏非常難確定,如果你能夠確定是請求導致的,那你的工作就非常簡單了。把你的程序放入到運行環境中,并在內存模擬器中運行,按下面的步驟來: 1. 在內存模擬器中運行你的應用程序 2. 執行使用方案(制造請求)以便讓程序在內存中裝載請求所需要的所有的對象,這可以為以后詳細的分析排除不必要的干擾 3. 在執行使用方案前對堆進行拍照以便捕獲其中所有運行的對象。 4. 再次運行使用方案。 5. 再次拍照,來捕獲使用方案運行之后堆中所有對象的狀態。 6. 比較這2個快照,找出執行使用方案后本不應該出現在堆中的對象。 這個時候,你需要去和開發者交流,告訴他你所碰到的棘手的請求,他們可以判斷究竟是對象泄漏還是為了某個目的特地讓對象保留下來的。如果執行完后并沒有發現內存泄漏的情況,我一般會轉到步驟4再進行多次類似的跟蹤。比如,我可能會將我的請求反復運行17次,希望我的泄漏分析能夠得到17個情況(或更多)。這個方法不一定總有用,但如果是因為請求引起的對象泄漏的話,就會有很大的幫助。 如果你無法明確的判斷泄漏是因為請求引發的,你有2個選擇: 1. 模擬每一個被懷疑的請求直至發現內存泄漏 2. 存配置一個內存性能跟蹤工具 第一個選項在小應用程序中是確實可用的或者你非常走運的解決了問題,但對大型應用程序不太有用。如果你有跟蹤工具的話第二個選擇是比較有用的。這些工具利用字節流工具跟蹤對象的創建和銷毀的數量,他們可以報告特定類中的對象的數量狀態,例如把Collections類作為特定的請求。例如,一個跟蹤工具可以跟蹤/action/login.do請求,并在它完成后將其中的100個對象放入HASHMAP中。這個報告并不能告訴你造成泄漏的是代碼還是某個對象,而是告訴你在內存模擬器中應該留意那些類型的請求。把程序服務器放到產品環境中并不會使他們變敏感,而是跟蹤性能的工具可以使你的工作變的更簡單化。 虛假內存泄漏 少數的一些問題看起來是內存泄漏實際上并非如此。 我將這些情況稱為假泄漏,表現在下面幾種情況: 1. 分析過早 2. Session泄漏 3. 異常的持久區域 這章節對這些假泄漏都進行了調查,描述了如何去判斷這些情況以及如何處理. 不要過早分析 為了在尋找內存泄漏的時候盡量減少出現判斷錯誤的可能性,你應當在適當的時候分析堆。危險是:一些生命周期長的對象需要裝載到堆中,因此在堆達到穩定狀態且包含了核心對象之前具有很大的欺騙性。在分析堆之前,應該讓應用程序達到穩定狀態。 為了判斷是否過早的對堆進行分析,持續2個小時對跟蹤到的分析快照進行分析,看堆的使用率是上升還是下降。如果是下降,保存這個時候的內存記錄。如果是上升,這個時候就需要分析內存中的SESSION了。 發生泄漏的session WEB請求經常導致內存泄漏,在一個WEB請求中,對象會被限制存儲在有限的幾個區域。這些區域就是: 1. 頁面區域 2. 請求區域 3. 上下文區域 4. 應用程序區域 5. 靜態變量 6. 長生命周期的變量,例如SERVLET 當實現一些JSP(JAVASERVER頁面)時,在頁面上聲明的變量在頁面結束的時候就被釋放,這些變量僅僅在這個單獨的頁面存在時存在。WEB服務器會向應用程序服務器傳送一系列參數和屬性,也就是在SERVLET和JSP之間傳輸HttpServletRequest中的對象。你的動態頁面依靠HttpServletRequest在不同的組件之間傳輸信息,但當請求完成或者socket結束的時候,SERVLET控制器會釋放所有在HttpServletRequest 中的對象。這些對象僅在他們的請求的生命周期內存在。 HTTP是無狀態的,這意味著客戶向服務器發送一個請求,服務器回應這個請求,這個傳遞就完成了,就是會話結束了。我們應該感激WEB頁面幫我們做的日志,這樣我們就能向購物車放置東西,并去檢查它,服務器能夠定義一個跨越多請求的擴展對話。屬性和參數被放在各自用戶的HttpSession對象中,并通過它讓程序的SERVLET和JSP交流。利用這種辦法,頁面存儲你的信息并把他們添加到HttpSession中,因此你可以用購物車購買東西,并檢查商品和使用信用卡付帳。作為一個無狀態的協議,它總是客戶端發起連接請求,服務器需要知道一個會話存在多長時間,到時候就應該釋放這個用戶的數據。超過這個會話的最長時間就是會話超時,他們在程序服務器中設置。除非明確的要求釋放對象或者這個會話失效,否則在會話超時之前會話中的對象會一直存在。 正如session是為每個用戶管理對象一樣,ServletContext為整個程序管理對象。ServletContext的有效范圍是整個程序,因此你可以利用Servlet中的ServletContext或者JSP應用程序對象在所有的Servlet和JSP之間讓在這個程序中的所有用戶共享數據。ServletContext是最主要的存放程序配置信息和緩存程序數據的地方,例如JNDI的信息。 如果數據不是存儲這個四個地方(頁面范圍,請求范圍,會話范圍,程序范圍)那就可能存儲在下面的對象中: 1. 靜態變量 2. 長生命周期的類變量 每個類的靜態變量被JVM(JAVA虛擬機)所控制,他們存在與否和類是否已經被初始化無關。一個類的所有實例共用一個存儲靜態變量的地方,因此在任何一個實例中修改靜態變量會影響這個類的其他實例。因此,如果一個程序在靜態變量中存放了一個對象,如果這個變量生命周期沒有到,那么這個對象就不會被JVM釋放。這些靜態對象是造成內存泄漏的主要原因。 最后,對象能夠被放到內部數據類型或者長生命周期類中的成員變量中,例如SERVLET。當一個SERVLET被創建并且被裝載到內存,它在內存中僅有一個實例,采用多線程去訪問這個SERVLET實例。如果在INIT()方法中裝載配置信息,將他存儲于類變量中,那么當需要維護的時候就可以隨時讀出這些信息,這樣所有的對象就用相同的配置。我常碰到的一個問題就是利用SERVLET類變量去存儲象頁面緩存這樣的信息。在他們自己內部本身存貯這些緩存配置是個不錯的選擇,但存貯在SERVLET中是最糟糕的情況。如果你需要使用緩存,你最好使用第三方控制插件,例如 TANGOSOL的COHERENCE。 當在頁面或者請求范圍中利用變量存放對象的時候,在他們結束的時候這些對象會自動釋放。同樣,在SESSION中存放對象的時候,當程序明確說明此SESSION失效的或者會話執行超時的時候,這些對象才會自動被釋放。 很多看起來象內存泄漏的情況都是上面的那些會話中的泄漏。一個造成泄漏的會話并不是泄漏了內存而是類似于泄漏,它消耗了內存,但最終這些內存都會被釋放的。如果程序服務器發生內存溢出,判斷是內存泄漏還是內存缺乏的最好的方法就是:停止所有向這個服務器所發的請求的對象,等待會話超時,看內存時候會被釋放出來。這雖然不會一定能夠達到你要的目的,但是這是最好的分段處理方法,當你裝載測試器的時候,你應該先掛斷你內容巨大的會話而不是先去尋找內存泄漏。 通常來說,如果你執行了一個很大的會話,你應該盡量去減少它所占用的內存空間,如果可以的話最好能重構程序,以減少session所占據的內存空間。下面2種方法可以降低大會話和內存的沖突: 1. 增大堆的空間以支持你的大會話 2. 縮短會話的超時時間,讓它能夠快速的失效 一個巨大的堆會導致垃圾回收花費更多的時間,因此這不是一個好解決方法,但總比發生OutofMemoryError強。增加足夠的堆空間以使它能夠存儲所有應該保存的有效值,也意味著你必須有足夠的內存去存儲所有訪問你站點的用戶的有效會話。如果商業規則允許的話最好能縮短會話超時的時間,以減少堆占用空間的沖突。 總結下,你應該依據合理性和重要性按下面的步驟依次去執行: 1. 重構程序,盡量減少擁有session范圍的變量所存儲的信息量 2. 鼓勵你的客戶在他們使用完后,明確的釋放會話 3. 縮短超時的時間,以便于讓你內存盡快的得到回收 4. 增加你堆空間的大小 無論如何,不要讓程序范圍級的變量,靜態變量,長生命周期的類存儲對象,事實上,你需要在內存模擬器中去分析泄漏。 異常的持久空間 容易誤解JVM為持久空間分配內存的目的。堆僅僅存儲類的實例,但JVM在堆中創建類實例之前,它必須把字節流文件(.class文件)裝載到程序內存中。它利用內存中的字節流在堆中創建類的實例。JVM利用程序的內存來裝載字節流文件,這個內存空間稱為持久空間。圖6顯示了持久空間和堆的關系:它存在于JVM程序中,并不是堆的一部分。 Figure 6. The relationship between the permanent space and the heap 通常,你可能想讓你的持久空間足夠大以便于它能夠裝載你程序所有的類,因為很明顯,從文件系統中讀取類文件比從內存中裝載代價高很多。JVM提供了一個參數讓你不的程序不卸載已經裝載到持久空間中的類文件: –noclassgc 這個參數選項告訴JVM不要跑到持久空間去執行垃圾收集釋放其中已經裝載的類文件。這個參數選項很聰明,但是會引起一個問題:當持久空間滿了以后依然需要裝載新文件的時候JVM會怎么處理呢?我觀測到的資料說明:如果JVM檢測到持久空間還需要內存,就會調用主垃圾收集程序。垃圾收集器清除堆,但它并不會對持久空間進行任何操作,因此它的努力是白費的。于是JVM就再重新檢測持久空間,看它是否滿,然后再次執行程序,一遍的一遍重復。 我第一次碰到這種問題的時候,用戶抱怨說程序性能很差勁,并且在運行了幾次后就出現了問題,可能是內存溢出問題。在我調查了詳細的關于堆和程序內存利用的收集器的記錄后,我迅速發覺堆的狀態非常正常,但程序確發生了內存溢出。這個用戶維持了數千的JSP頁面,在裝載到內存前把他們都編譯成了字節流文件放入持久空間。他的環境已經造成了持久空間溢出,但是在堆中由于用了 -noclassgc 選項,于是JVM并不去釋放類文件來裝載新的類文件。于是就導致了內存溢出錯誤,我把他的持久空間改為512M大小,并去掉了 -noclassgc 參數。 正像圖7顯示的,當持久空間變滿了的時候,就引發垃圾收集,清理了樂園和幸存者空間,但是并不釋放持久空間中的一點內存。 Figure 7. Garbage collection behavior when the permanent space becomes full. Click on thumbnail to view full-sized image. 注意 當設置持久空間大小時候,一般考慮128M,除非你的程序有很多的類文件,這個時候,你就可以考慮使用256M大小。如果你想讓他能夠裝載所有的類的時候,就會導致一個典型的結構錯誤。設置成512M就足夠了,它僅僅是暫時的時間的花費。把持久空間設置成512M大小就象給一個腳痛的人吃止痛藥,雖然暫時緩解了痛,但是腳還是沒有好,依然需要醫生把痛治療好,否則只是把問題延遲了而已。 線程池 外界同WEB或程序服務器連接的主要方法就是向他們發送請求,這些請求被放置到程序的執行次序隊列中。和內存最大的沖突就是程序服務器所設置的線程池的大小。線程池的大小就是程序可以同時處理的請求的數量。如果池太小,請求就需要在隊列中等待程序處理,如果太大,CPU就需要花費太多的時間在這些眾多的線程之間來回的切換。 每個服務器都有一個SOCKET負責監聽。程序把接受到的請求放到待執行隊列中,然后將這個請求從隊列移動到線程中被程序處理。 圖8顯示了服務器的處理程序。 Figure 8. 服務器處理請求的次序結構 線程池太小 每當我碰到有人抱怨裝載速度的性能隨著裝載的數量的增加變的越來越糟糕的時候,我會首先檢查線程池。特別是,我在看到下面這些信息的時候: 1.線程池的使用 2.很多請求等待處理(在隊列中等待處理) 當一個線程池被待處理的請求裝滿的時候,響應的時間就變的極其糟糕,因為這些在隊列中等待處理的請求會消耗很多的額外時間。這個時候,CPU的利用率會非常低,因為程序服務器沒有時間去指揮CPU工作。這個時候,我會按一定幅度增加調節池的大小,并在未處理請求的數量減少前一直監視程序的吞吐量,你需要一個合理甚至更好的負載量者,一個精確的負載量測試工具可以準確的幫你測試出結果。當你觀測吞吐量的時候,如果你發現吞吐量降低了,你就應該把池的大小下調一個幅度,一直到找到讓它保持最大吞吐量的大小為止。 圖9顯示了連接池太小的情況 Figure 9. 所有的線程都被占用了,請求就只能在隊列中等待 每當我閱讀性能調整手冊的時候,最讓我頭疼的就是他們從來不告訴你特殊情況下線程池應該是多大。由于這些值非常依賴程序的行為,他們只告訴你大普通情況下正確的大小,但是他們給了你一個范圍內的值,這對用戶很有利的。例如考慮下面2種情況:: 1. 一個程序從內存中讀出一個字符串,把它傳給JSP頁面,讓JSP頁面去顯示 2. 另一個程序從數據庫中讀出1000個數值,為這些不規則的數值求平均。第一個程序對請求的回應會很塊,大概僅需要不足0.25秒的時間,且不怎么占據CPU。第二個程序可能需要3秒去回應,同時會占據CPU。因此,為第一個程序配置的池大小是100就有點太小了,因為程序能夠同時處理200個;但為第二個程序配置的池是100,就有點太大了,因為CPU可能就能應付50個線程。 但是,很多程序并沒有在這種情況下動態的去調整的功能。多數情況下是做相同的事,但是應該為他們劃分范圍。因此,我建議你為一個CPU分配50到75個左右的線程。對一些程序來說,這個數量可能太少,對另一個些來說可能太多,我剛開始為每個CPU分配50到75個線程,然后根據吞吐量和CPU的性能,并做適當的調整。 線程池太大 除了線程池數量太小之外的情況外,環境也可能把線程數量配置的過大。當這些環境中的負載量不斷增大的時候,CPU的使用率會持續無法降低,就沒有什么響應請求的時間了,因為CPU只顧的在眾多的線程之間來回的切換跳動,沒時間讓線程去做他們應該做的事了。 連接池過大的最主要的跡象就是CPU的使用率一直很高。有些時候,垃圾收集也可能導致CPU使用率很高,但是垃圾收集導致的CPU使用率很高和池過大導致的使用率有一個主要的區別就是:垃圾收集引起的只是短時間的高使用率就象個釘子,而池過大導致的就是一直持續很高呈線性。 這個情況發生的時候,請求會被放在隊列中不被處理,但是不會始終如此,因為請求占用CPU的情況和程序占用的情況造成的后果不同。降低線程池的大小可能會讓請求等待,但是讓請求等待總比為了處理請求而讓CPU忙不過來的好。讓CPU保持持續的高使用率,同時性能不降低,新請求到來的時候放入到隊列中,這是最理想的程序。考慮下面這個很類似的情況:很多高速公里有交通燈來保證車輛進入到擁擠的公里中。在我看來,這些交通燈根本沒用,道理很充分。比如你來了,在交通燈后面的安全線上等待進入到高速公路上。如果所有的車輛都同時涌向公里,我們就動彈不得,但是只要減緩涌向高速公路車輛的速度,交通遲早會暢通。事實上,很多的大城市都有這樣功能,但根本沒用,他們真正需要的是一些更多的小路(CPU),涌向高速公路的速度真的降低了,那么交通會變的正常起來。 設置一個飽和的池,然后逐步減少連接池大小,一直到CPU占用率為75%到85%之間,同時用戶負載正常。如果等待隊列大小實在無法控制,考慮下面2中建議: 1.把你的程序放入代碼模擬器運行,調整程序代碼 2.增加額外的硬件 如果你的用戶負載超過了環境能承受的范圍,你應該考慮修正代碼減少和CPU的沖突或者增加CPU。 JDBC連接池 很多JAVA EE 程序連接到一個后臺數據源,大多數是通過JDBC(JAVA DATABASE CONNECTIVITY)將程序和后臺連接起來。由于創建數據庫連接的代價很高,程序服務器讓在同一個程序服務器實例下的所有程序共享特定數量的一些連接。如果一個請求需要連接到數據庫,但是數據庫的連接池無法為這個請求創建一個新連接,這個時候請求就會停下來等待連接池完成自己的操作再給她分配一個連接。反過來,如果數據庫連接池太大程序服務器就會浪費資源,并且程序有可能強迫數據庫承受過量的負荷。我們調試的目的就是盡量減少請求的等待時間和飽和的資源之間之間的沖突,讓一個請求在數據庫外等待要比強迫數據庫好的多。 一個程序服務器如果設置連接的數量不合理就會有下面這些特征: 1.程序運行速度緩慢 2.CPU使用率低 3.數據庫連接池使用率非常高 4.線程等待數據庫的連接 5.線程使用率很高 6.請求隊列中有待處理的請求(潛在的) 7.數據庫CPU使用率很低(因為沒有足夠的請求能夠讓他繁忙起來) JDBC prepared statements 和JDBC相關的另一個重要的設置就是:為JDBC使用的statement 所預設的緩存的大小。當你的程序在數據庫中運行SQL statement 的時候三下面3個步驟進行: 1.準備 2.執行 3.返回數值 在準備階段,數據庫驅動器讓數據庫完成隊列中的執行計劃。執行的時候,數據庫執行語句并返回指向結果的引用。在返回的時候,程序重新描述這些結果并描述出這些被請求的信息。 數據庫驅動會這樣優化程序:首先,你需要去準備一個statement ,這個statement 它會讓數據庫做好執行和緩存結果的準備。在此同時,數據庫驅動會從緩存中裝載已經準備好的statement ,而不用直接連接到數據庫。 如果prepared statement 設置太小,數據庫驅動器會被迫去查詢沒有裝載進緩存區的statement ,這就會增加額外的連接到數據庫的時間。prepared statement 緩存區設置不恰當最主要的癥狀就是花費大量的時間去連接相同的statement。這段被浪費的時間本來是為了讓它去裝載后面的調用的。 事情變的稍微復雜了點,緩存prepared statement 是每個statement的基礎,就是說在一個statement連接之前都應當緩存起來。這個增加的復雜性就產生了一個沖突:如果你有100個prepared statement需要去緩存,但你的連接池中有50個數據庫連接,這個時候你就需要有存放5000條預備語句的內存。 通過跟蹤性能,確定出你程序所執行的不重復的statement 的數量,并從這些statement 中找出哪些條是頻繁執行的。 Entity bean(實體BEAN)和stateful session bean的緩沖 無狀態(stateless)對象可以被放入到池中共享,但象Entity beans和 stateful session bean這樣的有狀態的對象就需要被緩存,因為這些bean的每個實例都是不相同的。當你需要一個有狀態對象時,你需要明確創建這個對象的特定實例,普通的實例是不能滿足的。類似的,你考慮一個超市類似的情況,你需要個售貨員但他叫什么并不重要,任何售貨員都可以滿足你。也就是,售貨員被放入池中共享,因為你只需要是售貨員就可以,而不是一個叫做史締夫的這個售貨員。當你離開超市的時候,你需要帶上你的孩子,不是其他人的孩子,而是你自己的。這個時候,孩子就需要被緩存。 Figure 10. The application requests an object from the cache that is in the cache, so a reference to that object is returned without making a network trip to the database 當你的緩存區太小的時候,緩存的性能就會明顯的受到影響。特別是,當一個請求去一個已經滿了的緩存區域去請求一個對象的時候,下面的步驟就會執行,這些步驟會在圖11中顯示: 1. 程序請求一個對象 2. 緩存檢測這個對象是否已經存在于緩存中 3. 緩存決定把一個對象開除出緩存(一般采用的算法是遺棄最近使用次數最少的對象) 4. 把這個對象扔出緩存(稱為passivated) 5. 把從數據庫中裝載這個新對象并放入到緩存(稱為activated) 6. 把指向這個對象的引用返回給程序 Figure 11. Because the requested object is not in the cache, an object must be selected for removal from the cache and removed from it. 如果多數的請求都需要執行這些步驟的話,那你采用緩存技術就不是好的選擇了!如果這些處理步驟頻繁發生的話,你就需要重新推敲下你的緩存了。回憶一下:從緩存中去除一個對象稱為passivation,從持久存儲區取出一個對象放入緩存稱為activation。能在緩存中找到的請求(緩存中有此請求的對象)的百分率稱為hit ratio,相反找不到的請求的百分率稱為miss ratio。 緩存剛被初始化的時候,hit ratio是0,它的activation數量非常高,因此在初始化后你需要去觀察緩存的性能。初始化以后,你應該跟蹤passivation的數量并把它和與向緩存請求對象的請求的總量相比較,因為passivations只會發生在緩存被初始化以后。但一般來說,我們更需要關心緩存的miss ratio。如果miss ratio超過25%,那么緩存可能是太小了。因此,如果missratio的數量超過75%,那么不是你的緩存設置的太小就是你不需要緩存這個技術。 一旦你覺得你的緩存太小,就去嘗試著增大大小,并測試增加的性能。如果miss ration下降到20%以下,那你的緩存的大小就非常棒了,如果沒有什么效果,那么你就需要和這個程序的技術員聯系,看是這個對象是不是需要緩存或者是否應該修正程序中這個對象的代碼。 Staless session bean和message-driven bean池 Stateless session bean 和message-driven bean 在商業應用方面很重要,不要期望它們會保持自己特有的狀態信息。當你的程序需要使用這些BEAN的商業功能的時候,它就從一個池中取出一個BEAN實例,用這個實例來調用一個個方法,用完后再將BEAN的實例再放回到池中。如果你的程序過了一會又需要這個一摸一樣的BEAN,就從池中再得到一個實例,但不能保證你得到的就是上一個實例。池能夠讓程序共享資源,但是會讓你的程序付出潛在的等待時間。如果你無法從池中得到想要的BEAN,請求就會等待,一直到這個BEAN被放入到池中。很多程序服務器都會把這些池調整的很好,但是我碰到過因為在環境中把他們設置的太小而引發的不少麻煩。Stateless bean池的大小應該和可執行線程池的大小一般大,因為一個線程同時只能使用一個對象,再多了就造成浪費的。因此,一些程序服務器把池的大小和線程的數量設置成同樣的數量。為了保險起見,你應該親自把它設置成這個數。 事務 使用Enterprise Java的一個好處就是它天生就支持事務。通過JAVAEE 5 EJB(Enterprise javaBeans)的注釋,你可以控制事務中方法的使用。事務會以下面2中方式結束: 1. 事務提交 2. 事務回滾 當一個事務被提交的時候,說明它已經完全成功了,但是當它回滾的時候,就說明發生了一些錯誤。回滾會是下面2種情況: 1. 程序造成的回滾(程序回滾) 2. 非程序造成的回滾(非程序回滾) 通常,程序回滾是因為商業的規定。比如一個WEB程序做一個素描畫的價格的調查,程序可能讓用戶輸入年齡,并且商業規定18歲以上才可以進入。如果一個16歲的提交了信息,那么程序就會拋出一個錯誤,打開一個網頁告訴他,他年齡還不能參與到這個信息的調查。因為程序拋出了異常,因此包含在程序中的事務的就會發生回滾。這只是普通的程序回滾,只有當發生大量的程序回滾才值得我們注意。 另一方面,非程序回滾是非常糟糕的。有三種情形的非程序回滾: 1. 系統回滾 2. 超時回滾 3. 資源回滾 系統回滾意味著程序服務器中的一些東西非常的糟糕,恢復的幾率很渺茫。超時回滾就是當程序服務器中的程序處理請求時超時;除非你把超時設置的很短才會出現這種錯誤。資源回滾就是當一個程序服務器管理內部的資源的時候發生錯誤。例如,如果你設置你的程序服務器通過一個簡單的SQL語句去測試數據庫的連接,但數據庫對于程序服務器來說是無法連接的,這個時候任何和這個資源相關的事情都會發生資源回滾。 如果發生非程序回滾,我們應該立刻注意,這個是不小的問題,但是你也需要留意程序回滾發生的頻率。很多時候人們對發生的異常很敏感,因此你需要哪些異常對你程序來說才是重要的。 總結 盡管各個程序和他們的環境都各不相同,但是有一些共同的問題困擾著他們。這篇文章的注意力并不是放在程序代碼的問題上,因為把注意力放在因為環境的問題而導致的低性能的問題上: 1.內存溢出 2.線程池大小 3.JDBC連接池大小 4.JDBC預先聲明語句緩存大小 5.緩存大小 6.池大小 7.執行事務時候的回滾 為了有效的診斷性能的問題,你應該了解什么問題會導致什么樣的癥狀。如果主要是程序的代碼導致的惡果那你應該帶著問題去尋求負責代碼的人尋求幫助,但是如果問題是由環境引起的,那么就要依靠你的操作來解決了。 問題的根源依賴于很多要素,但是一些指示器可以增加一些你處理問題時候的一些信心,依靠他們可以完全排除一些其他的原因。我希望這個文章能對你排解JAVAEE環境問題起到幫助。
轉自