沒有經驗的程序員常常認爲Java的自動垃圾回收徹底使他們免於擔憂內存管理。這是一個常見的誤解:雖然垃圾收集器作得很好,但即便是最好的程序員也徹底有可能成爲嚴重破壞內存泄漏的犧牲品。讓我解釋一下。php
當沒必要要地維護再也不須要的對象引用時,會發生內存泄漏。這些泄漏很糟糕。首先,當程序消耗愈來愈多的資源時,它們會對計算機施加沒必要要的壓力。更糟糕的是,檢測這些泄漏可能很困難:靜態分析一般很難精確識別這些冗餘引用,現有的泄漏檢測工具會跟蹤和報告有關單個對象的細粒度信息,產生難以解釋且缺少精確度的結果。html
換句話說,泄漏要麼太難以識別,要麼用太具體而沒法用術語來識別。java
實際上有四類內存問題具備類似和重疊的特徵,但緣由和解決方案各不相同:node
在這個內存管理教程中,我將專一於Java堆漏洞,並概述一種基於Java VisualVM報告檢測此類泄漏的方法,並利用可視化界面在運行時分析基於Java技術的應用程序。程序員
但在您能夠預防和發現內存泄漏以前,您應該瞭解它們的發生方式和緣由。 (注意:若是你能很好地處理錯綜複雜的內存泄漏,你能夠跳過。)數組
對於初學者來講,將內存泄漏視爲一種疾病,將Java的OutOfMemoryError(簡稱OOM)視爲一種症狀。但與任何疾病同樣,並不是全部OOM都意味着內存泄漏:因爲生成大量局部變量或其餘此類事件,OOM可能會發生。另外一方面,並不是全部內存泄漏都必然表現爲OOM,特別是在桌面應用程序或客戶端應用程序(沒有從新啓動時運行很長時間)的狀況下。bash
將內存泄漏視爲疾病,將OutOfMemoryError視爲症狀。但並不是全部OutOfMemoryErrors都意味着內存泄漏,並不是全部內存泄漏都表現爲OutOfMemoryErrors。服務器
爲何這些泄漏如此糟糕?除此以外,程序執行期間泄漏的內存塊一般會下降系統性能,由於分配但未使用的內存塊必須在系統耗盡空閒物理內存時進行換出。最終,程序甚至可能耗盡其可用的虛擬地址空間,從而致使OOM。oracle
如上所述,OOM是內存泄漏的常見指示。實質上,當沒有足夠的空間來分配新對象時,會拋出錯誤。當垃圾收集器找不到必要的空間,而且堆不能進一步擴展,會屢次嘗試。所以,會出現錯誤以及堆棧跟蹤。app
診斷OOM的第一步是肯定錯誤的實際含義。這聽起來很清除,但答案並不老是那麼清晰。例如:OOM是不是由於Java堆已滿而出現,仍是由於本機堆已滿?爲了幫助您回答這個問題,讓咱們分析一些可能的錯誤消息:
此錯誤消息不必定意味着內存泄漏。實際上,問題可能與配置問題同樣簡單。
例如,我負責分析一直產生這種類型的OutOfMemoryError的應用程序。通過一番調查後,我發現罪魁禍首是陣列實例化,由於須要太多的內存;在這種狀況下,並非應用程序的錯,而是應用程序服務器依賴於默認的堆過小了。我經過調整JVM的內存參數解決了這個問題。
在其餘狀況下,特別是對於長期存在的應用程序,該消息可能代表咱們無心中持有對象的引用,從而阻止垃圾收集器清理它們。這時Java語言等同於內存泄漏。 (注意:應用程序調用的API也可能無心中持有對象引用。)
這些「Java堆空間」OOM的另外一個潛在來源是使用finalizers。若是類具備finalize方法,則在垃圾收集時該類型的對象不會被回收。而是在垃圾收集以後,稍後對象將排隊等待最終肯定。在Sun實現中,finalizers由守護線程執行。若是finalizers線程沒法跟上finalization隊列,那麼Java堆可能會填滿而且可能拋出OOM。
此錯誤消息代表永久代已滿。永久代是存儲類和方法對象的堆的區域。若是應用程序加載了大量類,則可能須要使用-XX:MaxPermSize選項增長永久代的大小。
Interned java.lang.String對象也存儲在永久代中。 java.lang.String類維護一個字符串池。調用實習方法時,該方法檢查池以查看是否存在等效字符串。若是是這樣,它由實習方法返回;若是沒有,則將字符串添加到池中。更準確地說,java.lang.String.intern方法返回一個字符串的規範表示;結果是對該字符串顯示爲文字時將返回的同一個類實例的引用。若是應用程序實例化大量字符串,則可能須要增長永久代的大小。
注意:您可使用jmap -permgen命令打印與永久生成相關的統計信息,包括有關內部化String實例的信息。
此錯誤表示應用程序(或該應用程序使用的API)嘗試分配大於堆大小的數組。例如,若是應用程序嘗試分配512MB的數組但最大堆大小爲256MB,則將拋出此錯誤消息的OOM。在大多數狀況下,問題是配置問題或應用程序嘗試分配海量數組時致使的錯誤。
此消息彷佛是一個OOM。可是,當本機堆的分配失敗而且本機堆可能將被耗盡時,HotSpot VM會拋出此異常。消息中包括失敗請求的大小(以字節爲單位)以及內存請求的緣由。在大多數狀況下,是報告分配失敗的源模塊的名稱。
若是拋出此類型的OOM,則可能須要在操做系統上使用故障排除實用程序來進一步診斷問題。在某些狀況下,問題甚至可能與應用程序無關。例如,您可能會在如下狀況下看到此錯誤:
因爲本機泄漏,應用程序也可能失敗(例如,若是某些應用程序或庫代碼不斷分配內存但沒法將其釋放到操做系統)。
若是您看到此錯誤消息而且堆棧跟蹤的頂部框架是本機方法,則該本機方法遇到分配失敗。此消息與上一個消息之間的區別在於,在JNI或本機方法中檢測到Java內存分配失敗,而不是在Java VM代碼中檢測到。
若是拋出此類型的OOM,您可能須要在操做系統上使用實用程序來進一步診斷問題。
有時,應用程序可能會在從本機堆分配失敗後很快崩潰。若是您運行的本機代碼不檢查內存分配函數返回的錯誤,則會發生這種狀況。
例如,若是沒有可用內存,malloc系統調用將返回NULL。若是未檢查malloc的返回,則應用程序在嘗試訪問無效的內存位置時可能會崩潰。根據具體狀況,可能很難定位此類問題。
在某些狀況下,致命錯誤日誌或崩潰轉儲的信息就足以診斷問題。若是肯定崩潰的緣由是某些內存分配中缺乏錯誤處理,那麼您必須找到所述分配失敗的緣由。與任何其餘本機堆問題同樣,系統可能配置了但交換空間不足,另外一個進程可能正在消耗全部可用內存資源等。
在大多數狀況下,診斷內存泄漏須要很是詳細地瞭解相關應用程序。警告:該過程可能很長而且是迭代的。
咱們尋找內存泄漏的策略將相對簡單:
正如所討論的,在許多狀況下,Java進程最終會拋出一個OOM運行時異常,這是一個明確的指示,代表您的內存資源已經耗盡。在這種狀況下,您須要區分正常的內存耗盡和泄漏。分析OOM的消息並嘗試根據上面提供的討論找到罪魁禍首。
一般,若是Java應用程序請求的存儲空間超過運行時堆提供的存儲空間,則多是因爲設計不佳致使的。例如,若是應用程序建立映像的多個副本或將文件加載到數組中,則當映像或文件很是大時,它將耗盡存儲空間。這是正常的資源耗盡。該應用程序按設計工做(雖然這種設計顯然是愚蠢的)。
可是,若是應用程序在處理相同類型的數據時穩定地增長其內存利用率,則可能會發生內存泄漏。
斷言確實存在內存泄漏的最快方法之一是啓用詳細垃圾回收。一般能夠經過檢查verbosegc輸出中的模式來識別內存約束問題。
具體來講,-verbosegc參數容許您在每次垃圾收集(GC)過程開始時生成跟蹤。也就是說,當內存被垃圾收集時,摘要報告會打印到標準錯誤,讓您瞭解內存的管理方式。
這是使用-verbosegc選項生成的一些典型輸出:
此GC跟蹤文件中的每一個塊(或節)按遞增順序編號。要理解這種跟蹤,您應該查看連續的分配失敗節,並查找隨着時間的推移而減小的釋放內存(字節和百分比),同時總內存(此處,19725304)正在增長。這些是內存耗盡的典型跡象。
不一樣的JVM提供了生成跟蹤文件以反映堆活動的不一樣方法,這些方法一般包括有關對象類型和大小的詳細信息。這稱爲分析堆。
本文重點介紹Java VisualVM生成的跟蹤。跟蹤能夠有不一樣的格式,由於它們能夠由不一樣的Java內存泄漏檢測工具生成,但它們背後的想法老是相同的:在堆中找到不該該存在的對象塊,並肯定這些對象是否累積而不是釋放。特別感興趣的是每次在Java應用程序中觸發某個事件時已知的臨時對象。應該僅存少許,但存在許多對象實例,一般表示應用程序出現錯誤。
最後,解決內存泄漏須要您完全檢查代碼。瞭解對象泄漏的類型可能對此很是有用,而且能夠大大加快調試速度。
在咱們開始分析具備內存泄漏問題的應用程序以前,讓咱們首先看看垃圾收集在JVM中的工做原理。
JVM使用一種稱爲跟蹤收集器的垃圾收集器,它基本上經過暫停它周圍的世界來操做,標記全部根對象(由運行線程直接引用的對象),並遵循它們的引用,標記它沿途看到的每一個對象。
Java基於分代假設-實現了一種稱爲分代垃圾收集器的東西,該假設代表建立的大多數對象被快速丟棄,而未快速收集的對象可能會存在一段時間。
基於此假設,[Java將對象分爲多代](www.oracle.com/technetwork…. Generations|outline)。這是一個視覺解釋:
Java足夠聰明,能夠爲每一代應用不一樣的垃圾收集方法。使用名爲Parallel New Collector的跟蹤複製收集器處理年輕代。這個收集器阻止了這個世界,但因爲年輕一代一般很小,因此暫停很短暫。
有關JVM代及其工做原理的更多信息,請查閱Memory Management in the Java HotSpot™ Virtual Machine 。
要查找內存泄漏並消除它們,您須要合適的內存泄漏工具。是時候使用Java VisualVM檢測並刪除此類泄漏。
VisualVM是一種工具,它提供了一個可視化界面,用於查看有關基於Java技術的應用程序運行時的詳細信息。
使用VisualVM,您能夠查看與本地應用程序和遠程主機上運行的應用程序相關的數據。您還能夠捕獲有關JVM軟件實例的數據,並將數據保存到本地系統。
爲了從Java VisualVM的全部功能中受益,您應該運行Java平臺標準版(Java SE)版本6或更高版本。
Related: Why You Need to Upgrade to Java 8 Already
在生產環境中,一般很難訪問運行代碼的實際機器。幸運的是,咱們能夠遠程分析咱們的Java應用程序。
首先,咱們須要在目標機器上授予本身JVM訪問權限。爲此,請使用如下內容建立名爲jstatd.all.policy的文件:
grant codebase "file:${java.home}/../lib/tools.jar" {
permission java.security.AllPermission;
};
複製代碼
建立文件後,咱們須要使用jstatd - Virtual Machine jstat Daemon工具啓用與目標VM的遠程鏈接,以下所示:
jstatd -p <PORT_NUMBER> -J-Djava.security.policy=<PATH_TO_POLICY_FILE>
複製代碼
例如:
jstatd -p 1234 -J-Djava.security.policy=D:\jstatd.all.policy
複製代碼
經過在目標VM中啓動jstatd,咱們可以鏈接到目標計算機並遠程分析應用程序的內存泄漏問題。
在客戶端計算機中,打開提示並鍵入jvisualvm以打開VisualVM工具。
接下來,咱們必須在VisualVM中添加遠程主機。當目標JVM啓用以容許來自具備J2SE 6或更高版本的另外一臺計算機的遠程鏈接時,咱們啓動Java VisualVM工具並鏈接到遠程主機。若是與遠程主機的鏈接成功,咱們將看到在目標JVM中運行的Java應用程序,以下所示:
要在應用程序上運行內存分析器,咱們只需在側面板中雙擊其名稱便可。
如今咱們已經設置了內存分析器,讓咱們研究一個內存泄漏問題的應用程序,咱們稱之爲MemLeak。
固然,有不少方法能夠在Java中建立內存泄漏。爲簡單起見,咱們將一個類定義爲HashMap中的鍵,但咱們不會定義equals()和hashcode()方法。
HashMap是Map接口的哈希表實現,所以它定義了鍵和值的基本概念:每一個值都與惟一鍵相關,所以若是給定鍵值對的鍵已經存在於HashMap,它的當前值被替換。
咱們的密鑰類必須提供equals()和hashcode()方法的正確實現。沒有它們,就沒法保證會生成一個好的密鑰。
經過不定義equals()和hashcode()方法,咱們一遍又一遍地向HashMap添加相同的鍵,而不是按原樣替換鍵,HashMap不斷增加,沒法識別這些相同的鍵並拋出OutOfMemoryError 。
MemLeak類:
package com.post.memory.leak;
import java.util.Map;
public class MemLeak {
public final String key;
public MemLeak(String key) {
this.key =key;
}
public static void main(String args[]) {
try {
Map map = System.getProperties();
for(;;) {
map.put(new MemLeak("key"), "value");
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
複製代碼
注意:內存泄漏不是因爲第14行的無限循環:無限循環可能致使資源耗盡,但不會致使內存泄漏。若是咱們已經正確實現了equals()和hashcode()方法,那麼即便使用無限循環,代碼也能正常運行,由於咱們在HashMap中只有一個元素。
(對於那些感興趣的人,這裏有一些(故意)產生泄漏的替代方法。)
使用Java VisualVM,咱們能夠對Java Heap進行內存監視,並肯定其行爲是否存在內存泄漏。
這是剛剛初始化後MemLeak的Java堆分析器的圖形表示(回想一下咱們對各代的討論):
僅僅30秒以後,老年代幾乎已滿,代表即便使用Full GC,老年代也在不斷增加,這是內存泄漏的明顯跡象。
檢測此泄漏緣由的一種方法以下圖所示(單擊放大),使用帶有heapdump的Java VisualVM生成。在這裏,咱們看到50%的Hashtable $ Entry對象在堆中,而第二行指向MemLeak類。所以,內存泄漏是由MemLeak類中使用的哈希表引發的。
最後,在OutOfMemoryError以後觀察Java Heap,其中Young和Old代徹底填滿。
內存泄漏是最難解決的Java應用程序問題之一,由於症狀多種多樣且難以重現。在這裏,咱們概述了一種逐步發現內存泄漏並肯定其來源的方法。但最重要的是,仔細閱讀您的錯誤消息並注意堆棧跟蹤 - 並不是全部泄漏都像它們出現的那樣簡單。
與Java VisualVM一塊兒,還有其餘幾種能夠執行內存泄漏檢測的工具。許多泄漏檢測器經過攔截對存儲器管理例程的調用在庫級別操做。例如,HPROF是一個與Java 2平臺標準版(J2SE)捆綁在一塊兒的簡單命令行工具,用於堆和CPU分析。能夠直接分析HPROF的輸出,或將其用做JHAT等其餘工具的輸入。當咱們使用Java 2 Enterprise Edition(J2EE)應用程序時,有許多堆轉儲分析器解決方案更友好,例如IBM Heapdumps for Websphere應用程序服務器。
做者:Jose Ferreirade Souza Filho
譯者:Emma