OOM簡記

OOM簡記

notice: 下面說的比如10M老年代空間,在10M分配完畢的時(shí)候進(jìn)行FullGC都是簡化的說法,其實(shí)應(yīng)該是有個(gè)空間分配擔(dān)保機(jī)制的存在,不會(huì)出現(xiàn)在10M全部使用的情況下才進(jìn)行FullGC的情況。

1. OOME出現(xiàn)的區(qū)域

  1. heap java堆

  2. vm stack 虛擬機(jī)棧

  3. native method stack 本地方法棧

  4. method area 方法區(qū)

  5. direct memory 直接內(nèi)存

ps. 除了program counter register 程序計(jì)數(shù)器外的其他內(nèi)存區(qū)域都有可能發(fā)生oome

2. 怎么判斷OOME出現(xiàn)的區(qū)域

查看異常堆棧信息,一般在OOME后會(huì)進(jìn)一步跟著提示具體的區(qū)域,比如

Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
 at com.cqx.oom.OOMDemo.lambda$main$0(OOMDemo.java:23)
 at com.cqx.oom.OOMDemo$Lambda$1/668386784.run(Unknown Source)
 at java.lang.Thread.run(Thread.java:748)

3. 什么時(shí)候會(huì)拋出OOME

當(dāng)創(chuàng)建對(duì)象時(shí)jvm檢測到內(nèi)存不夠本次內(nèi)存分配,于是會(huì)進(jìn)行一次FullGC,如果本次GC后還是內(nèi)存不夠分配,那就拋出OOME,當(dāng)前的線程就會(huì)死亡。

OOME Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector. ----API文檔

The JVM will run the GC when it's on edge of the OutOfMemoryError. If the GC didn't help at all, then the JVM will throw OOME.

具體情況可能發(fā)生OOME的情況如下

假設(shè)jvm的參數(shù)設(shè)置現(xiàn)在是 -Xmx20M -Xmx20M -Xmn10M -XX:MaxDirectMemorySize=1M, 即分配堆大小為20M,其中老年代和新生代各10M
public static final int _1MB = 1024 * 1024;
1. 持續(xù)不斷創(chuàng)建對(duì)象,并不釋放他的引用
for(i = 0; i < 100; i++) {
byte[] M1 = new byte[_1MB];
}
隨著對(duì)象不斷創(chuàng)建,tenured generation到達(dá)了10MB,即已經(jīng)裝滿了,然后繼續(xù)創(chuàng)建對(duì)象,隨著新生代中Eden區(qū)再次被填滿,觸發(fā)一次Minor GC,之前Survivor0中部分對(duì)象由于各種原因,比如年齡大了,或者本次MinorGC中發(fā)現(xiàn)S0不夠存放本次存活的對(duì)象,所以會(huì)有對(duì)象晉升到Old Generation。此時(shí)JVM發(fā)現(xiàn)老年代的內(nèi)存也不夠進(jìn)行此次分配,于是就進(jìn)行一次FullGC(Major GC),根據(jù)可達(dá)性分析,從GC ROOTS開始分析對(duì)象的引用關(guān)系,發(fā)現(xiàn)任然存活,就不進(jìn)行回收,所有本次FullGC并沒有任何卵用,所有GC后還是分配不了,那么此時(shí)當(dāng)前線程就會(huì)拋出一個(gè)OOME,然后死亡。

2. byte[] _12MB = new byte[12 * _1MB];
大對(duì)象的創(chuàng)建會(huì)根據(jù)JVM參數(shù)的設(shè)置直接分配到老年代中,跟1類似,F(xiàn)ULLGC后發(fā)現(xiàn)內(nèi)存不夠,拋出OOME

3. ByteBuffer.allocateDirect(2 * _1MB);
由于之前指定了最大直接內(nèi)存為1MB 這邊分配了2MB就拋出了OOME
java.lang.OutOfMemoryError: Direct buffer memory

4. 方法區(qū)OOME
之前在第一次接 Jenkins發(fā)布平臺(tái)時(shí)就出現(xiàn)了這個(gè)問題。當(dāng)時(shí)發(fā)布www?指定的jdk版本是7,可能是因?yàn)閣ww大量的jsp文件(感覺也不多),在編譯生成class文件時(shí)導(dǎo)致方法區(qū)不夠用發(fā)生了OOME??串?dāng)時(shí)打印的異常日志確認(rèn)是方法區(qū)的OOME。
原因: jdk8之前版本不通過-XX:PermSize和-XX:MaxPermSize顯示指定方法區(qū)大小,應(yīng)該就是64MB。當(dāng)時(shí)的jenkins中配置只指定了堆大小,沒有指定方法區(qū)的大小
解決: 1. -XX:PermSize和-XX:MaxPermSize 2.改用jdk8(移除了使用永久代來當(dāng)做方法區(qū)的策略,使用了metaspace元數(shù)據(jù)區(qū),不暫用jvm指定的堆內(nèi)存,而是使用機(jī)器的native memory,不受jvm堆的限制)

5. 調(diào)用外部服務(wù),外部服務(wù)故障或者處理緩慢導(dǎo)致OOME
比如調(diào)用中交興路查詢位置接口,網(wǎng)絡(luò)或者ZJXL服務(wù)原因請(qǐng)求一直得不到反饋,兩邊處理速度不對(duì)等,導(dǎo)致越來越多的請(qǐng)求積壓,OOME。
思路:
a. 使用hystrix等熔斷功能的工具來管理外部服務(wù)調(diào)用
b. 復(fù)用tcp連接??梢钥紤]加上keepalive:true的請(qǐng)求頭,避免每次調(diào)用都重新發(fā)起tcp連接,
c. httpclient這種應(yīng)該是要單例的,不需要每次請(qǐng)求都單獨(dú)創(chuàng)建
d. 生產(chǎn)者消費(fèi)者模式來處理

6. 內(nèi)存泄漏
a. 監(jiān)聽器和一些回調(diào)注冊上來但是沒有顯示的取消,服務(wù)端一直持有這個(gè)監(jiān)聽器的引用。
b. 緩存泄漏,比如用HashMap實(shí)現(xiàn)的緩存,吧引用扔到進(jìn)去后忘了,一直扔一直扔就炸了,GC的時(shí)候發(fā)現(xiàn)對(duì)象引用仍然在緩存里,就不回收就炸了??梢杂肳eakHashMap來實(shí)現(xiàn),key是WeakReference,GC可以回收掉。
</pre>

4. OOME會(huì)導(dǎo)致JVM shutdown嗎

不會(huì)。OOME只會(huì)把拋出這個(gè)異常的線程給殺掉。 之所以發(fā)生OOME的時(shí)候應(yīng)用程序經(jīng)常出現(xiàn)假死,是因?yàn)镺OM了,其他正常運(yùn)行的線程也無法分配到資源,各種請(qǐng)求都無法得到處理,給人一種應(yīng)用掛掉的感覺。

如果你想在OOME的時(shí)候主動(dòng)kill掉當(dāng)前的應(yīng)用可以
-XX:OnOutOfMemoryError="kill -9 %p"  這里還可以執(zhí)行你的腳本,這樣就可以自動(dòng)重啟。。。
%p is the current Java process PID placeholder.

5. 上面說的OOME會(huì)殺死當(dāng)前的線程,那么GC可以回收到這部分資源,釋放空間,那么為什么應(yīng)用程序還是無法響應(yīng)請(qǐng)求呢。

因?yàn)橥鶔伋鯫OME的那個(gè)線程,不是大頭,他只是壓垮駱駝的最后一根稻草。

比如一個(gè)應(yīng)用,堆最大設(shè)為10M,一個(gè)定時(shí)任務(wù)的線程讀取數(shù)據(jù),在內(nèi)存里生成了上萬個(gè)對(duì)象,進(jìn)行處理,此時(shí)用了9.9MB內(nèi)存空間。然后一個(gè)普通的查詢請(qǐng)求進(jìn)來,OOME了,這個(gè)請(qǐng)求線程死亡了,由于定時(shí)任務(wù)線程還在處理,GC只把請(qǐng)求線程的資源回收。那么在定時(shí)任務(wù)線程處理期間,還是會(huì)一直出現(xiàn)OOME。

6. JVM可以自動(dòng)從OOME恢復(fù)嗎

可以的,但是不建議。

例如5中的情況,定時(shí)任務(wù)線程處理完畢了,那么GC會(huì)吧這部分內(nèi)存釋放掉,程序又正常了。 但是發(fā)生OOME的時(shí)候不建議讓JVM自動(dòng)恢復(fù)。因?yàn)槌霈F(xiàn)了這個(gè)異常一定是你程序上有漏洞,有問題,就算本次恢復(fù)了,接下去很可能還是會(huì)出現(xiàn)這個(gè)問題。而且OOM恢復(fù)期間很難熬,不管是對(duì)于JVM還是用戶來說。具體其他的可以看下面的鏈接

比如之前遇到的,之前做的司機(jī)證件導(dǎo)入功能,就發(fā)生過一次OOME。這個(gè)功能是處理用戶導(dǎo)入的照片文件,服務(wù)器接收這些文件,處理并存放到MongoDB。當(dāng)時(shí)用戶間隔很短時(shí)間導(dǎo)入兩次大約1G的照片文件,第一次導(dǎo)入的請(qǐng)求還在執(zhí)行中,因?yàn)檎掌枰x取到內(nèi)存,照片文件序列化生成的大對(duì)象,直接進(jìn)入老年代,MinorGC沒辦法清理,占用了大量的內(nèi)存。緊接著第二次導(dǎo)入請(qǐng)求進(jìn)來了,在處理過程中由于內(nèi)存不夠OOME了。但是這并不影響第一次的那個(gè)請(qǐng)求線程。當(dāng)時(shí)jmap -dump:live,format=b,file=path pid dump了堆棧信息,發(fā)現(xiàn)內(nèi)存夠用啊怎么會(huì)發(fā)生OOME呢,覺得很奇怪。然后我讓用戶再次導(dǎo)入,發(fā)現(xiàn)還是OOME。后面就突然發(fā)現(xiàn)-dump:live或者-histo:live會(huì)先觸發(fā)一次FullGC再進(jìn)行內(nèi)存信息收集,所以出現(xiàn)異常的那個(gè)線程的內(nèi)存都被回收掉了,所以dump文件上看來是正常的。 所以我就讓用戶先別導(dǎo),過個(gè)半個(gè)小時(shí)在來操作,這樣就好了。

live指令的解釋
dump only live objects; if not specified, all objects in the heap are dumped.</pre>

7. 怎么應(yīng)對(duì)OOME

  1. 考慮在JVM啟動(dòng)參數(shù)就設(shè)置上-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath,但是有風(fēng)險(xiǎn),比如5中例子,一直會(huì)觸發(fā)OOME,一直會(huì)生成dump文件,會(huì)更加麻煩,所以可以考慮加上-XX:OnOutOfMemoryError="kill -9 %p"

  2. 如果1沒做,那么可以考慮用jmap來生成dump堆棧快照迅速保留現(xiàn)場信息,并重啟應(yīng)用(1 2G可能幾分鐘就好了????沒找到數(shù)據(jù),懶得試)。在此之前可以用jstats查看GC的情況

  3. 如果JVM的分配內(nèi)存很大,好幾G,jmap可能需要執(zhí)行很久才生成dump文件,可以考慮用gdb來處理,好像會(huì)快很多。不管他不是jdk自帶的工具,要自己下載。參考鏈接

  4. 考慮使用softReference、weakReference、weakHashMap等等。

  5. 不用jmap,使用編程式方式觸發(fā)生成當(dāng)前的堆轉(zhuǎn)儲(chǔ)快照,參考鏈接

8. 堆轉(zhuǎn)儲(chǔ)快照分析

工具: jhat/visualVM/JPofiler/MAT 語句: OQL(對(duì)象查詢語句)

9. spring actuator/JMX Java Management Extensions

actuator 應(yīng)用監(jiān)控的東西,暴露了一些http接口,有個(gè)事heapdump還有個(gè)stackdump,可以通過定時(shí)任務(wù)之類去訪問然后分析,如果有問題就通知之類的。

jmx 公司有平臺(tái)radar??梢远鄬W(xué)習(xí)下這方面的東西。

10. 死鎖與死循環(huán),CPU飆升相關(guān)排查

  1. jstack 看線程堆棧

  2. top查找cpu占用率高的pid top -p pid -H 查看進(jìn)程內(nèi)線程的pid

  3. pid轉(zhuǎn)16進(jìn)制,去jstack中查日志

https://blog.51cto.com/13732225/2347907

. . . . . .

參考的文檔鏈接

https://stackoverflow.com/questions/3058198/can-the-jvm-recover-from-an-outofmemoryerror-without-a-restart

https://stackoverflow.com/questions/12096403/java-shutting-down-on-out-of-memory-error

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

https://segmentfault.com/a/1190000010603813

https://stackoverflow.com/questions/6418089/does-jmap-force-garbage-collection-when-the-live-option-is-used

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容