1.記憶體洩漏:基礎 對于初學者來說,将記憶體洩漏視為一種疾病,将Java的OutOfMemoryError(簡稱OOM)視為一種症狀。但與任何疾病一樣,并非所有OOM都意味着記憶體洩漏:由于生成大量局部變量或其他此類事件,OOM可能會發生。另一方面,并非所有記憶體洩漏都必然表現為OOM,特别是在桌面應用程式或用戶端應用程式(沒有重新啟動時運作很長時間)的情況下。
将記憶體洩漏視為疾病,将OutOfMemoryError視為症狀。但并非所有OutOfMemoryErrors都意味着記憶體洩漏,并非所有記憶體洩漏都表現為OutOfMemoryErrors。
為什麼這些洩漏如此糟糕?除此之外,程式執行期間洩漏的記憶體塊通常會降低系統性能,因為配置設定但未使用的記憶體塊必須在系統耗盡空閑實體記憶體時進行換出。最終,程式甚至可能耗盡其可用的虛拟位址空間,進而導緻OOM。
2.解密OutOfMemoryError 如上所述,OOM是記憶體洩漏的常見訓示。實質上,當沒有足夠的空間來配置設定新對象時,會抛出錯誤。當垃圾收集器找不到必要的空間,并且堆不能進一步擴充,會多次嘗試。是以,會出現錯誤以及堆棧跟蹤。
診斷OOM的第一步是确定錯誤的實際含義。這聽起來很清除,但答案并不總是那麼清晰。例如:OOM是否是因為Java堆已滿而出現,還是因為本機堆已滿?為了幫助您回答這個問題,讓我們分析一些可能的錯誤消息:
java.lang.OutOfMemoryError: Java heap space java.lang.OutOfMemoryError: PermGen space java.lang.OutOfMemoryError: Requested array size exceeds VM limit java.lang.OutOfMemoryError: request bytes for . Out of swap space? java.lang.OutOfMemoryError: (Native method) 2.1.“Java heap space” 此錯誤消息不一定意味着記憶體洩漏。實際上,問題可能與配置問題一樣簡單。
例如,我負責分析一直産生這種類型的OutOfMemoryError的應用程式。經過一番調查後,我發現罪魁禍首是陣列執行個體化,因為需要太多的記憶體;在這種情況下,并不是應用程式的錯,而是應用程式伺服器依賴于預設的堆太小了。我通過調整JVM的記憶體參數解決了這個問題。
在其他情況下,特别是對于長期存在的應用程式,該消息可能表明我們無意中持有對象的引用,進而阻止垃圾收集器清理它們。這時Java語言等同于記憶體洩漏。 (注意:應用程式調用的API也可能無意中持有對象引用。)
這些“Java堆空間”OOM的另一個潛在來源是使用finalizers。如果類具有finalize方法,則在垃圾收集時該類型的對象不會被回收。而是在垃圾收集之後,稍後對象将排隊等待最終确定。在Sun實作中,finalizers由守護線程執行。如果finalizers線程無法跟上finalization隊列,那麼Java堆可能會填滿并且可能抛出OOM。
2.2.“PermGen space” 此錯誤消息表明永久代已滿。永久代是存儲類和方法對象的堆的區域。如果應用程式加載了大量類,則可能需要使用-XX:MaxPermSize選項增加永久代的大小。
Interned java.lang.String對象也存儲在永久代中。 java.lang.String類維護一個字元串池。調用實習方法時,該方法檢查池以檢視是否存在等效字元串。如果是這樣,它由實習方法傳回;如果沒有,則将字元串添加到池中。更準确地說,java.lang.String.intern方法傳回一個字元串的規範表示;結果是對該字元串顯示為文字時将傳回的同一個類執行個體的引用。如果應用程式執行個體化大量字元串,則可能需要增加永久代的大小。
注意:您可以使用jmap -permgen指令列印與永久生成相關的統計資訊,包括有關内部化String執行個體的資訊。
2.3.“Requested array size exceeds VM limit” 此錯誤表示應用程式(或該應用程式使用的API)嘗試配置設定大于堆大小的數組。例如,如果應用程式嘗試配置設定512MB的數組但最大堆大小為256MB,則将抛出此錯誤消息的OOM。在大多數情況下,問題是配置問題或應用程式嘗試配置設定海量數組時導緻的錯誤。
2.4.“Request bytes for . Out of swap space?” 此消息似乎是一個OOM。但是,當本機堆的配置設定失敗并且本機堆可能将被耗盡時,HotSpot VM會抛出此異常。消息中包括失敗請求的大小(以位元組為機關)以及記憶體請求的原因。在大多數情況下,是報告配置設定失敗的源子產品的名稱。
如果抛出此類型的OOM,則可能需要在作業系統上使用故障排除實用程式來進一步診斷問題。在某些情況下,問題甚至可能與應用程式無關。例如,您可能會在以下情況下看到此錯誤:
作業系統配置的交換空間不足。 系統上的另一個程序是消耗所有可用的記憶體資源。 由于本機洩漏,應用程式也可能失敗(例如,如果某些應用程式或庫代碼不斷配置設定記憶體但無法将其釋放到作業系統)。
2.5. (Native method) 如果您看到此錯誤消息并且堆棧跟蹤的頂部架構是本機方法,則該本機方法遇到配置設定失敗。此消息與上一個消息之間的差別在于,在JNI或本機方法中檢測到Java記憶體配置設定失敗,而不是在Java VM代碼中檢測到。
如果抛出此類型的OOM,您可能需要在作業系統上使用實用程式來進一步診斷問題。
2.6.Application Crash Without OOM 有時,應用程式可能會在從本機堆配置設定失敗後很快崩潰。如果您運作的本機代碼不檢查記憶體配置設定函數傳回的錯誤,則會發生這種情況。
例如,如果沒有可用記憶體,malloc系統調用将傳回NULL。如果未檢查malloc的傳回,則應用程式在嘗試通路無效的記憶體位置時可能會崩潰。根據具體情況,可能很難定位此類問題。
在某些情況下,緻命錯誤日志或崩潰轉儲的資訊就足以診斷問題。如果确定崩潰的原因是某些記憶體配置設定中缺少錯誤處理,那麼您必須找到所述配置設定失敗的原因。與任何其他本機堆問題一樣,系統可能配置了但交換空間不足,另一個程序可能正在消耗所有可用記憶體資源等。
3.洩漏診斷 在大多數情況下,診斷記憶體洩漏需要非常詳細地了解相關應用程式。警告:該過程可能很長并且是疊代的。
我們尋找記憶體洩漏的政策将相對簡單:
識别症狀 啟用詳細垃圾回收 啟用分析 分析蹤迹 3.1 識别症狀 正如所讨論的,在許多情況下,Java程序最終會抛出一個OOM運作時異常,這是一個明确的訓示,表明您的記憶體資源已經耗盡。在這種情況下,您需要區分正常的記憶體耗盡和洩漏。分析OOM的消息并嘗試根據上面提供的讨論找到罪魁禍首。
通常,如果Java應用程式請求的存儲空間超過運作時堆提供的存儲空間,則可能是由于設計不佳導緻的。例如,如果應用程式建立映像的多個副本或将檔案加載到數組中,則當映像或檔案非常大時,它将耗盡存儲空間。這是正常的資源耗盡。該應用程式按設計工作(雖然這種設計顯然是愚蠢的)。
但是,如果應用程式在處理相同類型的資料時穩定地增加其記憶體使用率,則可能會發生記憶體洩漏。
3.2 啟用詳細垃圾收集 斷言确實存在記憶體洩漏的最快方法之一是啟用詳細垃圾回收。通常可以通過檢查verbosegc輸出中的模式來識别記憶體限制問題。
具體來說,-verbosegc參數允許您在每次垃圾收集(GC)過程開始時生成跟蹤。也就是說,當記憶體被垃圾收集時,摘要報告會列印到标準錯誤,讓您了解記憶體的管理方式。
這是使用-verbosegc選項生成的一些典型輸出:
image
此GC跟蹤檔案中的每個塊(或節)按遞增順序編号。要了解這種跟蹤,您應該檢視連續的配置設定失敗節,并查找随着時間的推移而減少的釋放記憶體(位元組和百分比),同時總記憶體(此處,19725304)正在增加。這些是記憶體耗盡的典型迹象。
3.3 啟用分析 不同的JVM提供了生成跟蹤檔案以反映堆活動的不同方法,這些方法通常包括有關對象類型和大小的詳細資訊。這稱為分析堆。
3.4 分析路徑 本文重點介紹Java VisualVM生成的跟蹤。跟蹤可以有不同的格式,因為它們可以由不同的Java記憶體洩漏檢測工具生成,但它們背後的想法總是相同的:在堆中找到不應該存在的對象塊,并确定這些對象是否累積而不是釋放。特别感興趣的是每次在Java應用程式中觸發某個事件時已知的臨時對象。應該僅存少量,但存在許多對象執行個體,通常表示應用程式出現錯誤。
最後,解決記憶體洩漏需要您徹底檢查代碼。了解對象洩漏的類型可能對此非常有用,并且可以大大加快調試速度。
4.垃圾收集如何在JVM中運作? 在我們開始分析具有記憶體洩漏問題的應用程式之前,讓我們首先看看垃圾收集在JVM中的工作原理。
JVM使用一種稱為跟蹤收集器的垃圾收集器,它基本上通過暫停它周圍的世界來操作,标記所有根對象(由運作線程直接引用的對象),并遵循它們的引用,标記它沿途看到的每個對象。
Java基于分代假設-實作了一種稱為分代垃圾收集器的東西,該假設表明建立的大多數對象被快速丢棄,而未快速收集的對象可能會存在一段時間。
基于此假設,[Java将對象分為多代](www.oracle.com/technetwork…. Generations|outline)。這是一個視覺解釋:
image
Young Generation -這是對象的開始。它有兩個子代 Eden Space -對象從這裡開始。大多數物體都是在Eden Space中創造和銷毀的。在這裡,GC執行Minor GCs,這是優化的垃圾收集。執行Minor GC時,對仍然需要的對象的任何引用都将遷移到其中一個survivors空間(S0或S1)。 Survivor Space (S0 and S1)-幸存Eden Space的對象最終來到這裡。其中有兩個,在任何給定時間隻有一個正在使用(除非我們有嚴重的記憶體洩漏)。一個被指定為空,另一個被指定為活動,與每個GC循環交替。 Tenured Generation -也被稱為老年代(圖2中的舊空間),這個空間容納存活較長的對象,使用壽命更長(如果它們活得足夠長,則從Survivor空間移過來)。填充此空間時,GC會執行完整GC,這會在性能方面降低成本。如果此空間無限制地增長,則JVM将抛出OutOfMemoryError - Java堆空間。 Permanent Generation -作為與終身代密切相關的第三代,永久代是特殊的,因為它儲存虛拟機所需的資料,以描述在Java語言級别上沒有等價的對象。例如,描述類和方法的對象存儲在永久代中。 Java足夠聰明,可以為每一代應用不同的垃圾收集方法。使用名為Parallel New Collector的跟蹤複制收集器處理年輕代。這個收集器阻止了這個世界,但由于年輕一代通常很小,是以暫停很短暫。
有關JVM代及其工作原理的更多資訊,請查閱Memory Management in the Java HotSpot™ Virtual Machine 。
5 檢測記憶體洩漏 要查找記憶體洩漏并消除它們,您需要合适的記憶體洩漏工具。是時候使用Java VisualVM檢測并删除此類洩漏。
5.1 使用Java VisualVM遠端分析堆 VisualVM是一種工具,它提供了一個可視化界面,用于檢視有關基于Java技術的應用程式運作時的詳細資訊。
使用VisualVM,您可以檢視與本地應用程式和遠端主機上運作的應用程式相關的資料。您還可以捕獲有關JVM軟體執行個體的資料,并将資料儲存到本地系統。
為了從Java VisualVM的所有功能中受益,您應該運作Java平台标準版(Java SE)版本6或更高版本。
Related: Why You Need to Upgrade to Java 8 Already
5.2. 為JVM啟用遠端連接配接 在生産環境中,通常很難通路運作代碼的實際機器。幸運的是,我們可以遠端分析我們的Java應用程式。
首先,我們需要在目标機器上授予自己JVM通路權限。為此,請使用以下内容建立名為jstatd.all.policy的檔案:
grant codebase "file:${java.home}/../lib/tools.jar" {
permission java.security.AllPermission;