記憶體溢位的多種原因及優化方法
對於JVM的記憶體寫過的文章已經有點多了,而且有點爛了,不過說那麼多大多數在解決OOM的情況,於此,本文就只闡述這個內容,攜帶一些分析和理解和部分擴充套件內容,也就是JVM宕機中的一些問題,OK,下面說下OOM的常見情況:
第一類記憶體溢位,也是大家認為最多,第一反應認為是的記憶體溢位,就是堆疊溢位:
那什麼樣的情況就是堆疊溢位呢?當你看到下面的關鍵字的時候它就是堆疊溢位了:
java.lang.OutOfMemoryError: ......java heap space.....
也就是當你看到heap相關的時候就肯定是堆疊溢位了,此時如果程式碼沒有問題的情況下,適當調整-Xmx和-Xms是可以避免的,不過一定是程式碼沒有問題的前提,為什麼會溢位呢,要麼程式碼有問題,要麼訪問量太多並且每個訪問的時間太長或者資料太多,導致資料釋放不掉,因為垃圾回收器是要找到那些是垃圾才能回收,這裡它不會認為這些東西是垃圾,自然不會去回收了;主意這個溢位之前,可能系統會提前先報錯關鍵字為:
java.lang.OutOfMemoryError:GC over head limit exceeded
這種情況是當系統處於高頻的GC狀態,而且回收的效果依然不佳的情況,就會開始報這個錯誤,這種情況一般是產生了很多不可以被釋放的物件,有可能是引用使用不當導致,或申請大物件導致,但是java heap space的記憶體溢位有可能提前不會報這個錯誤,也就是可能記憶體就直接不夠導致,而不是高頻GC.
第二類記憶體溢位,PermGen的溢位,或者PermGen 滿了的提示,你會看到這樣的關鍵字:
關鍵資訊為:
java.lang.OutOfMemoryError: PermGen space
原因:系統的程式碼非常多或引用的第三方包非常多、或程式碼中使用了大量的常量、或通過intern注入常量、或者通過動態程式碼載入等方法,導致常量池的膨脹,雖然JDK 1.5以後可以通過設定對永久帶進行回收,但是我們希望的是這個地方是不做GC的,它夠用就行,所以一般情況下今年少做類似的操作,所以在面對這種情況常用的手段是:增加-XX:PermSize和-XX:MaxPermSize的大小。
第三類記憶體溢位:在使用ByteBuffer中的allocateDirect()的時候會用到,很多javaNIO的框架中被封裝為其他的方法
溢位關鍵字:
java.lang.OutOfMemoryError: Direct buffer memory
如果經常有類似的操作,可以考慮設定引數:-XX:MaxDirectMemorySize
第四類記憶體溢位錯誤:
溢位關鍵字:
java.lang.StackOverflowError
這個引數直接說明一個內容,就是-Xss太小了,我們申請很多區域性呼叫的棧針等內容是存放在使用者當前所持有的執行緒中的,執行緒在jdk 1.4以前預設是256K,1.5以後是1M,如果報這個錯,只能說明-Xss設定得太小,當然有些廠商的JVM不是這個引數,本文僅僅針對Hotspot VM而已;不過在有必要的情況下可以對系統做一些優化,使得-Xss的值是可用的。
第五類記憶體溢位錯誤:
溢位關鍵字:
java.lang.OutOfMemoryError: unable to create new native thread
上面第四種溢位錯誤,已經說明了執行緒的記憶體空間,其實執行緒基本只佔用heap以外的記憶體區域,也就是這個錯誤說明除了heap以外的區域,無法為執行緒分配一塊記憶體區域了,這個要麼是記憶體本身就不夠,要麼heap的空間設定得太大了,導致了剩餘的記憶體已經不多了,而由於執行緒本身要佔用記憶體,所以就不夠用了,說明了原因,如何去修改,不用我多說,你懂的。
第六類記憶體溢位:
溢位關鍵字
java.lang.OutOfMemoryError: request {} byte for {}out of swap
這類錯誤一般是由於地址空間不夠而導致。
六大類常見溢位已經說明JVM中99%的溢位情況,要逃出這些溢位情況非常困難,除非一些很怪異的故障問題會發生,比如由於實體記憶體的硬體問題,導致了code cache的錯誤(在由byte code轉換為native code的過程中出現,但是概率極低),這種情況記憶體 會被直接crash掉,類似還有swap的頻繁互動在部分系統中會導致系統直接被crash掉,OS地址空間不夠的話,系統根本無法啟動,呵呵;JNI的濫用也會導致一些本地記憶體無法釋放的問題,所以儘量避開JNI;socket連線資料開啟過多的socket也會報類似:IOException: Too many open files等錯誤資訊。
JNI就不用多說了,儘量少用,除非你的程式碼太牛B了,我無話可說,呵呵,這種記憶體如果沒有在被呼叫的語言內部將記憶體釋放掉(如C語言),那麼在程序結束前這些記憶體永遠釋放不掉,解決辦法只有一個就是將程序kill掉。
另外GC本身是需要記憶體空間的,因為在運算和中間資料轉換過程中都需要有記憶體,所以你要保證GC的時候有足夠的記憶體哦,如果沒有的話GC的過程將會非常的緩慢。
順便這裡就提及一些新的CMS GC的內容和策略(有點亂,每次寫都很亂,但是能看多少看多少吧):
首先我再寫一次一前部落格中的已經寫過的內容,就是很多引數沒啥建議值,建議值是自己在現場根據實際情況科學計算和測試得到的綜合效果,建議值沒有絕對好的,而且預設值很多也是有問題的,因為不同的版本和廠商都有很大的區別,預設值沒有永久都是一樣的,就像-Xss引數的變化一樣,要看到你當前的java程式heap的大致情況可以這樣看看(以下引數是隨便設定的,並不是什麼預設值):
$sudo jmap -heap `pgrep java`
Attaching to process ID 4280, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 19.1-b02
using thread-local object allocation.
Parallel GC with 8 thread(s)
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 1073741824 (1024.0MB)
NewSize = 134217728 (128.0MB)
MaxNewSize = 134217728 (128.0MB)
OldSize = 5439488 (5.1875MB)
NewRatio = 2
SurvivorRatio = 8
PermSize = 134217728 (128.0MB)
MaxPermSize = 268435456 (256.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 85721088 (81.75MB)
used = 22481312 (21.439849853515625MB)
free = 63239776 (60.310150146484375MB)
26.22611602876529% used
From Space:
capacity = 24051712 (22.9375MB)
used = 478488 (0.45632171630859375MB)
free = 23573224 (22.481178283691406MB)
1.9894134770946867% used
To Space:
capacity = 24248320 (23.125MB)
used = 0 (0.0MB)
free = 24248320 (23.125MB)
0.0% used
PS Old Generation
capacity = 939524096 (896.0MB)
used = 16343864 (15.586723327636719MB)
free = 923180232 (880.4132766723633MB)
1.7395896571023124% used
PS Perm Generation
capacity = 134217728 (128.0MB)
used = 48021344 (45.796722412109375MB)
free = 86196384 (82.20327758789062MB)
35.77868938446045% used
付:sudo是需要拿到管理員許可權,如果你的系統許可權很大那麼就不需要了,最後的grep java那個內容如果不對,可以直接通過jps或者ps命令將和java相關的程序號直接寫進去,如:java -map 4280,這個引數其實完全可以通過jstat工具來替代,而且看到的效果更加好,這個引數在線上應用中,儘量少用(尤其是高併發的應用中),可能會觸發JVM的bug,導致應用掛起;在jvm 1.6u14後可以編寫任意一段程式,然後在執行程式的時候,增加引數為:-XX:+PrintFlagsFinal來輸出當前JVM中執行時的引數值,或者通過jinfo來檢視,jinfo是非常強大的工具,可以對部分引數進行動態修改,當然記憶體相關的東西是不能修改的,只能增加一些不是很相關的引數,有關JVM的工具使用,後續文章中如果有機會我們再來探討,不是本文的重點;補充:關於引數的預設值對不同的JVM版本、不同的廠商、運行於不同的環境(一般和位數有關係)預設值會有區別。
OK,再說下反覆的一句,沒有必要的話就不要亂設定引數,引數不是拿來玩的,預設的引數對於這門JDK都是有好處的,關鍵是否適合你的應用場景,一般來講你常規的只需要設定以下幾個引數就可以了:
-server 表示為伺服器端,會提供很多伺服器端預設的配置,如並行回收,而伺服器上一般這個引數都是預設的,所以都是可以省掉,與之對應的還有一個-client引數,一般在64位機器上,JVM是預設啟動-server引數,也就是預設啟動並行GC的,但是是ParallelGC而不是ParallelOldGC,兩者演算法不同(後面會簡單說明下),而比較特殊的是windows 32位上預設是-client,這兩個的區別不僅僅是預設的引數不一樣,在jdk包下的jre包下一般會包含client和server包,下面分別對應啟動的動態連結庫,而真正看到的java、javac等相關命令指示一個啟動導向,它只是根據命令找到對應的JVM並傳入jvm中進行啟動,也就是看到的java.exe這些檔案並不是jvm;說了這麼多,最終總結一下就是,-server和-client就是完全不同的兩套VM,一個用於桌面應用,一個用於伺服器的。
-Xmx 為Heap區域的最大值
-Xms 為Heap區域的初始值,線上環境需要與-Xmx設定為一致,否則capacity的值會來回飄動,飄得你心曠神怡,你懂的。
-Xss(或-ss) 這個其實也是可以預設的,如果你真的覺得有設定的必要,你就改下吧,1.5以後是1M的預設大小(指一個執行緒的native空間),如果程式碼不多,可以設定小點來讓系統可以接受更大的記憶體。注意,還有一個引數是-XX:ThreadStackSize,這兩個引數在設定的過程中如果都設定是有衝突的,一般按照JVM常理來說,誰設定在後面,就以誰為主,但是最後發現如果是在1.6以上的版本,-Xss設定在後面的確都是以-Xss為主,但是要是-XX:ThreadStackSize設定在後面,主執行緒還是為-Xss為主,而其它執行緒以-XX:ThreadStackSize為主,主執行緒做了一個特殊判定處理;單獨設定都是以本身為主,-Xss不設定也不會採用其預設值,除非兩個都不設定會採用-Xss的預設值。另外這個引數針對於hotspot的vm,在IBM的jvm中,還有一個引數為-Xoss,主要原因是IBM在對棧的處理上有運算元棧和方法棧等各種不同的棧種類,而hotspot不管是什麼棧都放在一個私有的執行緒內部的,不區分是什麼棧,所以只需要設定一個引數,而IBM的J9不是這樣的;有關棧上的細節,後續我們有機會專門寫文章來說明。
-XX:PermSize與-XX:MaxPermSize兩個包含了class的裝載的位置,或者說是方法區(但不是本地方法區),在Hotspot預設情況下為64M,主意全世界的JVM只有hostpot的VM才有Perm的區域,或者說只有hotspot才有對使用者可以設定的這塊區域,其他的JVM都沒有,其實並不是沒有這塊區域,而是這塊區域沒有讓使用者來設定,其實這塊區域本身也不應該讓使用者來設定,我們也沒有一個明確的說法這塊空間必須要設定多大,都是拍腦袋設定一個數字,如果釋出到線上看下如果用得比較多,就再多點,如果用的少,就減少點,而這塊區域和效能關鍵沒有多大關係,只要能裝下就OK,並且時不時會因為Perm不夠而導致Full GC,所以交給開發者來調節這個引數不知道是怎麼想的;所以Oracle將在新一代JVM中將這個區域徹底刪掉,也就是對使用者透明,G1的如果真正穩定起來,以後JVM的啟動引數將會非常簡單,而且理論上管理再大的記憶體也是沒有問題的,其實G1(garbage first,一種基於region的垃圾收集回收器)已經在hotspot中開始有所試用,不過目前效果不好,還不如CMS呢,所以只是試用,G1已經作為ORACLE對JVM研發的最高重點,CMS自現在最高版本後也不再有新功能(可以修改bug),該專案已經進行5年,尚未釋出正式版,CMS是四五年前釋出的正式版,但是是最近一兩年才開始穩定,而G1的複雜性將會遠遠超越CMS,所以要真正使用上G1還有待考察,全世界目前只有IBM J9真正實現了G1論文中提到的思想(論文於05年左右發表),IBM已經將J9應用於websphere中,但是並不代表這是全世界最好的jvm,全世界最好的jvm是Azul(無停頓垃圾回收演算法和一個零開銷的診斷/監控工具),幾乎可以說這個jvm是沒有暫停的,在全世界很多頂尖級的公司使用,不過價格非常貴,不能直接使用,目前這個jvm的主導者在研究JRockit,而目前hotspot和JRockit都是Oracle的,所以他們可能會合並,所以我們應該對JVM的效能充滿信心。
也就是說你常用的情況下只需要設定4個引數就OK了,除非你的應用有些特殊,否則不要亂改,那麼來看看一些其他情況的引數吧:
先來看個不大常用的,就是大家都知道JVM新的物件應該說幾乎百分百的在Eden裡面,除非Eden真的裝不下,我們不考慮這種變態的問題,因為線上環境Eden區域都是不小的,來降低GC的次數以及全域性 GC的概率;而JVM習慣將記憶體按照較為連續的位置進行分配,這樣使得有足夠的記憶體可以被分配,減少碎片,那麼對於記憶體最後一個位置必然就有大量的徵用問題,JVM在高一點的版本里面提出了為每個執行緒分配一些私有的區域來做來解決這個問題,而1.5後的版本還可以動態管理這些區域,那麼如何自己設定和檢視這些區域呢,看下英文全稱為:Thread Local Allocation Buffer,簡稱就是:TLAB,即記憶體本地的持有的buffer,設定引數有:
-XX:+UseTLAB 啟用這種機制的意思
-XX:TLABSize=<size in kb> 設定大小,也就是本地執行緒中的私有區域大小(只有這個區域放不下才會到Eden中去申請)。
-XX:+ResizeTLAB 是否啟動動態修改
這幾個引數在多CPU下非常有用。
-XX:+PrintTLAB 可以輸出TLAB的內容。
下面再閒扯些其它的引數:
如果你需要對Yong區域進行並行回收應該如何修改呢?在jdk1.5以後可以使用引數:
-XX:+UseParNewGC
注意: 與它衝突的引數是:-XX:+UseParallelOldGC和-XX:+UseSerialGC,如果需要用這個引數,又想讓整個區域是並行回收的,那麼就使用-XX:+UseConcMarkSweepGC引數來配合,其實這個引數在使用了CMS後,預設就會啟動該引數,也就是這個引數在CMS GC下是無需設定的,後面會提及到這些引數。
預設伺服器上的對Full並行GC策略為(這個時候Yong空間回收的時候啟動PSYong演算法,也是並行回收的):
-XX:+UseParallelGC
另外,在jdk1.5後出現一個新的引數如下,這個對Yong的回收演算法和上面一樣,對Old區域會有所區別,上面對Old回收的過程中會做一個全域性的Compact,也就是全域性的壓縮操作,而下面的演算法是區域性壓縮,為什麼要區域性壓縮呢?是因為JVM發現每次壓縮後再邏輯上資料都在Old區域的左邊位置,申請的時候從左向右申請,那麼生命力越長的物件就一般是靠左的,所以它認為左邊的物件就是生命力很強,而且較為密集的,所以它針對這種情況進行部分密集,但是這兩種演算法mark階段都是會暫停的,而且存活的物件越多活著的越多;而ParallelOldGC會進行部分壓縮演算法(主意一點,最原始的copy演算法是不需要經過mark階段,因為只需要找到一個或活著的就只需要做拷貝就可以,而Yong區域借用了Copy演算法,只是唯一的區別就是傳統的copy演算法是採用兩個相同大小的記憶體來拷貝,浪費空間為50%,所以分代的目標就是想要實現很多優勢所在,認為新生代85%以上的物件都應該是死掉的,所以S0和S1一般並不是很大),該演算法為jdk 1.5以後對於絕大部分應用的最佳選擇。
-XX:+UseParallelOldGC
-XX:ParallelGCThread=12:並行回收的執行緒數,最好根據實際情況而定,因為執行緒多往往存在徵用排程和上下文切換的開銷;而且也並非CPU越多執行緒數也可以設定越大,一般設定為12就再增加用處也不大,主要是演算法本身內部的徵用會導致其執行緒的極限就是這樣。
設定Yong區域大小:
-Xmn Yong區域的初始值和最大值一樣大
-XX:NewSize和-XX:MaxNewSize如果設定以為一樣大就是和-Xmn,在JRockit中會動態變化這些引數,根據實際情況有可能會變化出兩個Yong區域,或者沒有Yong區域,有些時候會生出來一個半長命物件區域;這裡除了這幾個引數外,還有一個引數是NewRatio是設定Old/Yong的倍數的,這幾個引數都是有衝突的,伺服器端建議是設定-Xmn就可以了,如果幾個引數全部都有設定,-Xmn和-XX:NewSize與-XX:MaxNewSize將是誰設定在後面,以誰的為準,而-XX:NewSize -XX:MaxNewSize與-XX:NewRatio時,那麼引數設定的結果可能會以下這樣的(jdk 1.4.1後):
min(MaxNewSize,max(NewSize, heap/(NewRatio+1)))
-XX:NewRatio為Old區域為Yong的多少倍,間接設定Yong的大小,1.6中如果使用此引數,則預設會在適當時候被動態調整,具體請看下面引數UseAdaptiveSizepollcy 的說明。
三個引數不要同時設定,因為都是設定Yong的大小的。
-XX:SurvivorRatio:該引數為Eden與兩個求助空間之一的比例,注意Yong的大小等價於Eden + S0 + S1,S0和S1的大小是等價的,這個引數為Eden與其中一個S區域的大小比例,如引數為8,那麼Eden就佔用Yong的80%,而S0和S1分別佔用10%。
以前的老版本有一個引數為:-XX:InitialSurivivorRatio,如果不做任何設定,就會以這個引數為準,這個引數的預設值就是8,不過這個引數並不是Eden/Survivor的大小,而是Yong/Survivor,所以所以預設值8,代表每一個S區域的空間大小為Yong區域的12.5%而不是10%。另外順便提及一下,每次大家看到GC日誌的時候,GC日誌中的每個區域的最大值,其中Yong的空間最大值,始終比設定的Yong空間的大小要小一點,大概是小12.5%左右,那是因為每次可用空間為Eden加上一個Survivor區域的大小,而不是整個Yong的大小,因為可用空間每次最多是這樣大,兩個Survivor區域始終有一塊是空的,所以不會加上兩個來計算。
-XX:MaxTenuringThreshold=15:在正常情況下,新申請的物件在Yong區域發生多少次GC後就會被移動到Old(非正常就是S0或S1放不下或者不太可能出現的Eden都放不下的物件),這個引數一般不會超過16(因為計數器從0開始計數,所以設定為15的時候相當於生命週期為16)。
要檢視現在的這個值的具體情況,可以使用引數:-XX:+PrintTenuringDistribution
通過上面的jmap應該可以看出我的機器上的MinHeapFreeRatio和MaxHeapFreeRatio分別為40個70,也就是大家經常說的在GC後剩餘空間小於40%時capacity開始增大,而大於70%時減小,由於我們不希望讓它移動,所以這兩個引數幾乎沒有意義,如果你需要設定就設定引數為:
-XX:MinHeapFreeRatio=40
-XX:MaxHeapFreeRatio=70
JDK 1.6後有一個動態調節板塊的,當然如果你的每一個板塊都是設定固定值,這個引數也沒有用,不過如果是非固定的,建議還是不要動態調整,預設是開啟的,建議將其關掉,引數為:
-XX:+UseAdaptiveSizepollcy 建議使用-XX:-UseAdaptiveSizepollcy關掉,為什麼當你的引數設定了NewRatio、Survivor、MaxTenuringThreshold這幾個引數如果在啟動了動態更新情況下,是無效的,當然如果你設定-Xmn是有效的,但是如果設定的比例的話,初始化可能會按照你的引數去執行,不過執行過程中會通過一定的演算法動態修改,監控中你可能會發現這些引數會發生改變,甚至於S0和S1的大小不一樣。
如果啟動了這個引數,又想要跟蹤變化,那麼就使用引數:-XX:+PrintAdaptiveSizePolicy
上面已經提到,javaNIO中通過Direct記憶體來提高效能,這個區域的大小預設是64M,在適當的場景可以設定大一些。
-XX:MaxDirectMemorySize
一個不太常用的引數:
-XX:+ScavengeBeforeFullGC 預設是開啟狀態,在full GC前先進行minor GC。
對於java堆中如果要設定大頁記憶體,可以通過設定引數:
付:此引數必須在作業系統的核心支援的基礎上,需要在OS級別做操作為:
echo 1024 > /proc/sys/vm/nr_hugepages
echo 2147483647 > /proc/sys/kernel/shmmax
-XX:+UseLargePages
-XX:LargePageSizeInBytes
此時整個JVM都將在這塊記憶體中,否則全部不在這塊記憶體中。
javaIO的臨時目錄設定
-Djava.io.tmpdir
jstack會去尋找/tmp/hsperfdata_admin下去尋找與程序號相同的檔案,32位機器上是沒有問題的,64為機器的是有BUG的,在jdk 1.6u23版本中已經修復了這個bug,如果你遇到這個問題,就需要升級JDK了。
還記得上次說的平均晉升大小嗎,在並行GC時,如果平均晉升大小大於old剩餘空間,則發生full GC,那麼當小於剩餘空間時,也就是平均晉升小於剩餘空間,但是剩餘空間小於eden + 一個survivor的空間時,此時就依賴於引數:
-XX:-HandlePromotionFailure
啟動該引數時,上述情況成立就發生minor gc(YGC),大於則發生full gc(major gc)。
一般預設直接分配的物件如果大於Eden的一半就會直接晉升到old區域,但是也可以通過引數來指定:
-XX:PretenureSizeThreshold=2m 我個人不建議使用這個引數
也就是當申請物件大於這個值就會晉升到old區域。
傳說中GC時間的限制,一個是通過比例限制,一個是通過最大暫停時間限制,但是GC時間能限制麼,呵呵,在增量中貌似可以限制,不過不能限制住GC總體的時間,所以這個引數也不是那麼關鍵。
-XX:GCTimeRatio=
-XX:MaxGCPauseMillis
-XX:GCTimeLimit
要看到真正暫停的時間就一個是看GCDetail的日誌,另一個是設定引數看:
-XX:+PrintGCApplicationStoppedTime
有些人,有些人就是喜歡在程式碼裡面裡頭寫System.gc(),耍酷,這個不是測試程式是線上業務,這樣將會導致N多的問題,不多說了,你應該懂的,不懂的話看下書吧,而RMI是很不聽話的一個鳥玩意,EJB的框架也是基於RMI寫的,RMI為什麼不聽話呢,就是它自己在裡面非要搞個System.gc(),哎,為了放置頻繁的做,頻繁的做,你就將這個命令的執行禁用掉吧,當然程式不用改,不然那些EJB都跑步起來了,呵呵:
-XX:+DisableExplicitGC 預設是沒有禁用掉,寫成+就是禁用掉的了,但是有些時候在使用allocateDirect的時候,很多時候還真需要System.gc來強制回收這塊資源。
記憶體溢位時匯出溢位的錯誤資訊:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/xieyu/logs/ 這個引數指定匯出時的路徑,不然匯出的路徑就是虛擬機器的目標位置,不好找了,預設的檔名是:java_pid<程序號>.hprof,這個檔案可以類似使用jmap -dump:file=....,format=b <pid>來dump類似的內容,檔案字尾都是hprof,然後下載mat工具進行分析即可(不過記憶體有多大dump檔案就多大,而本地分析的時候記憶體也需要那麼大,所以很多時候下載到本地都無法啟動是很正常的),後續文章有機會我們來說明這些工具,另外jmap -dump引數也不要經常用,會導致應用掛起哦;另外此引數只會在第一次輸出OOM的時候才會進行堆的dump操作(java heap的溢位是可以繼續執行再執行的程式的,至於web應用是否服務要看應用伺服器自身如何處理,而c heap區域的溢位就根本沒有dump的機會,因為直接就宕機了,目前系統無法看到c heap的大小以及內部變化,要看大小隻能間接通過看JVM程序的記憶體大小(top或類似引數),這個大小一般會大於heap+perm的大小,多餘的部分基本就可以認為是c heap的大小了,而看內部變化呢只有google perftools可以達到這個目的),如果記憶體過大這個dump操作將會非常長,所以hotspot如果以後想管理大記憶體,這塊必須有新的辦法出來。
最後,用dump出來的檔案,通過mat分析出來的結果往往有些時候難以直接確定到底哪裡有問題,可以看