01-工具概述
使用上一章命令行工具或組合能幫助我們獲取目標Java應用性能相關的基礎信息,但它們存在下列局限:
1篓吁、無法獲取方法級別的分析數(shù)據(jù)阵幸,如方法間的調(diào)用關系、各方法的調(diào)用次數(shù)和調(diào)用時間等(這對定位應用性能瓶頸至關重要)。
2斜脂、要求用戶登陸到目標Java應用所在的宿主機上抓艳,使用起來不是很方便。
3帚戳、分析數(shù)據(jù)終端輸出玷或,結果展示不夠直觀。
為此,JDK提供了一些內(nèi)存泄露的分析工具夺克,如jconsole朋截,jvisualvm等,用于輔助開發(fā)人員定位問題位他,但是這些工具很多時候并不滿足快速定位的需求氛濒。所以這里我們介紹的工具相對多一些、豐富一些鹅髓。
圖形化綜合診斷工具
JDK自帶的工具
jconsole:JDK自帶的可視化監(jiān)控工具舞竿。查看Java應用程序的運行概況、監(jiān)控堆信息窿冯、永久區(qū)(或元空間)使用情況骗奖、類加載情況等。位置jdk\bin\jconsole.exe
Visual VM:Visual VM?是一個工具醒串,它提供了一個可視界面执桌,用于查看Java虛擬機上運行的基于Java技術的應用程序的詳細信息。位置:jdk\bin\jvisualvm.exe
JMC:Java Mission Control,內(nèi)置Java?Flight?Recorder芜赌。能夠以極低的性能開銷收集Java虛擬機的性能數(shù)據(jù)仰挣。
第三方工具
MAT:MAT(Memory?Analyzer?Tool)是基于Eclipse的內(nèi)存分析工具,是一個快速较鼓、功能豐富的Java?heap分析工具椎木,它可以幫助我們查找內(nèi)存泄露和減少內(nèi)存消耗。它可以是獨立軟件博烂,也可以Eclipse的插件形式出現(xiàn)香椎。
JProfiler:商業(yè)軟件,需要付費禽篱。功能強大畜伐。與Visual?VM類似。
Arthas:Alibaba開源的Java診斷工具躺率。深受開發(fā)者的喜愛玛界。
Btrace:Java運行時追蹤工具〉恐ǎ可以在不停機的情況下慎框,跟蹤指定的方法調(diào)用、構造函數(shù)調(diào)用和系統(tǒng)內(nèi)存等信息后添。
02-jConsole
基本概述:
從Java5開始笨枯,在JDK中自帶的java監(jiān)控和管理控制臺。
用于對JVM內(nèi)存、線程和類的監(jiān)控馅精,是一個基于JMX(java?management?extensions)的GUI性能監(jiān)控工具严嗜。
三種連接方式:
Local:使用JConsole連接一個正在本地系統(tǒng)運行的JVM,并且執(zhí)行程序和運行JConsole的需要是同一個用戶洲敢。JConsole使用文件系統(tǒng)的授權通過RMI連接器連接到平臺的MBean服務器上漫玄。這種從本地連接的監(jiān)控能力只有Sun的JDK具有。
Remote:使用下面的URL通過RMI連接器連接到一個JMX代理压彭,service:jmx:rmi:///jndi/rmi://hostName:portNum/jmxrmi睦优。JConsole為建立連接,需要在環(huán)境變量中設置mx.remote.credentials來指定用戶名和密碼哮塞,從而進行授權刨秆。
Advance:使用一個特殊的URL連接JMX代理。一般情況使用自己定制的連接器而不是RMI提供的連接器來連接JMX代理忆畅,或者是一個使用JDK1.4的實現(xiàn)了JMX和JMX? Romote的應用衡未。
03-Visual VM??
基本概述:
Visual?VM是一個功能強大的多合一故障診斷和性能監(jiān)控的可視化工具。
它集成了多個JDK命令行工具家凯,使用Visual?VM可用于顯示虛擬機進程即進程的配置和環(huán)境信息(jps缓醋、jinfo),監(jiān)視應用程序的CPU绊诲、GC送粱、堆、方法區(qū)即線程的信息(jstat掂之、jstack)等抗俄,甚至代替JConsole。
在JDK 6?Update 7以后世舰,Visual?VM便作為JDK的一部分發(fā)布(VisaulVM?在JDK/bin目錄下)动雹,即:它完全免費。
此外跟压,Visaul?VM也可以作為獨立的軟甲安裝胰蝠。
連接方式:
本地連接:監(jiān)控本地線程的CPU、類震蒋、線程等茸塞。
遠程連接:
1、確定遠程服務器的ip地址
2查剖、添加JMX(通過JMX技術具體監(jiān)控遠端服務器哪個Java進程)
3钾虐、修改bin/catalina.sh文件,連接遠程的tomact
4笋庄、在../conf中添加jmxremote.access和jmxremote.password文件
5效扫、將服務器地址改為公網(wǎng)ip地址
6效览、設置阿里云安全策略和防火墻策略
7、啟動?tomcat荡短,查看tomcat啟動日志和端口監(jiān)聽
8、JMX中輸入端口號哆键、用戶名掘托、密碼登陸
主要功能:
生成、讀取堆內(nèi)存快照
查看JVM參數(shù)和系統(tǒng)屬性
查看正在運行的虛擬機進程
生成籍嘹、讀取線程快照
程序資源的實時監(jiān)控
其他功能:JMX代理連接闪盔、遠程環(huán)境監(jiān)控、CPU分析和?內(nèi)存分析
04-eclipse?MAT
基本概述:
MAT工具是一款功能強大的Java內(nèi)存分析器辱士±嵯疲可用于查找內(nèi)存泄露以及查看內(nèi)存消耗情況。
獲取dump文件:
dump文件內(nèi)容:MAT可以分析heap?dump文件颂碘。在進行內(nèi)存分析時异赫,只要獲得了反映當前設備內(nèi)存映像的hprof文件,通過MAT打開就可以直觀地看到當前的內(nèi)存信息头岔。一般來說這些信息含有:
所有的對象信息塔拳,包括對象實例、成員變量峡竣、存儲于棧中的基本類型值和存儲于堆中的其他對象的引用值靠抑。
所有的類信息,包括classloader适掰、類名稱颂碧、父類、靜態(tài)變量等类浪。
GCRoot到所有這些對象的引用路徑
線程信息载城,包括線程的調(diào)用棧以及線程的線程局部變量(TLS)。
兩點說明:
說明1:缺點:MAT不是一個萬能工具戚宦,它并不能處理所有的堆轉(zhuǎn)儲文件个曙。但是比較主流的廠家和格式,例如Sun受楼,HP垦搬,SAP所采用的HPROF二進制堆轉(zhuǎn)儲文件,以及IBM的PHD堆存儲文件都能被很好的解析艳汽。
說明2:最吸引人的還是能夠快速為開發(fā)人員生成內(nèi)存泄露報表猴贰,方便問題定位和分析問題。雖然MAT有如此強大的功能河狐,但是內(nèi)存分析也沒有簡單到一鍵完成的程度米绕,很多內(nèi)存問題還是需要我們從MAT展現(xiàn)給我們的信息當中通過經(jīng)驗和直覺才能發(fā)現(xiàn)瑟捣。
獲取dump文件:
方法1:通過前一章介紹的jmap工具生成,可以生成任意一個java進程的dump文件栅干;
方法2:通過配置JVM參數(shù)生成迈套。
選項"-XX:+HeapDumpOutOfMemoryError"或"-XX:+HeapDumpBeforeFullGC"
選項"-XX:HeapDumpPath"所代表的含義就是當程序出現(xiàn)OutOfMemory時,將會在相應的目錄下生成一份dump文件碱鳞。如果不指定選項"XX:HeapDumpPath"則在當前目錄下生成dump文件桑李。
對比:考慮到生產(chǎn)環(huán)境中幾乎不可能在線對其進行分析,大都是采用離線分析窿给,因此使用jmap+MAT工具是最常見的組合贵白。
?方法3:使用VisualVM可以導出dump文件
方法4:使用MAT既可以打開一個已有的堆快照,也可以通過MAT直接從活動Java程序中導出快照崩泡。該功能將借助jps列出當前正在運行的Java進程禁荒,以供選擇并獲取快照。
分析堆dump文件:
1.histogram:展示了各個類的實例數(shù)目以及這些實例的Shallow heap或Retainedheap的總和
MAT的直方圖和jmap的-histo子命令一樣角撞,都能夠展示各個類的實例數(shù)目以及這些實例的Shallow?heap總和呛伴。但是,MAT的直方圖還能計算Retained?heap靴寂,并支持基于實例數(shù)目或Retained?heap的排序方式(默認為?Shallow?heap)磷蜀。此外,MAT還可以將直方圖中的類按照超類百炬、類加載器或者包名分組褐隆。當選中某個類時,MAT界面左上角的Inspector窗口將展示該類Class實例的相關信息剖踊,比如類加載器等庶弃。
2.thread?overview:查看系統(tǒng)中的Java線程;查看局部變量的信息德澈。
3.獲得對象相互引用的關系:with?outgoing referneces; with incoming references
4.淺堆和深堆:
? ? shallow?heap:淺堆是指一個對象消耗的內(nèi)存歇攻。在32位系統(tǒng)中,一個對象引用會占據(jù)4個字節(jié)梆造,一個int類型會占據(jù)4個字節(jié)缴守,long型變量會占據(jù)8個字節(jié),每個對象頭需要占用8個字節(jié)镇辉。根據(jù)堆快照格式不同屡穗,對象的大小可能會向8字節(jié)進行對齊。
以String為例子:兩個int值共占8字節(jié)忽肛,對象引用占4字節(jié)村砂,對象頭占8字節(jié),合計20字節(jié)屹逛,向8字節(jié)對齊础废,故占24字節(jié)(jdk7中)汛骂。
這24字節(jié)為String對象淺堆大小。它與String的value實際取值無關评腺,無論字符串長度如何帘瞭,淺堆的大小始終是24字節(jié)。
? ? retained?heap:
保留集(Retained?Set):
對象A的保留集指當對象A被垃圾回收后蒿讥,可以釋放的所有對象集合(包括對象A本身)图张,即對象A的保留集可以被認為是只能通過對象A被直接訪問或間接訪問到的所有對象的集合。通俗的說诈悍,就是指僅被對象A所持有的對象的集合。
深堆(Retained?Heap):
深堆是指對象的保留集中所有的對象的淺堆大小之和兽埃。
注意:淺堆指對象本身占用的內(nèi)存侥钳,不包括內(nèi)部引用對象的大小。一個對象的深堆指只能通過該對象訪問到的(直接或間接)所有對象的淺堆之和柄错,及對象被回收后舷夺,可以釋放的真實空間。?
? ? 補充:對象實際大小
另外一個概念是對象的實際大小售貌。這里给猾,對象的實際大小定義為一個對象所能觸及的所有對象的淺堆大小之和,也就是我們通常說的對象大小颂跨。與深堆相比敢伸,似乎這個在日常開發(fā)中更為直觀和被人接受,但實際上恒削,這個概念和垃圾回收無關池颈。
下圖展示了一個簡單的對象引用關系圖,對象A引用了C和D钓丰,對象B引用了C和E躯砰。那么對象A的淺堆大小只是只是A本身,不含C和D携丁,而A的實際大小為A琢歇、C、D三者之和梦鉴。而A的深堆大小是A李茫、D之和,由于C還可以通過對象B訪問到尚揣,因此不在對象A的深堆范圍之內(nèi)涌矢。
? ?支配樹:
支配樹(Dominator?Tree)
支配樹的概念源自圖論。
MAT提供了一個稱為支配樹(Dominator?Tree)的對象圖快骗。支配樹體現(xiàn)了對象實例間的支配關系娜庇。在對象引用圖中塔次,所有指向?qū)ο驜的路徑都經(jīng)過對象A,則認為對象A支配對象B名秀。如果對象A是距離對象B最近的支配對象励负,則認為對象A是對象B路徑的直接支配者。支配樹是基于對象間的引用圖鎖建立的匕得,它有以下基本性質(zhì):
對象A的子樹(所有被對象A支配的對象集合)表示對象A的保留集(retained?set)继榆,即深堆。
如果對象A支配對象B汁掠,那么對象A的直接支配者也支配對象B
支配樹的邊與對象引用圖的邊不直接對應略吨。
如下圖所示:左圖表示對象引用圖,右圖表示左圖對應的支配樹考阱。對象A和B由根對象直接支配翠忠,由于在到對象C的路徑中,可以經(jīng)過A乞榨,也可以經(jīng)過B秽之,因此對象C的直接支配者也是根對象。對象F與對象D相互引用吃既,因為到對象F的所有路徑必然經(jīng)過對象D考榨,因此,對象D是對象F的直接支配者鹦倚。而到對象D的所有路徑中河质,必然經(jīng)過對象C,即使是從對象F到對象D的引用震叙,從根節(jié)點出發(fā)愤诱,也是經(jīng)過對象C的,所以捐友,對象D的直接支配者為對象C淫半。
同理,對象E支配對象G匣砖。到達對象H的可以通過對象D科吭,也可以通過對象E,因此對象D和E都不能支配對象H,而經(jīng)過對象C既可以到達D也可以到達E猴鲫,因此對象C為對象H的支配者对人。
在MAT中,單擊工具欄上的對象支配者樹按鈕拂共,可以打開對象支配者視圖牺弄。
補充1:?再談內(nèi)存泄露
內(nèi)存泄露的理解與分類:
何為內(nèi)存泄露(memory?leak)?
可達性分析算法來判斷對象是否是不再使用的對象宜狐,本質(zhì)都是判斷一個對象是否還被引用势告。那么對于這種情況下蛇捌,由于代碼的實現(xiàn)不同就會出現(xiàn)很多種內(nèi)存泄露問題(讓JVM誤以為此對象還在引用中,無法回收咱台,造成內(nèi)存泄露)络拌。
內(nèi)存泄露(memory?leak)的理解:
嚴格來說,只有對象不會再被程序用到了回溺,但是GC又不能回收它們的情況春贸,才叫做內(nèi)存泄露。但實際情況中遗遵,可能會出現(xiàn)一些疏忽導致對象的生命周期變得很長甚至導致OOM萍恕,也可以叫做寬泛意義上的內(nèi)存泄露。
對象X引用對象Y车要,X的生命周期比Y的生命周期長雄坪;那么當Y的生命周期結束的時候,X依然引用的Y屯蹦,這時候,垃圾回收器是不會回收對象Y的绳姨;如果對象X還引用著生命周期比較短的A/B/C登澜,對象A又引用著對象a、b飘庄、c這樣就可能造成大量無用的對象不能被回收脑蠕,進而占據(jù)了內(nèi)存資源,造成內(nèi)存泄露跪削,直至內(nèi)存溢出谴仙。
內(nèi)存泄露和內(nèi)存溢出的關系:
內(nèi)存泄露的增多最終會導致內(nèi)存溢出。
泄露的分類
經(jīng)常發(fā)生:發(fā)生內(nèi)存泄露的代碼被多次執(zhí)行碾盐,每執(zhí)行一次晃跺,泄露一塊內(nèi)存。
偶然發(fā)生:在某些特定情況下才會發(fā)生毫玖。
一次性:發(fā)生內(nèi)存泄露的方法只會執(zhí)行一次掀虎。
隱式泄露:一直站著內(nèi)存不釋放,直到執(zhí)行結束付枫;嚴格的說這個不算泄露烹玉,因為內(nèi)存釋放掉了,但是如果執(zhí)行時間特別長阐滩,也會導致內(nèi)存耗盡二打。
Java內(nèi)存泄露的8種情況:
1.靜態(tài)集合類
靜態(tài)集合類,如HashMap掂榔、LinkedList等等继效。如果這些容器為靜態(tài)的症杏,那么它們的生命周期與JVM程序一致,則容器中的對象在程序結束之前不能被釋放莲趣,從而造成內(nèi)存泄露鸳慈。簡單而言,長生命周期的對象持有短生命周期對象的引用喧伞,盡管短生命周期的對象不再使用走芋,但是因為長生命周期對象持有它的引用而導致不能被回收。
public class MemoryLeak{
? ? static List list = new ArrayList();
? ? public void oomTest(){
? ? ? ? Object obj = new Object();
? ? ? ? List.add(obj);
? ? }
}
2.單例模式
單例模式潘鲫,和靜態(tài)集合導致內(nèi)存泄露的原因類似翁逞,但因為單例的靜態(tài)特性,它的生命周期和JVM的生命周期一樣長溉仑,所以如果單例對象持有外部對象的引用挖函,那么這個外部對象也不會回收,那么就會造成內(nèi)存泄露浊竟。
3.內(nèi)部類持有外部類
內(nèi)部類持有外部類怨喘,如果一個外部類的實例對象的方法返回了一個內(nèi)部類的實例對象。這個內(nèi)部類對象被長期持有了振定,即使那個外部類實例對象不再被使用必怜,但由于內(nèi)部類持有外部類的實例對象,這個外部類對象將不會被垃圾回收后频,這也會造成內(nèi)存泄露梳庆。
4.各種連接,如數(shù)據(jù)庫連接卑惜、網(wǎng)絡連接和IO連接等膏执。
在對數(shù)據(jù)庫進行操作的過程中,首先需要建立與數(shù)據(jù)庫的連接露久,當不再使用時更米,需要調(diào)用close方法來釋放與數(shù)據(jù)庫的連接。只有在連接被關閉后毫痕,垃圾回收器才會回收相應的對象壳快。否則,如果在數(shù)據(jù)庫訪問過程中镇草,對Connection眶痰、Statement或ResultSet不顯性的關閉,將會造成大量對象無法回收梯啤,從而引起內(nèi)存泄露竖伯。
public static void main(String args[]){
? ? try{
? ? ? ? Connection conn = null;
? ? ? ? Class.forName("com.mysql.jdbc.Driver");
? ? ? ? conn = DriverManager.getConnection("url", "", "");
? ? ? ? Statement stmt = conn.createStatement();
? ? ? ? ResultSet rs = stmt.executeQuery("...");
? ? }catch(Exception e){//異常日志
? ? }finally{
? ? ? ? //關閉結果集 Statement
? ? ? ? //關閉聲明的對象 ResultSet
? ? ? ? //關閉連接 Connection
? ? }
}
5.變量不合理的作用域
變量不合理的作用域。一般而言,一個變量定義的范圍可能大于器使用范圍七婴,很有可能會造成內(nèi)存泄露祟偷。另一方面,如果沒有即時地把對象設置為null打厘,很有可能導致內(nèi)存泄露的發(fā)生修肠。
public class UsingRandom{
? ? private String msg;
? ? public void receiveMsg(){
? ? ? ? readFromNet(); //從網(wǎng)絡中接受數(shù)據(jù)保存到msg中
? ? ? ? saveDB();//把數(shù)據(jù)保存到數(shù)據(jù)庫中
? ? }
}
如上面這個偽代碼,通過readFromNet方法把接受的消息保存在變量msg中户盯,然后調(diào)用saveDB方法把msg中的內(nèi)容保存到數(shù)據(jù)庫中嵌施,此時msg已經(jīng)沒用了,由于msg的生命周期與對象的生命周期相同莽鸭,此時msg還不能回收吗伤,因此造成了內(nèi)存泄露。
實際上這個msg變量可以放在receiveMsg內(nèi)部硫眨,當方法使用完足淆,那么msg的生命周期也就結束,此時就可以回收了礁阁。還有一種方法巧号,就是在使用哇msg后,把msg設為null姥闭,這樣垃圾回收器也會會后msg的內(nèi)存空間丹鸿。
6.改變哈希值
改變哈希值,當一個對象被存儲進HashSet集合中以后泣栈,就不能修改這個對象中的那些參與計算哈希值的字段了。否則弥姻,對象修改后的哈希值與最初存儲進HashSet集合中的哈希值就不同了南片,在這種情況下,即使contains方法使用該對象的當前引用作為參數(shù)去HashSet集合中檢索對象庭敦,也將找不到對象的結果疼进。這也會導致無法從HashSet集合中單獨刪除當前對象,造成內(nèi)存泄露秧廉。
這也是為什么String會被設置為不可變類型伞广,我們可以放心把String存入HashSet,或者把String?當做HashMap的key值疼电;當我們想把自己定義的類保存到散列表的時候嚼锄,需要保證對象的hashcode不可變。
7.緩存泄露
內(nèi)存泄露的另一個常見來源是緩存蔽豺,一旦你把對象放到緩存中区丑,就很容易被遺忘。比如:之前項目再一次上線的時候,應用啟動奇慢直至夯死沧侥,就是因為代碼中會加載一個表中的數(shù)據(jù)到緩存(內(nèi)存)中可霎,測試環(huán)境只有幾百條數(shù)據(jù),但是生產(chǎn)環(huán)境有幾百萬的數(shù)據(jù)宴杀。
對于這個問題癣朗,可以使用WeakHashMap代表緩存,此種Map的特點是旺罢,當除了自身有對key的引用外旷余,此key沒有其他引用那么此map會自動丟棄此值。
8.內(nèi)存泄露的第三個常見來源是監(jiān)聽器和其他回調(diào)主经,如果客戶端在你實現(xiàn)的API中注冊回調(diào)荣暮,卻沒有顯示取消,那么就會積聚罩驻。需要確彼胨郑回調(diào)立即被當做垃圾回收的最佳方法是只保存它的弱引用,例如將他們保存為WeakHashMap中的鍵惠遏。