1. 程式人生 > >Java內存問題的一些見解

Java內存問題的一些見解

pda 垃圾回收器 copy 裏的 pre pdf文檔 研發 活動 事務

在Java中,內存泄露和其它內存相關問題在性能和可擴展性方面表現的最為突出。我們有充分的理由去具體地討論他們。

Java內存模型——或者更確切的說垃圾回收器——已經攻克了很多內存問題。

然而同一時候,也帶來了新的問題。特別是在有著大量並行用戶的J2EE運行環境下,內存越來越成為一種至關重要的資源。

乍看之下。這似乎有些奇怪,因為當前內存已經足夠便宜,而且我們也有了64位的JVM和更先進的垃圾回收算法。

接下來。我們將會細致的討論一下關於Java內存的問題。這些問題可以分為四組:

  • 在Java中,內存泄露一般都是因為引用對象不再被使用而造成的。

    當有多個引用的對象。同一時候這些對象又不再須要,然而開發人員又忘記清理它們,這時極easy導致內存泄露的發生。

  • 運行消耗太多的內存而導致不必要的高內存占用。這在為了用戶體驗而管理大量狀態信息的 Web 應用中非經常見。

    隨著活躍用戶數量的添加。內存非常快到達了上限。未綁定或低效緩存配置是持續高內存占用的還有一來源。

  • 當用戶負載添加時。低效的對象創建easy導致性能問題。從而垃圾回收器必須不斷地清理堆內存。而這導致了垃圾回收器對CPU產生了不必要的高占用。隨著CPU因垃圾回收而被堵塞,應用程序響應時間頻繁的添加。導致其一直處於中等負載之下。

    這樣的行為也成為“GC trashing”。

  • 低效的垃圾回收行為往往是因為垃圾回收器的缺失或者錯誤的配置。這些垃圾回收器將會時刻追蹤對象是否被清理。然而這樣的行為怎樣以及何時發生必須由配置或者程序猿
    ,或者系統架構師決定的。通常,人們僅僅是簡單地“忘記”了正確的配置和優化垃圾回收器。我曾參加過一些關於“性能”的專題討論會。發現一個簡單的參數變化將會導致高達25%的性能提升。

在大多數情況下,內存問題不僅影響性能,還會影響可擴展性。每次請求消耗的內存數量越高,用戶或Session可以運行的並行事務就越少。

在某些情況下內存問題也影響可用性。當JVM耗盡了內存或者即將接近內存極限。這個時候它將退出並報OutOfMemory錯誤。這時經理會來到你的辦公室,你就知道自己攤上大事了。

內存問題非常難被解決通常有兩個原因: 第一,某些情況下分析非常復雜,也非常困難。特別是假設你缺少正確的方法來解決他們;其次,他們一般是應用程序的架構基礎。簡單的代碼更改不會幫助解決他們。

為了使開發過程更easy。我會展示一些實際應用中常被使用的反模式。這些模式已經可以在開發過程中避免內存問題。

HTTPSession作為緩存

此反模式是指濫用HTTPSession對象作為數據緩存。session對象的存在是為了存儲信息,這個信息裏面存在著一個HTTP請求。這也稱為一個Session狀態。這意味著,數據將被保存直至它們被處理。這些方法通常存在於一些重要的web應用程序中。web應用程序除了在server上存儲這些信息外。沒有別的方法。

然而,一些信息是可以存儲在cookie中,可是這將會帶來一些其它的影響。

在cookie中,盡可能地保持少而短的數據,這是非常重要的。

有時候非常easy發生這樣的現象,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狀態也可以在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內存問題的一些見解