在java中,記憶體洩露和其他記憶體相關問題在性能和可擴充性方面表現的最為突出。我們有充分的理由去詳細地讨論他們。
java記憶體模型——或者更确切的說垃圾回收器——已經解決了許多記憶體問題。然而同時,也帶來了新的問題。特别是在有着大量并行使用者的j2ee運作
環境下,記憶體越來越成為一種至關重要的資源。乍看之下,這似乎有些奇怪,因為目前記憶體已經足夠廉價,并且我們也有了64位的jvm和更先進的垃圾回收算
法。
接下來,我們将會仔細的讨論一下關于java記憶體的問題。這些問題可以分為四組:
在java中,記憶體洩露一般都是由于引用對象不再被使用而造成的。當有多個引用的對象,同時這些對象又不再需要,然而開發者又忘記清理它們,這時極容易導緻記憶體洩露的發生。
執行消耗太多的記憶體而導緻不必要的高記憶體占用。這在為了使用者體驗而管理大量狀态資訊的 web 應用中很常見。随着活躍使用者數量的增加,記憶體很快到達了上限。未綁定或低效緩存配置是持續高記憶體占用的另一來源。
當使用者負載增加時,低效的對象建立容易導緻性能問題。進而垃圾回收器必須不斷地清理堆記憶體。而這導緻了垃圾回收器對cpu産生了不必要的高占用。
随着cpu因垃圾回收而被阻塞,應用程式響應時間頻繁的增加,導緻其一直處于中等負載之下。這種行為也成為“gc trashing”。
在大多數情況下,記憶體問題不僅影響性能,還會影響可擴充性。每次請求消耗的記憶體數量越高,使用者或session可以執行的并行事務就越少。在某些情
況下記憶體問題也影響可用性。當jvm耗盡了記憶體或者即将接近記憶體極限,這個時候它将退出并報outofmemory錯誤。這時經理會來到你的辦公室,你就
知道自己攤上大事了。
記憶體問題很難被解決通常有兩個原因: 第一,某些情況下分析很複雜,也很困難,特别是如果你缺少正确的方法來解決他們;其次,他們通常是應用程式的架構基礎。簡單的代碼更改不會幫助解決他們。
為了使開發過程更容易,我會展示一些實際應用中常被使用的反模式。這些模式已經能夠在開發過程中避免記憶體問題。
httpsession作為緩存
此反模式是指濫用httpsession對象作為資料緩存。session對象的存在是為了存儲資訊,這個資訊裡面存在着一個http請求。這也稱
為一個session狀态。這意味着,資料将被儲存直至它們被處理。這些方法通常存在于一些重要的web應用程式中。web應用程式除了在伺服器上存儲這
些資訊外,沒有别的方法。然而,一些資訊是能夠存儲在cookie中,但是這将會帶來一些其他的影響。
在cookie中,盡可能地保持少而短的資料,這是非常重要的。有時候很容易發生這種現象,session裡存儲着成兆位元組的資料對象。這将會立即
導緻堆棧高占用和記憶體短缺。同時并行使用者的數量非常有限,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狀态也可以在用戶端進行管理。這使服務端程式變成無狀态的,或接近無狀态的,同時也顯然有着更好的可擴充性。
線程本地變量記憶體洩露
量是為了在一個特定的線程中綁定變量。這意味着每個線程都有它自己的單獨執行個體。這種方法一般在一個線程中用于處理狀态資訊,例如使用者授權。然而,一個
threadlocal變量的生命周期與另外一個線程的生命周期是息息相關的。被遺忘的threadlocal變量很容易導緻記憶體問題,尤其是在應用服務
器中。
如果忘記了設定threadlocal變量,尤其是在應用伺服器中,這很容易導緻記憶體問題。應用伺服器利用線程池避免常量不斷建立和線程銷毀。舉個
例子,一個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數組将被建立,同時這
個老的數組将拷貝到新的數組中。這意味着當讀取文檔時,一個新數組将被建立,同時将讀取的每位元組都複制到新數組中。這将導緻大量的臨時對象和兩倍于實際數
據大小的記憶體消耗——資料将永久被複制。
在處理大量資料時,優化處理邏輯性能是至關重要的。在這種情況下,一個簡單的負載測試會顯示這一問題。
糟糕的垃圾回收器配置
到目前為止,在所提到的情境中出現的問題基本都是由應用程式代碼所導緻的。然而,這些原因的根源是由于垃圾回收器配置錯誤,或者丢失。我常常看到用
戶相信他們的應用程式伺服器的預設設定,同時也相信應用伺服器的開發者了解哪些是自己的程式最好的。無論如何,堆的配置很大程度上取決于應用程式和實際使
用場景。根據場景來調整參數,應用程式才能更好地執行。和一批執行長期任務的應用程式相比,一個執行大量短而持久的應用程式配置起來是完全不同的。此外,
實際的配置還取決于jvm使用情況。對ibm來說,什麼才能使sun jvm正常運作可能是一場噩夢(或至少是不理想的)。配置錯誤的垃圾收集器通常不會
立即被确認為性能問題的根源(除非你監控了垃圾收集器的活動)。通常我們肉眼可見的問題都是響應過慢。同時,了解垃圾回收活動與響應時間的關系也是不明顯
的。如果垃圾回收的時間與響應時間沒什麼關聯,人們通常會發現一個非常複雜的性能問題。響應時間和執行時間度量問題主要展現在應用程式——對于這種現象,
在不同的地方都沒有一個明顯的模式。
下圖顯示了事務名額與垃圾收集時間在dynatrace中的關系。我發現了一些情況,關于垃圾回收器的優化問題。人們正打算花幾周的時間去解決如何在幾分鐘内設定解決性能問題。
類加載器記憶體洩露
在談到記憶體洩漏時,大部分人主要認為是堆中的對象。除了對象,類和常量也是托管在堆中。根據jvm,它們被放入堆中特定的區域。例如sun jvm
使用所謂的永久代或permgen。通常情況下,類被放入堆中好幾次。僅僅是因為他們已經被不同的類加載器加載。在現代化企業級應用程式中,加載類的記憶體
占用能夠達到幾百mb。
關鍵是避免無謂地增加類的大小。一個很好的例子是大量字元串常量的定義——例如在gui應用程式中。這裡所有的文本通常存儲在常量。而使用常量字元
串的方法原則上是一個好的設計方法,記憶體消耗不應該被忽視。在真實的情況下,在一個國際化應用程式中,所有常量都會被定義為各種語言。一個很不起眼的代碼
錯誤都會影響到已經被加載的類。最終的結果是,在應用程式的永久代中,jvm将出現outofmemoryerror 錯誤,同時崩潰。
應用伺服器還面臨着類加載器洩漏的問題。這些洩漏的原因主要是因為類加載器不能被垃圾回收,因為類加載器中的類的一個對象仍然活着。結果,這些類并
不打算釋放這些記憶體占用。而現在,這個問題已經被j2ee 應用程式伺服器很好的解決了,它似乎更常出現在osgi-based應用程式環境。
總結
在java應用程式中記憶體問題通常是多方面的,這容易導緻性能和可擴充性的問題。特别是在有着大量并行使用者的j2ee應用程式中,記憶體管理必須是應
用程式體系結構的核心部分。然而垃圾回收器對于那些未使用的對象是否被清理并不關心,是以開發人員還是需要适當的記憶體管理。此外,應用程式記憶體管理設計是
應用程式配置的核心部分。
你的經驗
這些都是我在現實世界應用程式中發現的反模式。如果你有額外的反模式或共同的問題,我很願意更多地了解他們。
關于作者
其他感興趣的部落格
由于性能反模式是我的愛好,我将定期釋出關于反模式的文章。它們将會從這些文章裡選擇你們可能感興趣的一篇文章。
<a target="_blank" href="http://apmblog.dynatrace.com/tag/hibernate/">關于hibernate緩存的一些見解</a>
<a target="_blank" href="http://apmblog.dynatrace.com/2008/07/09/performance-antipattern-logical-structure-vs-physical-deployment/">遠端問題</a>
如果你想要了解更多關于如何解決像本文中提到的記憶體問題,和其他一些java運作壞境相關的問題,你也許對我同僚最近一本關于持續應用程式性能管理的白皮書感興趣。
來源:51cto