Java內(nèi)存問題的一些見解
在Java中,內(nèi)存泄露和其他內(nèi)存相關(guān)問題在性能和可擴(kuò)展性方面表現(xiàn)的最為突出。我們有充分的理由去詳細(xì)地討論他們。
Java內(nèi)存模型——或者更確切的說(shuō)垃圾回收器——已經(jīng)解決了許多內(nèi)存問題。然而同時(shí),也帶來(lái)了新的問題。特別是在有著大量并行用戶的J2EE運(yùn)行 環(huán)境下,內(nèi)存越來(lái)越成為一種至關(guān)重要的資源。乍看之下,這似乎有些奇怪,因?yàn)楫?dāng)前內(nèi)存已經(jīng)足夠廉價(jià),并且我們也有了64位的JVM和更先進(jìn)的垃圾回收算 法。
接下來(lái),我們將會(huì)仔細(xì)的討論一下關(guān)于Java內(nèi)存的問題。這些問題可以分為四組:
-
在Java中,內(nèi)存泄露一般都是由于引用對(duì)象不再被使用而造成的。當(dāng)有多個(gè)引用的對(duì)象,同時(shí)這些對(duì)象又不再需要,然而開發(fā)者又忘記清理它們,這時(shí)極容易導(dǎo)致內(nèi)存泄露的發(fā)生。
-
執(zhí)行消耗太多的內(nèi)存而導(dǎo)致不必要的高內(nèi)存占用。這在為了用戶體驗(yàn)而管理大量狀態(tài)信息的 Web 應(yīng)用中很常見。隨著活躍用戶數(shù)量的增加,內(nèi)存很快到達(dá)了上限。未綁定或低效緩存配置是持續(xù)高內(nèi)存占用的另一來(lái)源。
-
當(dāng)用戶負(fù)載增加時(shí),低效的對(duì)象創(chuàng)建容易導(dǎo)致性能問題。從而垃圾回收器必須不斷地清理堆內(nèi)存。而這導(dǎo)致了垃圾回收器對(duì)CPU產(chǎn)生了不必要的高占用。 隨著CPU因垃圾回收而被阻塞,應(yīng)用程序響應(yīng)時(shí)間頻繁的增加,導(dǎo)致其一直處于中等負(fù)載之下。這種行為也成為“GC trashing”。
-
低效的垃圾回收行為往往是由于垃圾回收器的缺失或者錯(cuò)誤的配置。這些垃圾回收器將會(huì)時(shí)刻追蹤對(duì)象是否被清理。然而這種行為如何以及何時(shí)發(fā)生必須由配置或者程序員,或者系統(tǒng)架構(gòu)師決定的。通常,人們只是簡(jiǎn)單地“忘記”了正確的配置和優(yōu)化垃圾回收器。我曾參加過一些關(guān)于“性能”的專題討論會(huì),發(fā)現(xiàn)一個(gè)簡(jiǎn)單的參數(shù)變化將會(huì)導(dǎo)致高達(dá)25%的性能提升。
在大多數(shù)情況下,內(nèi)存問題不僅影響性能,還會(huì)影響可擴(kuò)展性。每次請(qǐng)求消耗的內(nèi)存數(shù)量越高,用戶或Session可以執(zhí)行的并行事務(wù)就越少。在某些情 況下內(nèi)存問題也影響可用性。當(dāng)JVM耗盡了內(nèi)存或者即將接近內(nèi)存極限,這個(gè)時(shí)候它將退出并報(bào)OutOfMemory錯(cuò)誤。這時(shí)經(jīng)理會(huì)來(lái)到你的辦公室,你就 知道自己攤上大事了。
內(nèi)存問題很難被解決通常有兩個(gè)原因: 第一,某些情況下分析很復(fù)雜,也很困難,特別是如果你缺少正確的方法來(lái)解決他們;其次,他們通常是應(yīng)用程序的架構(gòu)基礎(chǔ)。簡(jiǎn)單的代碼更改不會(huì)幫助解決他們。
為了使開發(fā)過程更容易,我會(huì)展示一些實(shí)際應(yīng)用中常被使用的反模式。這些模式已經(jīng)能夠在開發(fā)過程中避免內(nèi)存問題。
HTTPSession作為緩存
此反模式是指濫用HTTPSession對(duì)象作為數(shù)據(jù)緩存。session對(duì)象的存在是為了存儲(chǔ)信息,這個(gè)信息里面存在著一個(gè)HTTP請(qǐng)求。這也稱 為一個(gè)Session狀態(tài)。這意味著,數(shù)據(jù)將被保存直至它們被處理。這些方法通常存在于一些重要的web應(yīng)用程序中。web應(yīng)用程序除了在服務(wù)器上存儲(chǔ)這 些信息外,沒有別的方法。然而,一些信息是能夠存儲(chǔ)在cookie中,但是這將會(huì)帶來(lái)一些其他的影響。
在cookie中,盡可能地保持少而短的數(shù)據(jù),這是非常重要的。有時(shí)候很容易發(fā)生這種現(xiàn)象,session里存儲(chǔ)著成兆字節(jié)的數(shù)據(jù)對(duì)象。這將會(huì)立即 導(dǎo)致堆棧高占用和內(nèi)存短缺。同時(shí)并行用戶的數(shù)量非常有限,JVM將應(yīng)對(duì)越來(lái)越多出現(xiàn)OutOfMemoryError錯(cuò)誤的用戶。多數(shù)用戶Session 也有其他性能損失。集群場(chǎng)景的session復(fù)制中,這將會(huì)增加序列化和溝通工作將導(dǎo)致額外的性能和可伸縮性問題。
在某些項(xiàng)目中這些問題的解決方案是增加數(shù)量的內(nèi)存和切換到64位jvm。他們無(wú)法抵抗住僅僅增加幾個(gè)G大小的堆棧內(nèi)存的誘惑。然而,與其提供一個(gè)對(duì) 真正問題的解決方案,不如隱藏這個(gè)現(xiàn)象。這個(gè)“解決方案”只是暫時(shí)的,同時(shí)還會(huì)引入了一個(gè)新的問題。越來(lái)越大的堆內(nèi)存使它更難以找到“真正的”內(nèi)存問題。 對(duì)這種非常大的堆(大約6G)來(lái)說(shuō),大部分可用的分析工具是無(wú)法處理這些內(nèi)存垃圾。我們?cè)赿ynaTrace投入了大量的研發(fā)工作希望能夠有效地分析大量 的內(nèi)存垃圾。隨著這個(gè)問題變得越來(lái)越重要,一種新的JSR規(guī)范也提到了它。
由于應(yīng)用程序架構(gòu)尚未明確,導(dǎo)致Session緩存問題經(jīng)常出現(xiàn),。在開發(fā)過程中,數(shù)據(jù)被輕松而又簡(jiǎn)單的放入session當(dāng)中。這是經(jīng)常發(fā)生的, 類似于一種“add and forget”方式,即沒有人能夠確保當(dāng)這種數(shù)據(jù)不再需要時(shí)是被移除的。通常,當(dāng)session超時(shí)時(shí)不需要的 session數(shù)據(jù)應(yīng)該被處理。在企業(yè)中,一些應(yīng)用程序常常大量使用Session超時(shí),這將會(huì)導(dǎo)致無(wú)法正常工作。此外經(jīng)常使用非常高的Session超 時(shí)- 24小時(shí)為用戶提供額外的“體驗(yàn)”,使他們不必再次登錄。
舉一個(gè)實(shí)際的例子,從session里的數(shù)據(jù)庫(kù)列表中選擇所需要的數(shù)據(jù)。其目的是為了避免不必要的數(shù)據(jù)庫(kù)查詢。(是不是覺得有點(diǎn)過早優(yōu)化呢?)。這 將導(dǎo)致在session對(duì)象中為每個(gè)單獨(dú)的用戶放入幾千個(gè)字節(jié)。雖然,緩存這些信息它是合理的,但用戶session可以肯定是一個(gè)錯(cuò)誤的地方。
另外一個(gè)例子是,為了管理Session狀態(tài)而濫用Hibernate session。Hibernatesession對(duì)象只是為了快速訪問數(shù)據(jù)庫(kù)而放入HTTPsession對(duì)象中。然而,這將導(dǎo)致更多必要的數(shù)據(jù)被存儲(chǔ)。同時(shí)每個(gè)用戶的內(nèi)存占用也將顯著提高。
現(xiàn)如今,AJAX應(yīng)用程序Session狀態(tài)也可以在客戶端進(jìn)行管理。這使服務(wù)端程序變成無(wú)狀態(tài)的,或接近無(wú)狀態(tài)的,同時(shí)也顯然有著更好的可擴(kuò)展性。
線程本地變量?jī)?nèi)存泄露
在Java中使用ThreadLocal變 量是為了在一個(gè)特定的線程中綁定變量。這意味著每個(gè)線程都有它自己的單獨(dú)實(shí)例。這種方法一般在一個(gè)線程中用于處理狀態(tài)信息,例如用戶授權(quán)。然而,一個(gè) ThreadLocal變量的生命周期與另外一個(gè)線程的生命周期是息息相關(guān)的。被遺忘的ThreadLocal變量很容易導(dǎo)致內(nèi)存問題,尤其是在應(yīng)用服務(wù) 器中。
如果忘記了設(shè)置ThreadLocal變量,尤其是在應(yīng)用服務(wù)器中,這很容易導(dǎo)致內(nèi)存問題。應(yīng)用服務(wù)器利用線程池避免常量不斷創(chuàng)建和線程銷毀。舉個(gè) 例子,一個(gè)HTTPServletRequest類在運(yùn)行時(shí)得到一個(gè)空閑的已分配的線程,在執(zhí)行完后將它回傳到線程池中。如果應(yīng)用程序邏輯使用 ThreadLocal變量和忘記了顯式地移除它們,這時(shí),內(nèi)存是不會(huì)被釋放的。
根據(jù)線程池大小——在程序系統(tǒng)中這些線程池可以是幾百個(gè)線程。同時(shí),由ThreadLocal變量引用的對(duì)象的大小,這可能導(dǎo)致一些問題。例如,在 最壞的情況下,一個(gè)200個(gè)線程的線程池和一個(gè)5M大小的線程池將會(huì)導(dǎo)致1 GB的不必要的內(nèi)存占用。這將立即導(dǎo)致強(qiáng)烈的垃圾回收反應(yīng),同時(shí)導(dǎo)致糟糕的響 應(yīng)時(shí)間和潛在的OutOfMemoryError錯(cuò)誤。
一個(gè)實(shí)際的例子就是在JBossWS 1.2.0版本中出現(xiàn)的一個(gè)bug(在JBossWS1.2.1版本已經(jīng)被修復(fù))—— “DOMUtils doesn’t clear thread locals”。此問題就是ThreadLocal變量導(dǎo)致的,它引用了一個(gè)14MB的 解析文檔。
大型臨時(shí)對(duì)象
大型臨時(shí)對(duì)象在最壞的情況下也能導(dǎo)致outofmemoryerror錯(cuò)誤或者至少?gòu)?qiáng)烈的GC反應(yīng)。例如,如果非常大的文檔(XML、PDF、圖 片…)必須閱讀和處理時(shí)。在一個(gè)特定的情況下,應(yīng)用程序幾分鐘都沒有響應(yīng)或性能非常有限,幾乎沒有可用的。其中根本原因是垃圾回收反應(yīng)過于強(qiáng)烈。下面對(duì)讀 取PDF文檔的一段代碼作了詳細(xì)分析:
- 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);
這些文檔采用按固定字節(jié)數(shù)的方式來(lái)讀取。首先,他們被讀入中字節(jié)數(shù)組中,然后發(fā)送到用戶的瀏覽器中。然而僅僅幾個(gè)并行請(qǐng)求將會(huì)導(dǎo)致堆溢出。由于讀取 文檔采用了極其低效的算法,這將導(dǎo)致問題越來(lái)越糟糕。最初的想法只是創(chuàng)建1KB的初始字節(jié)數(shù)組。如果這個(gè)數(shù)組滿了,則一個(gè)新的1KB數(shù)組將被創(chuàng)建,同時(shí)這 個(gè)老的數(shù)組將拷貝到新的數(shù)組中。這意味著當(dāng)讀取文檔時(shí),一個(gè)新數(shù)組將被創(chuàng)建,同時(shí)將讀取的每字節(jié)都復(fù)制到新數(shù)組中。這將導(dǎo)致大量的臨時(shí)對(duì)象和兩倍于實(shí)際數(shù) 據(jù)大小的內(nèi)存消耗——數(shù)據(jù)將永久被復(fù)制。
在處理大量數(shù)據(jù)時(shí),優(yōu)化處理邏輯性能是至關(guān)重要的。在這種情況下,一個(gè)簡(jiǎn)單的負(fù)載測(cè)試會(huì)顯示這一問題。
糟糕的垃圾回收器配置
到目前為止,在所提到的情境中出現(xiàn)的問題基本都是由應(yīng)用程序代碼所導(dǎo)致的。然而,這些原因的根源是由于垃圾回收器配置錯(cuò)誤,或者丟失。我常??吹接?戶相信他們的應(yīng)用程序服務(wù)器的默認(rèn)設(shè)置,同時(shí)也相信應(yīng)用服務(wù)器的開發(fā)者了解哪些是自己的程序最好的。無(wú)論如何,堆的配置很大程度上取決于應(yīng)用程序和實(shí)際使 用場(chǎng)景。根據(jù)場(chǎng)景來(lái)調(diào)整參數(shù),應(yīng)用程序才能更好地執(zhí)行。和一批執(zhí)行長(zhǎng)期任務(wù)的應(yīng)用程序相比,一個(gè)執(zhí)行大量短而持久的應(yīng)用程序配置起來(lái)是完全不同的。此外, 實(shí)際的配置還取決于JVM使用情況。對(duì)IBM來(lái)說(shuō),什么才能使Sun Jvm正常運(yùn)行可能是一場(chǎng)噩夢(mèng)(或至少是不理想的)。配置錯(cuò)誤的垃圾收集器通常不會(huì) 立即被確認(rèn)為性能問題的根源(除非你監(jiān)控了垃圾收集器的活動(dòng))。通常我們?nèi)庋劭梢姷膯栴}都是響應(yīng)過慢。同時(shí),理解垃圾回收活動(dòng)與響應(yīng)時(shí)間的關(guān)系也是不明顯 的。如果垃圾回收的時(shí)間與響應(yīng)時(shí)間沒什么關(guān)聯(lián),人們通常會(huì)發(fā)現(xiàn)一個(gè)非常復(fù)雜的性能問題。響應(yīng)時(shí)間和執(zhí)行時(shí)間度量問題主要體現(xiàn)在應(yīng)用程序——對(duì)于這種現(xiàn)象, 在不同的地方都沒有一個(gè)明顯的模式。
下圖顯示了事務(wù)指標(biāo)與垃圾收集時(shí)間在dynaTrace中的關(guān)系。我發(fā)現(xiàn)了一些情況,關(guān)于垃圾回收器的優(yōu)化問題。人們正打算花幾周的時(shí)間去解決如何在幾分鐘內(nèi)設(shè)置解決性能問題。
類加載器內(nèi)存泄露
在談到內(nèi)存泄漏時(shí),大部分人主要認(rèn)為是堆中的對(duì)象。除了對(duì)象,類和常量也是托管在堆中。根據(jù)JVM,它們被放入堆中特定的區(qū)域。例如Sun JVM 使用所謂的永久代或PermGen。通常情況下,類被放入堆中好幾次。僅僅是因?yàn)樗麄円呀?jīng)被不同的類加載器加載。在現(xiàn)代化企業(yè)級(jí)應(yīng)用程序中,加載類的內(nèi)存 占用能夠達(dá)到幾百M(fèi)B。
關(guān)鍵是避免無(wú)謂地增加類的大小。一個(gè)很好的例子是大量字符串常量的定義——例如在GUI應(yīng)用程序中。這里所有的文本通常存儲(chǔ)在常量。而使用常量字符 串的方法原則上是一個(gè)好的設(shè)計(jì)方法,內(nèi)存消耗不應(yīng)該被忽視。在真實(shí)的情況下,在一個(gè)國(guó)際化應(yīng)用程序中,所有常量都會(huì)被定義為各種語(yǔ)言。一個(gè)很不起眼的代碼 錯(cuò)誤都會(huì)影響到已經(jīng)被加載的類。最終的結(jié)果是,在應(yīng)用程序的永久代中,JVM將出現(xiàn)OutOfMemoryError 錯(cuò)誤,同時(shí)崩潰。
應(yīng)用服務(wù)器還面臨著類加載器泄漏的問題。這些泄漏的原因主要是因?yàn)轭惣虞d器不能被垃圾回收,因?yàn)轭惣虞d器中的類的一個(gè)對(duì)象仍然活著。結(jié)果,這些類并 不打算釋放這些內(nèi)存占用。而現(xiàn)在,這個(gè)問題已經(jīng)被J2EE 應(yīng)用程序服務(wù)器很好的解決了,它似乎更常出現(xiàn)在OSGI-based應(yīng)用程序環(huán)境。
總結(jié)
在Java應(yīng)用程序中內(nèi)存問題通常是多方面的,這容易導(dǎo)致性能和可擴(kuò)展性的問題。特別是在有著大量并行用戶的J2EE應(yīng)用程序中,內(nèi)存管理必須是應(yīng) 用程序體系結(jié)構(gòu)的核心部分。然而垃圾回收器對(duì)于那些未使用的對(duì)象是否被清理并不關(guān)心,所以開發(fā)人員還是需要適當(dāng)?shù)膬?nèi)存管理。此外,應(yīng)用程序內(nèi)存管理設(shè)計(jì)是 應(yīng)用程序配置的核心部分。
你的經(jīng)驗(yàn)
這些都是我在現(xiàn)實(shí)世界應(yīng)用程序中發(fā)現(xiàn)的反模式。如果你有額外的反模式或共同的問題,我很愿意更多地了解他們。
關(guān)于作者
這篇文章基于我和codecentric的作者M(jìn)irko Novakovic共同研究的性能反模式系列。
其他感興趣的博客
由于性能反模式是我的愛好,我將定期發(fā)布關(guān)于反模式的帖子。它們將會(huì)從這些帖子里選擇你們可能感興趣的一篇帖子。
如果你想要了解更多關(guān)于如何解決像本文中提到的內(nèi)存問題,和其他一些java運(yùn)行壞境相關(guān)的問題,你也許對(duì)我同事最近一本關(guān)于持續(xù)應(yīng)用程序性能管理的白皮書感興趣。



















