網(wǎng)站設(shè)計公司 上seo賺錢方法大揭秘
大綱
1.如何對系統(tǒng)的OOM異常進行監(jiān)控和報警
2.如何在JVM內(nèi)存溢出時自動dump內(nèi)存快照
3.Metaspace區(qū)域內(nèi)存溢出時應(yīng)如何解決(OutOfMemoryError: Metaspace)
4.JVM棧內(nèi)存溢出時應(yīng)如何解決(StackOverflowError)
5.JVM堆內(nèi)存溢出時應(yīng)該如何解決(OutOfMemoryError: Java heap space)
1.如何對系統(tǒng)的OOM異常進行監(jiān)控和報警
(1)最佳的解決方案
(2)搭建系統(tǒng)監(jiān)控體系的建議
(1)最佳的解決方案
最佳的OOM監(jiān)控方案就是:建立一套監(jiān)控平臺,比如搭建Zabbix、Open-Falcon之類的監(jiān)控平臺。如果有監(jiān)控平臺,就可以接入系統(tǒng)異常的監(jiān)控和報警,可以設(shè)置當系統(tǒng)出現(xiàn)OOM異常,就發(fā)送報警給對應(yīng)的開發(fā)人員。這是中大型公司里最常用的一種方案。
監(jiān)控平臺一般會監(jiān)控系統(tǒng)的如下幾個層面:
一.機器資源(CPU、磁盤、內(nèi)存、網(wǎng)絡(luò))的負載
二.JVM的GC頻率和內(nèi)存使用率
三.系統(tǒng)自身的業(yè)務(wù)指標
四.系統(tǒng)的異常報錯
(2)搭建系統(tǒng)監(jiān)控體系的建議
建議一:對機器指標進行監(jiān)控
一.首先可以看到機器的資源負載情況
比如CPU負載,可以看到現(xiàn)在CPU目前的使用率有多高。比如磁盤IO負載,包括磁盤上發(fā)生了多少數(shù)據(jù)量的IO、一些IO耗時等。當然一般業(yè)務(wù)系統(tǒng)不會直接讀寫本地磁盤,最多就是寫一些本地日志。但是一般業(yè)務(wù)系統(tǒng)也應(yīng)該關(guān)注本地磁盤的使用量和剩余空間,因為有的系統(tǒng)可能會因為一些代碼bug,導(dǎo)致一直往本地磁盤寫東西。萬一把磁盤空間寫滿了就麻煩了,磁盤滿了也會導(dǎo)致系統(tǒng)無法運行。
二.其次可以看到機器的內(nèi)存使用量
這個是從機器整體層面去看的,看看機器對內(nèi)存使用的一些變化。當然內(nèi)存這塊,比較核心的還是JVM的監(jiān)控,通過監(jiān)控平臺可以看到JVM各個內(nèi)存區(qū)域的使用量的變化的。
三.還有一個比較關(guān)鍵的是JVM的FGC頻率
一般會監(jiān)控一段時間內(nèi)的FGC次數(shù),比如5分鐘內(nèi)發(fā)生了幾次FGC。
四.最后就是機器上的網(wǎng)絡(luò)負載
就是通過網(wǎng)絡(luò)IO讀寫了多少數(shù)據(jù)、一些耗時等。
其實線上機器最容易出問題的主要有三方面:
一是CPU(必須要監(jiān)控CPU的使用率),如果CPU負載過高(如長期超過90%)就報警。
二是內(nèi)存(必須要監(jiān)控內(nèi)存的使用率),如果機器內(nèi)存使用率超過一定閾值(如超90%)則可能內(nèi)存不夠。
三是JVM的FGC頻率,假設(shè)5分鐘內(nèi)發(fā)生了10次FGC,那一定是頻繁FGC了。
建議二:對業(yè)務(wù)指標進行監(jiān)控
另外比較常見的就是對系統(tǒng)的業(yè)務(wù)指標進行監(jiān)控。比如在系統(tǒng)每創(chuàng)建一個訂單時就上報一次監(jiān)控,然后監(jiān)控系統(tǒng)會收集1分鐘內(nèi)的訂單數(shù)量。這樣就可以設(shè)定一個閾值,比如1分內(nèi)要是訂單數(shù)超過100就報警。因為訂單過多可能涉及一些刷單行為,這就是業(yè)務(wù)指標監(jiān)控。
建議三:對系統(tǒng)異常進行監(jiān)控
最后就是對系統(tǒng)中所有的try catch異常報錯,都接入報警。一旦發(fā)現(xiàn)有try catch異常,就上報到監(jiān)控平臺。然后監(jiān)控平臺就能通告這次異常,讓相關(guān)負責人收到報警。比如一旦發(fā)現(xiàn)有OOM異常,就能馬上通知相關(guān)開發(fā)人員。
2.如何在JVM內(nèi)存溢出時自動dump內(nèi)存快照
(1)解決OOM問題的一個初步思路
(2)在OOM時自動dump內(nèi)存快照
(3)一份比較全面的JVM參數(shù)模板
(1)解決OOM問題的一個初步思路
如果發(fā)生OOM,則說明系統(tǒng)中某個區(qū)域的對象太多,塞滿了那個區(qū)域。而且一定是無法回收掉區(qū)域中的那些對象,最終才會導(dǎo)致內(nèi)存溢出。
要解決OOM,首先得知道是什么對象太多了,從而最終導(dǎo)致OOM的。所以必須有一份JVM發(fā)生OOM時的dump內(nèi)存快照,只要有了那個dump內(nèi)存快照,就可以用MAT工具分析什么對象太多了。
那么現(xiàn)在一個關(guān)鍵問題來了:到底怎么做才可以在JVM內(nèi)存溢出時自動dump出一份內(nèi)存快照呢?
(2)在OOM時自動dump內(nèi)存快照
如果JVM發(fā)生OOM,JVM是不是完全來不及處理然后突然進程就沒了?也就是JVM是不是非常突然的、自己都無法控制,就掛掉了?
其實不是的,JVM在發(fā)生OOM之前會盡可能進行GC騰出一些內(nèi)存空間。如果GC后還是沒有空間,放不下對象,?才會觸發(fā)內(nèi)存溢出。所以JVM自己對OOM情況的發(fā)生是完全有把控權(quán)的。
JVM知道什么時候會觸發(fā)OOM,它是在無法放下對象的時候才會觸發(fā)的。因此OOM的發(fā)生并不是突然內(nèi)存太多,連JVM自己都沒反應(yīng)過來就崩潰。
JVM如果知道自己將要發(fā)生OOM了,那么此時完全可以讓它做點事情。比如可以讓JVM在OOM時dump一份內(nèi)存快照,事后只要分析這個內(nèi)存快照,就可以知道是哪些對象導(dǎo)致OOM的了。為此,需要在JVM的啟動參數(shù)中加入如下參數(shù):
?-XX:+HeapDumpOnOutOfMemoryError??
?-XX:HeapDumpPath=/usr/local/app/oom
第一個參數(shù)的意思是:在OOM時自動進行dump內(nèi)存快照。第二個參數(shù)的意思是:把內(nèi)存快照放到哪里。只要加入了這兩個參數(shù),可以事后再獲取OOM時的內(nèi)存快照進行分析。
(3)一份比較全面的JVM參數(shù)模板???????
?-Xms4096M -Xmx4096M -Xmn3072M -Xss1M?
?-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
?-XX:+UseParNewGC?-XX:+UseConcMarkSweepGC?
?-XX:CMSInitiatingOccupancyFaction=92?
?-XX:+UseCMSCompactAtFullCollection?-XX:CMSFullGCsBeforeCompaction=0?
?-XX:+CMSParallelInitialMarkEnabled?-XX:+CMSScavengeBeforeRemark?
?-XX:+DisableExplicitGC?
?-XX:+PrintGCDetails?-Xloggc:gc.log?
?-XX:+HeapDumpOnOutOfMemoryError?-XX:HeapDumpPath=/usr/local/app/oom
這份JVM參數(shù)模板基本上涵蓋了所有需要的一些參數(shù)。首先是各個內(nèi)存區(qū)域的大小分配,這個需要精心調(diào)優(yōu)的。其次是兩種垃圾回收器的指定。接著是一些常規(guī)的CMS垃圾回收參數(shù),可優(yōu)化偶爾發(fā)生的FGC性能。最重要的,就是平時要打印出GC日志。GC日志可以配合jstat工具分析GC頻率和性能的時候使用。jstat可分析出GC頻率,但每次具體的GC情況則要結(jié)合GC日志來看。還有次重要的,就是發(fā)生OOM時能自動dump內(nèi)存快照。這樣即使突然發(fā)生OOM,且事后才知道,都可以分析當時的內(nèi)存快照。
3.Metaspace區(qū)域內(nèi)存溢出時應(yīng)如何解決(OutOfMemoryError: Metaspace)
(1)解決思路
(2)示例代碼
(3)分析GC日志
(4)分析內(nèi)存快照
(1)解決思路
面對Metaspace區(qū)域內(nèi)存溢出,思路如下:首先分析GC日志,然后再讓JVM自動dump出內(nèi)存快照。最后用MAT來分析這份內(nèi)存快照,從內(nèi)存快照里尋找OOM的原因。
(2)示例代碼???????
public?class?Demo1?{public?static?void?main(String[] args)?{long?counter =?0;while?(true) {Enhancer enhancer =?new?Enhancer();enhancer.setSuperclass(Car.class);enhancer.setUseCache(false);enhancer.setCallback(new?MethodInterceptor() {@Overridepublic?Object?intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable?{if?(method.getName().equals("run")) {System.out.println("Check before run");return?methodProxy.invokeSuper(o, objects);}?else?{return?methodProxy.invokeSuper(o, objects);}}});Car car = (Car) enhancer.create();car.run();System.out.println("目前創(chuàng)建了"?+ (++counter) +?"個Car類的子類了");}}static?class?Car?{public?void?run()?{System.out.println("Run...");}}static?class?SafeCar?extends?Car?{@Overridepublic?void?run()?{System.out.println("Safe Run...");super.run();}}
}
接著JVM參數(shù)修改為如下,因為我們想看一下GC日志和導(dǎo)出內(nèi)存快照。???????
?-XX:+UseParNewGC
?-XX:+UseConcMarkSweepGC
?-XX:MetaspaceSize=10m
?-XX:MaxMetaspaceSize=10m
?-XX:+PrintGCDetails
?-Xloggc:gc.log
?-XX:+HeapDumpOnOutOfMemoryError
?-XX:HeapDumpPath=./
注意,上面那個HeapDumpPath參數(shù)調(diào)整為當前項目下,這樣方便查看。
(3)分析GC日志
接下來用上述JVM參數(shù)運行這段程序,會發(fā)現(xiàn)項目下多了兩個文件:一個是gc.log,一個是java_pid910.hprof。當然不同的機器運行這個程序,導(dǎo)出的hprof文件的名字是不太一樣的,因為hprof文件會用PID進程id作為文件名字。
步驟一:首先分析gc.log
分析它是如何不斷往Metaspace區(qū)放入大量生成的類,然后觸發(fā)FGC的。接著回收Metaspace區(qū),回收后還是無法放下更多類,才拋出OOM。
步驟二:然后用MAT分析OOM時的內(nèi)存快照
也就是使用MAT工具找到Metaspace內(nèi)存溢出問題的原因,GC日志如下:???????
CommandLine?flags:?-XX:CompressedClassSpaceSize=2097152?-XX:+HeapDumpOnOutOfMemoryError?-XX:HeapDumpPath=./ -XX:InitialHeapSize=268435456?-XX:MaxHeapSize=4294967296?-XX:MaxMetaspaceSize=10485760?-XX:MaxNewSize=348966912?-XX:MaxTenuringThreshold=6?-XX:MetaspaceSize=10485760?-XX:OldPLABSize=16?-XX:+PrintGC?-XX:+PrintGCDetails?-XX:+PrintGCTimeStamps?-XX:+UseCompressedClassPointers?-XX:+UseCompressedOops?-XX:+UseConcMarkSweepGC?-XX:+UseParNewGC?
0.716: [GC?(Allocation?Failure)?0.717: [ParNew: 139776K->2677K(157248K),?0.0038770?secs] 139776K->2677K(506816K),?0.0041376?secs] [Times: user=0.03?sys=0.01, real=0.00?secs]
0.771: [Full?GC?(Metadata?GC?Threshold)?0.771: [CMS: 0K->2161K(349568K),?0.0721349?secs] 20290K->2161K(506816K), [Metaspace: 9201K->9201K(1058816K)],?0.0722612?secs] [Times: user=0.12?sys=0.03, real=0.08?secs]
0.843: [Full?GC?(Last?ditch collection)?0.843: [CMS: 2161K->1217K(349568K),?0.0164047?secs] 2161K->1217K(506944K), [Metaspace: 9201K->9201K(1058816K)],?0.0165055?secs] [Times: user=0.03?sys=0.00, real=0.01?secs]
0.860: [GC?(CMS?Initial?Mark) [1?CMS-initial-mark:?1217K(349568K)] 1217K(506944K),?0.0002251?secs] [Times: user=0.00?sys=0.00, real=0.00?secs]
0.860: [CMS-concurrent-mark-start]
0.878: [CMS-concurrent-mark:?0.003/0.018?secs] [Times: user=0.05?sys=0.01, real=0.02?secs]
0.878: [CMS-concurrent-preclean-start]
Heappar new generation ? total 157376K, used 6183K [0x00000005ffe00000,?0x000000060a8c0000,?0x0000000643790000)eden space 139904K, ??4% used [0x00000005ffe00000,?0x0000000600409d48,?0x00000006086a0000)from space 17472K, ??0% used [0x00000006086a0000,?0x00000006086a0000,?0x00000006097b0000)to ? space 17472K, ??0% used [0x00000006097b0000,?0x00000006097b0000,?0x000000060a8c0000)concurrent mark-sweep generation total 349568K, used 1217K [0x0000000643790000,?0x0000000658cf0000,?0x00000007ffe00000)
Metaspace? ? ? ?used 9229K, capacity 10146K, committed 10240K, reserved 1058816Kclass?space ? ?used 794K, capacity 841K, committed 896K, reserved 1048576K
一.第一行GC日志如下
0.716: [GC (Allocation Failure)?0.717: [ParNew:?139776K->2677K(157248K),?0.0038770?secs]?139776K->2677K(506816K),?0.0041376?secs] [Times: user=0.03?sys=0.01, real=0.00?secs]
這行日志,這是第一次GC,它本身是一個Allocation Failure的問題。也就是說,在Eden區(qū)中分配對象時,已經(jīng)發(fā)現(xiàn)Eden區(qū)內(nèi)存不足了,于是就觸發(fā)了一次YGC。
二.那么這個對象到底是什么對象
回到代碼中,可知Enhancer是一個對象,它是用來生成類的,如下所示:
Enhancer?enhancer?=?new?Enhancer();
接著會基于new Enhancer生成類對象來生成那個子類的對象,如下所示:
Car?car?=?(Car) enhancer.create();
上述代碼會在while(true)里不停創(chuàng)建Enhancer對象和Car的子類對象,因此Eden區(qū)慢慢就會被占滿了,于是會看到上述日志:
[ParNew: 139776K->2677K(157248K), 0.0038770 secs]?
這行日志就是說:在默認的內(nèi)存分配策略下,新生代一共可用空間是150M左右。然后大概用到140M,Eden區(qū)都占滿了,就會觸發(fā)Allocation Failure。即沒有Eden區(qū)的空間去分配對象了,此時就只能觸發(fā)YGC。
三.接著來看如下GC日志
0.771: [Full GC (Metadata GC Threshold)?0.771: [CMS:?0K->2161K(349568K),?0.0721349?secs]?20290K->2161K(506816K), [Metaspace:?9201K->9201K(1058816K)],?0.0722612?secs] [Times: user=0.12?sys=0.03, real=0.08?secs]
這行日志說明就是發(fā)生FGC了,而且通過Metadata GC Threshold清晰看到,是Metaspace區(qū)域滿了,這時看后面的日志:
20290K->2161K(506816K);
這就是說堆內(nèi)存(新生代+老年代)一共是500M,有20M被使用了,這20M的內(nèi)存是被新生代使用的。
然后FGC必然會帶著一次YGC,因此這次FGC執(zhí)行了YGC,所以回收了很多對象,剩下2161K的對象,這2161K的對象大概就是JVM的一些內(nèi)置對象。
然后直接就把這些對象都放入老年代,為什么呢?因為后面的日志:
[CMS: 0K->2161K(349568K), 0.0721349 secs]
明顯表明,FGC帶著CMS進行了老年代的Old GC,結(jié)果人家本來是0K。然后從新生代轉(zhuǎn)移來了2161K的對象,所以老年代變成2161K了。
接著看日志:?
[Metaspace: 9201K->9201K(1058816K)]
這說明此時Metaspace區(qū)域已經(jīng)使用了差不多9M左右的內(nèi)存了。JVM發(fā)現(xiàn)離限制的10M內(nèi)存很接近了,于是觸發(fā)了FGC。但是對Metaspace進行GC后發(fā)現(xiàn)類的對象全部都還存活,因此還是剩余9M左右的類在Metaspace里。
四.接著來看下一行GC日志
0.843: [Full?GC (Last?ditch collection)?0.843: [CMS:?2161K->1217K(349568K),?0.0164047?secs]?2161K->1217K(506944K), [Metaspace:?9201K->9201K(1058816K)],?0.0165055?secs] [Times:?user=0.03?sys=0.00,?real=0.01?secs]
接著又是一次FGC。其中的Last ditch collection,說明這是最后一次拯救的機會了。因為之前Metaspace回收了一次但發(fā)現(xiàn)沒有類可以回收,所以新的類無法放入Metaspace了。因此才再最后試一試FGC,看看能不能回收掉一些。
結(jié)果發(fā)現(xiàn)還是:
[Metaspace: 9201K->9201K(1058816K)],?0.0165055?secs]
這說明Metaspace區(qū)域還是無法回收掉任何類,Metaspace區(qū)幾乎還是占滿了設(shè)置的10M內(nèi)存。
五.繼續(xù)看如下GC日志???????
0.860: [GC (CMS?Initial?Mark) [1?CMS-initial-mark:?1217K(349568K)]?1217K(506944K),?0.0002251?secs] [Times:?user=0.00?sys=0.00,?real=0.00?secs]
0.860: [CMS-concurrent-mark-start]
0.878: [CMS-concurrent-mark:?0.003/0.018?secs] [Times:?user=0.05?sys=0.01,?real=0.02?secs]
0.878: [CMS-concurrent-preclean-start]
Heappar?new?generation ? total?157376K, used?6183K [0x00000005ffe00000,?0x000000060a8c0000,?0x0000000643790000)eden space?139904K, ??4%?used [0x00000005ffe00000,?0x0000000600409d48,?0x00000006086a0000)from?space?17472K, ??0%?used [0x00000006086a0000,?0x00000006086a0000,?0x00000006097b0000)to? ?space?17472K, ??0%?used [0x00000006097b0000,?0x00000006097b0000,?0x000000060a8c0000)concurrent mark-sweep generation total?349568K, used?1217K [0x0000000643790000,?0x0000000658cf0000,?0x00000007ffe00000)
Metaspace ? ? ? used?9229K, capacity?10146K, committed?10240K, reserved?1058816Kclass space ? ?used?794K, capacity?841K, committed?896K, reserved?1048576K
接著JVM就直接退出了,退出的時候打印了當前內(nèi)存的一個情況:年輕代和老年代幾乎沒占用。但是Metaspace的capacity是10M,使用了9M左右。無法再繼續(xù)使用了,所以觸發(fā)了內(nèi)存溢出。
此時在控制臺會打印出如下的信息:???????
Caused by: java.lang.OutOfMemoryError: Metaspace
? ? at java.lang.ClassLoader.defineClass1(Native Method)
? ? at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
? ? ...?11?more
明確拋出異常表明OutOfMemoryError,原因就是Metaspace區(qū)域滿了。
因此可以假設(shè)是Metaspace內(nèi)存溢出了,然后我們自己監(jiān)控到了異常。此時直接去線上機器看一下GC日志和異常信息。通過上述分析就能知道系統(tǒng)是如何運行、觸發(fā)幾次GC后引發(fā)內(nèi)存溢出。
(4)分析內(nèi)存快照
當我們知道是Metaspace引發(fā)的內(nèi)存溢出后:就把內(nèi)存快照文件從線上機器拷回本地電腦,打開MAT工具進行分析。如下圖示:
從上圖可以看到實例最多的就是AppClassLoader。為什么會有這么多的ClassLoader呢?一看就是CGLIB之類的東西在動態(tài)生成類的時候搞出來的,此時我們可以點擊上圖的Details進去看看。
為什么有一大堆在Demo1中動態(tài)生成的Car$$EnhancerByCGLIB類?看到截圖中的Object數(shù)組里出現(xiàn)的很多這種類,就已經(jīng)很清晰說明了:某個類生成了大量動態(tài)類EnhancerByCGLIB,填滿了Metaspace區(qū)。所以此時直接去代碼里排查動態(tài)生成類的代碼即可。
解決這個問題的辦法也很簡單:對Enhancer進行緩存,只需要一個Enhancer實例即可,不需要無限制地生成類。
4.JVM棧內(nèi)存溢出時應(yīng)如何解決(StackOverflowError)
(1)棧內(nèi)存溢出能否按照之前的方法解決
(2)代碼示例
(3)運行代碼后分析異常報錯信息的調(diào)用棧
(1)棧內(nèi)存溢出能否按照之前的方法解決
也就是說,GC日志、內(nèi)存快照,這些東西對解決棧內(nèi)存溢出有幫助嗎?其實棧內(nèi)存溢出跟堆內(nèi)存是沒有關(guān)系的,因為它的本質(zhì)是一個線程的棧中壓入了過多調(diào)用方法的棧楨。比如調(diào)用了幾千次方法就會壓入幾千個方法棧楨,此時就會導(dǎo)致線程的棧內(nèi)存不足,無法放入更多棧楨。
GC日志對棧內(nèi)存溢出是沒有用的,因為GC日志主要分析的是堆內(nèi)存和Metaspace區(qū)域的一些GC情況。就線程的棧內(nèi)存和棧楨而言,不存在所謂的GC。
線程調(diào)用一個方法時,會在線程的虛擬機棧里壓入方法棧楨。接著線程執(zhí)行完該方法后,方法棧楨會從線程的虛擬機棧里出棧。最后一個線程運行完畢時,它的虛擬機棧內(nèi)存就會被釋放。
所以本身棧內(nèi)存不存在所謂的GC和回收。線程調(diào)用方法時就會給方法棧楨分配內(nèi)存,線程執(zhí)行完方法時就會回收掉那個方法棧楨的內(nèi)存。
內(nèi)存快照是分析內(nèi)存占用的,同樣是針對堆內(nèi)存和Metaspace的。所以對線程的棧內(nèi)存而言,也不需要使用內(nèi)存快照。
(2)代碼示例???????
public?class?Demo2?{
? ??public?static?long?counter =?0;
? ??public?static?void?main(String[] args)?{
? ? ? ? work();
? ? }
? ??public?static?void?work()?{
? ? ? ? System.out.println("目前是第"?+ (++counter) +?"次調(diào)用方法");
? ? ? ? work();
? ? }
}
使用的JVM參數(shù)如下:???????
?-XX:+UseParNewGC?-XX:+UseConcMarkSweepGC
?-XX:ThreadStackSize=1m?
?-XX:+PrintGCDetails?-Xloggc:gc.log
?-XX:+HeapDumpOnOutOfMemoryError
?-XX:HeapDumpPath=./
(3)運行代碼后分析異常報錯信息的調(diào)用棧
接著運行代碼產(chǎn)生棧內(nèi)存溢出,如下:???????
目前是第5549次調(diào)用方法
java.lang.StackOverflowError
? at java.io.FileOutputStream.write(FileOutputStream.java:326)
? at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
? at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
? at java.io.PrintStream.write(PrintStream.java:482)
? at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
? at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
? at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
? at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
? at java.io.PrintStream.write(PrintStream.java:527)
? at java.io.PrintStream.print(PrintStream.java:669)
? at java.io.PrintStream.println(PrintStream.java:806)
? at com.demo.rpc.test.Demo.work(Demo.java:9)
? at com.demo.rpc.test.Demo.work(Demo.java:10)
上述所示異常會明確提示:出現(xiàn)棧內(nèi)存溢出的問題,原因是不斷調(diào)用Demo類的work()方法導(dǎo)致的。
因此對于棧內(nèi)存溢出的問題,定位和解決的思路就很簡單了。只要把所有的異常都寫入本地日志文件,那么發(fā)現(xiàn)系統(tǒng)崩潰時,去日志里定位異常信息即可。
(4)總結(jié)
情形一:Metaspace區(qū)域溢出
通過異常信息可以直接定位出是Metaspace區(qū)域發(fā)生異常,然后分析GC日志就可以知道Metaspace發(fā)生溢出的全過程,接著再使用MAT分析內(nèi)存快照,就知道是哪個類太多導(dǎo)致異常。
情形二:棧內(nèi)存溢出
首先從異常日志中就能知道是棧內(nèi)存溢出。
然后從異常日志中可以找到對應(yīng)的報錯方法。
知道哪個方法后,就可以到代碼中定位問題。
5.JVM堆內(nèi)存溢出時應(yīng)該如何解決(OutOfMemoryError: Java heap space)
(1)示例代碼
(2)運行后的觀察
(3)用MAT分析內(nèi)存快照
(4)總結(jié)
(1)示例代碼
運行如下程序:???????
public?class?Demo3?{
? ??public?static?void?main(String[] args) {
? ? ? ? long counter =?0;
? ? ? ??List<Object> list =?new?ArrayList<Object>();
? ? ? ??while(true) {
? ? ? ? ? ? list.add(new?Object());
? ? ? ? ? ??System.out.println("當前創(chuàng)建了第"?+ (++counter) +?"個對象");
? ? ? ? } ? ?
? ? }
}
采用的JVM參數(shù)如下:???????
?-Xms10m?-Xmx10m
?-XX:+PrintGCDetails?-Xloggc:gc.log
?-XX:+HeapDumpOnOutOfMemoryError?-XX:HeapDumpPath=./
?-XX:+UseParNewGC?-XX:+UseConcMarkSweepGC
(2)運行后的觀察
其實堆內(nèi)存溢出的現(xiàn)象也是很簡單的,在系統(tǒng)運行一段時間之后,會發(fā)現(xiàn)系統(tǒng)崩潰了,然后登錄到線上機器檢查日志文件。先看到底為什么崩潰:???????
java.lang.OutOfMemoryError: Java heap space
Dumping heap to ./java_pid1023.hprof ...
Heap dump file created [13409210 bytes?in?0.033 secs]
? ? Exception?in?thread?"main"?java.lang.OutOfMemoryError: Java heap space
這個就表明了:是Java堆內(nèi)存溢出了,而且還導(dǎo)出了一份內(nèi)存快照。此時GC日志都不用分析,因為堆內(nèi)存溢出往往對應(yīng)著大量的GC日志。這些大量的GC日志分析起來很麻煩,可以直接將線上自動導(dǎo)出的內(nèi)存快照拷貝回本地,然后使用MAT分析。
(3)用MAT分析內(nèi)存快照
采用MAT打開內(nèi)存快照后會看到下圖:
這次MAT非常簡單,直接在內(nèi)存泄漏報告中告訴我們。內(nèi)存溢出原因只有一個,因為它沒提示任何其他的問題。接下來仔細分析一下MAT提供的分析報告。
一.首先看下面的提示
The thread java.lang.Thread @?0x7bf6a9a98?main keeps local variables?with?total size?7,203,536?(92.03%)?bytes.
意思是main線程通過局部變量引用了7230536個字節(jié)的對象(7M),考慮到總共就給堆內(nèi)存10M,所以7M基本上個已經(jīng)到極限了。
二.接著看下面的提示
The memory?is?accumulated?in?one instance of?"java.lang.Object[]"?loaded?by?"<system class loader>".
這句話的意思是內(nèi)存都被一個實例對象占用了,就是java.lang.Object[]。
三.這時還不能判斷,得點擊Details
在Details里能看到這個東西,即占用了7M內(nèi)存的的java.lang.Object[]。這里會列出該Object數(shù)組的每個元素,可見是大量的java.lang.Object,而這些java.lang.Object其實就是我們在代碼里創(chuàng)建的。至此真相大白,就是大量的Object對象占用了7M的內(nèi)存導(dǎo)致內(nèi)存溢出。
四.接著分析這些對象是如何創(chuàng)建出來的
此時可以回到上一級頁面進行查找,如下:
這個意思是可以查看創(chuàng)建那么多對象的線程,它的一個執(zhí)行棧。從線程執(zhí)行棧中就可知這個線程在執(zhí)行什么方法時創(chuàng)建了大量的對象。
從上面的調(diào)用??梢钥吹?#xff1a;Demo3.main()方法會一直調(diào)用ArrayList.add()方法,然后引發(fā)OOM。所以接下來只要在對應(yīng)代碼里看一下,就知道怎么回事了。接著優(yōu)化對應(yīng)的代碼即可,這樣就不會發(fā)生內(nèi)存溢出了。
(4)總結(jié)
堆內(nèi)存溢出問題的分析和定位:
一是加入自動導(dǎo)出內(nèi)存快照的參數(shù)
二是到線上看一下日志文件里的報錯
如果是堆溢出,則用MAT分析內(nèi)存快照。
MAT分析的時候一些順序和技巧:
一.首先看占用內(nèi)存最多的對象是誰
二.然后分析那個線程的調(diào)用棧
三.接著看哪個方法引發(fā)內(nèi)存溢出
四.最后優(yōu)化代碼即可