標籤:rdl 服務端 也有 copy java應用 自己的 架構 不同 模型
在Java中,記憶體泄露和其它記憶體相關問題在效能和可擴充性方面表現的最為突出。我們有充分的理由去具體地討論他們。
Java記憶體模型——或者更確切的說記憶體回收行程——已經攻克了很多記憶體問題。
然而同一時候,也帶來了新的問題。特別是在有著大量並行使用者的J2EE運行環境下,記憶體越來越成為一種至關重要的資源。
乍看之下。這似乎有些奇怪,因為當前記憶體已經足夠便宜,而且我們也有了64位的JVM和更先進的記憶體回收演算法。
接下來。我們將會細緻的討論一下關於Java記憶體的問題。這些問題可以分為四組:
在大多數情況下,記憶體問題不僅影響效能,還會影響可擴充性。每次請求消耗的記憶體數量越高,使用者或Session可以啟動並執行並行事務就越少。
在某些情況下記憶體問題也影響可用性。當JVM耗盡了記憶體或者即將接近記憶體極限。這個時候它將退出並報OutOfMemory錯誤。這時經理會來到你的辦公室,你就知道自己攤上大事了。
記憶體問題非常難被解決通常有兩個原因: 第一,某些情況下分析非常複雜,也非常困難。特別是假設你缺少正確的方法來解決他們;其次,他們一般是應用程式的架構基礎。簡單的代碼更改不會協助解決他們。
為了使開發過程更easy。我會展示一些實際應用中常被使用的反模式。這些模式已經可以在開發過程中避免記憶體問題。
HTTPSession作為緩衝
此反模式是指濫用HTTPSession對象作為資料緩衝。session對象的存在是為了儲存資訊,這個資訊裡面存在著一個HTTP請求。這也稱為一個Session狀態。這意味著,資料將被儲存直至它們被處理。這些方法通常存在於一些重要的web應用程式中。web應用程式除了在server上儲存這些資訊外。沒有別的方法。
然而,一些資訊是可以儲存在cookie中,可是這將會帶來一些其它的影響。
在cookie中,儘可能地保持少而短的資料,這是非常重要的。
有時候非常easy發生這樣的現象,session裡儲存著成MB的資料對象。這將會馬上導致堆棧高佔用和記憶體短缺。同一時候並行使用者的數量非常有限,JVM將應對越來越多出現OutOfMemoryError錯誤的使用者。多數使用者Session也有其它效能損失。
叢集情境的session複製中,這將會添加序列化和溝通工作將導致額外的效能和延展性問題。
在某些項目中這些問題的解決方式是添加數量的記憶體和切換到64位jvm。他們無法抵抗住僅僅添加幾個G大小的堆棧記憶體的誘惑。然而,與其提供一個對真正問題的解決方式,不如隱藏這個現象。這個“解決方式”僅僅是暫時的,同一時候還會引入了一個新的問題。越來越大的堆記憶體使它更難以找到“真正的”記憶體問題。對這樣的非常大的堆(大約6G)來說,大部分可用的分析工具是無法處理這些記憶體垃圾。我們在dynaTrace投入了大量的研發工作希望可以有效地分析大量的記憶體垃圾。隨著這個問題變得越來越重要,一種新的JSR規範也提到了它。
因為應用程式架構尚未明白,導致Session緩衝問題經常出現,。在開發過程中,資料被輕鬆而又簡單的放入session其中。這是經常發生的。相似於一種“add and forget”方式。即沒有人可以確保當這樣的資料不再須要時是被移除的。通常,當session逾時時不須要的session資料應該被處理。在企業中,一些應用程式經常大量使用Session逾時,這將會導致無法正常工作。
此外經常使用非常高的Session逾時- 24小時為使用者提供額外的“體驗”,使他們不必再次登入。
舉一個實際的範例。從session裡的資料庫列表中選擇所須要的資料。其目的是為了避免不必要的資料庫查詢。
(是不是覺得有點過早最佳化呢?)。這將導致在session對象中為每一個單獨的使用者放入幾千個位元組。儘管。緩衝這些資訊它是合理的。但使用者session可以肯定是一個錯誤的地方。
另外一個範例是,為了管理Session狀態而濫用Hibernate session。Hibernatesession對象僅僅是為了高速訪問資料庫而放入HTTPsession對象中。然而。這將導致很多其它必要的資料被儲存。
同一時候每一個使用者的記憶體佔用也將顯著提高。
現如今,AJAX應用程式Session狀態也可以在client進行管理。這使服務端程式變成無狀態的,或接近無狀態的,同一時候也顯然有著更好的可擴充性。
執行緒區域變數記憶體泄露
在Java中使用ThreadLocal變數是為了在一個特定的線程中綁定變數。這意味著每一個線程都有它自己的單獨執行個體。這樣的方法一般在一個線程中用於處理狀態資訊,比如使用者授權。
然而,一個ThreadLocal變數的生命週期與另外一個線程的生命週期是息息相關的。被遺忘的ThreadLocal變數非常easy導致記憶體問題,尤其是在應用server中。
假設忘記了設定ThreadLocal變數,尤其是在應用server中,這非常easy導致記憶體問題。應用server利用線程池避免常量不斷建立和線程銷毀。舉個範例,一個HTTPServletRequest類在運行時得到一個空暇的已指派的線程。在運行完後將它回傳到線程池中。假設應用程式邏輯使用ThreadLocal變數和忘記了顯式地移除它們,這時,記憶體是不會被釋放的。
依據線程池大小——在程式系統中這些線程池可以是幾百個線程。同一時候。由ThreadLocal變數引用的對象的大小,這可能導致一些問題。比如。在最壞的情況下,一個200個線程的線程池和一個5M大小的線程池將會導致1 GB的不必要的記憶體佔用。這將馬上導致強烈的記憶體回收反應,同一時候導致糟糕的回應時間和潛在的OutOfMemoryError錯誤。
一個實際的範例就是在JBossWS 1.2.0版本號碼中出現的一個bug(在JBossWS1.2.1版本號碼已經被修複)——“DOMUtils doesn’t clear thread locals”。此問題就是ThreadLocal變數導致的,它引用了一個14MB的解析文檔。
大型暫時對象
大型暫時對象在最壞的情況下也能導致outofmemoryerror錯誤或者至少強烈的GC反應。比如,假設非常大的文檔(XML、PDF、圖片…)必須閱讀和處理時。在一個特定的情況下。應用程式幾分鐘都沒有響應或效能非常有限,差點兒沒有可用的。其中根本原因是記憶體回收反應過於強烈。以下對讀取PDF文檔的一段代碼作了具體分析:
byte tmpData[] = new byte [1024];int offs = 0;do{int readLen = bis.read (tmpData, offs, tmpData.length - offs);if (readLen == -1)break;offs+= readLen;if (oofs == tmpData.length){byte newres[] = new byte[tmpData.length + 1024];System.arraycopy(tmpData, 0, newres, 0, tmpData.length);tmpData = newres;}} while (true);
這些文檔採用按固定位元組數的方式來讀取。首先,他們被讀入中位元組數組中,然後發送到使用者的瀏覽器中。然而僅僅幾個並行請求將會導致堆溢出。因為讀取文檔採用了極其低效的演算法,這將導致問題越來越糟糕。最初的想法僅僅是建立1KB的初始位元組數組。假設這個數組滿了。則一個新的1KB數組將被建立,同一時候這個老的數組將複製到新的數組中。
這意味著當讀取文檔時,一個新數組將被建立,同一時候將讀取的每位元組都複製到新數組中。
這將導致大量的暫時對象和兩倍於實際資料大小的記憶體消耗——資料將永久被複製。
在處理大量資料時,最佳化處理邏輯效能是至關重要的。在這樣的情況下,一個簡單的負載測試會顯示這一問題。
糟糕的記憶體回收行程配置
到眼下為止。在所提到的情境中出現的問題基本都是由應用程式代碼所導致的。然而,這些原因的根源是因為記憶體回收行程配置錯誤。或者丟失。我經常看到使用者相信他們的應用程式server的預設設定。同一時候也相信應用server的開發人員瞭解哪些是自己的程式最好的。
不管怎樣,堆的配置非常大程度上取決於應用程式和實際使用情境。
依據情境來調整參數,應用程式才幹更好地運行。和一批運行長期任務的應用程式相比,一個運行大量短而持久的應用程式配置起來是全然不同的。此外。實際的配置還取決於JVM使用方式。對IBM來說,什麼才幹使Sun Jvm正常運行可能是一場噩夢(或至少是不理想的)。配置錯誤的垃圾收集器通常不會馬上被確覺得效能問題的根源(除非你監控了垃圾收集器的活動)。通常我們肉眼可見的問題都是響應過慢。同一時候,理解記憶體回收活動與回應時間的關係也是不明顯的。假設記憶體回收的時間與回應時間沒什麼關聯,人們一般會發現一個非常複雜的效能問題。回應時間和已耗用時間度量問題主要體如今應用程式——對於這樣的現象。在不同的地方都沒有一個明顯的模式。
顯示了事務指標與垃圾收集時間在dynaTrace中的關係。我發現了一些情況,關於記憶體回收行程的最佳化問題。
人們正打算花幾周的時間去解決怎樣在幾分鐘內設定解決效能問題。
類載入器記憶體泄露
在談到記憶體流失時,大部分人主要覺得是堆中的對象。
除了對象,類和常量也是託管在堆中。
依據JVM。它們被放入堆中特定的地區。比如Sun JVM使用所謂的永久代或PermGen。通常情況下,類被放入堆中好幾次。僅僅是因為他們已經被不同的類載入器載入。在現代化企業級應用程式中,載入類的記憶體佔用可以達到幾百MB。
關鍵是避免無謂地添加類的大小。一個非常好的範例是大量字串常量的定義——比如在GUI應用程式中。這裡全部的文本通常儲存在常量。而使用常量字串的方法原則上是一個好的設計方法,記憶體消耗不應該被忽視。在真實的情況下,在一個國際化應用程式中,全部常量都會被定義為各種語言。一個非常不起眼的代碼錯誤都會影響到已經被載入的類。終於的結果是。在應用程式的永久代中,JVM將出現OutOfMemoryError 錯誤。同一時候崩潰。
應用server還面臨著類載入器泄漏的問題。這些泄漏的原因主要是因為類載入器不能被記憶體回收,因為類載入器中的類的一個對象仍然活著。
結果,這些類並不打算釋放這些記憶體佔用。而如今。這個問題已經被J2EE 應用程式server非常好的攻克了,它似乎更常出如今OSGI-based應用程式環境。
總結
在Java應用程式中記憶體問題一般是多方面的,這easy導致效能和可擴充性的問題。特別是在有著大量並行使用者的J2EE應用程式中,記憶體管理必須是應用程式體繫結構的核心部分。
然而記憶體回收行程對於那些未使用的對象是否被清理並不關心。所以開發人員還是須要適當的記憶體管理。此外,應用程式記憶體管理設計是應用程式配置的核心部分。
Java記憶體問題的一些見解