Java內存溢出時,還能正常處理請求嗎?
當你被問到“當Java程序發生內存溢出時,進程還能正常處理請求嗎?”這樣的面試題,會不會很懵?這里分享一次網友車轍在當初剛畢業那幾年,意義風發,總覺得天下沒有自己不會的面試題。然后在一次字節的面試中,徹徹底底的翻車的面試過程,希望提供大家一些面試經驗。
Java 的優勢有什么
面試官一上來,直接進入主題:你覺得在內存管理上,Java 有什么優勢?
我:小菜一碟。相對于需要手動釋放內存的 C 語言,Java 則通過垃圾回收機制實現了自動內存管理。這一機制能夠自動辨識并清理不再使用的內存資源,從而省去了繁瑣的手動釋放過程,極大地簡化了開發人員的工作。這讓開發者能夠將精力更專注地放在業務邏輯的構建上,而不必過多憂慮內存管理的問題,從而大大簡化了開發人員的工作量。
什么是 OOM
面試官:請問您是否熟悉內存溢出(OOM)情況?
我:在我的經驗中,我在線上經常遇到內存溢出的情況。特別是在使用Java編寫的應用程序中,OOM通常是指內存溢出異常,即當應用程序需要為新對象分配內存空間時,可用內存不足以滿足需求,從而導致了OOM異常的拋出。
什么情況會產生 OOM
面試官:好小子,線上的事故代碼不會都是你寫的吧,那你能談談是什么情況會導致內存溢出呢?
我:當然,一個常見的情況就是堆內存溢出。在創建對象時,大部分情況下都是占用 JVM 的堆內存。一旦堆內存無法滿足對象的分配需求,就會拋出OOM異常。
錯誤信息通常是:java.lang.OutOfMemoryError: Java heap space
堆內存溢出的具體場景
面試官:你這個太抽象了,能不能具體點?
我:嗯,常見導致內存溢出的情況有這么幾種:
對象生命周期過長:如果某個對象的生命周期過長,而且該對象占用的內存很大,那么在不斷創建新對象的過程中,堆內存會被耗盡,從而導致內存溢出。這種情況一般出現在用集合當緩存,卻忽略了緩存的淘汰機制。
無限遞歸:遞歸調用中缺少退出條件或遞歸深度過大,會導致空間耗盡,引發溢出錯誤。往往在測試環境就會發現該問題,不會暴露在生產環境
大數據集合:在處理大量數據時,如果沒有正確管理內存,例如加載過大的文件、查詢結果集過大等,會導致內存溢出。
JVM配置不當:如果JVM的內存參數配置不合理,例如堆內存設置過小,無法滿足應用程序的內存需求,也會導致內存溢出。
下面的這個例子就是無限循環導致內存溢出。
什么是內存泄漏
面試官:你知道在我們的程序里,有可能會出現內存泄漏,你對它了解嗎?
我:對的,和內存溢出的情況不同,還有一種特殊場景,叫做內存泄漏(本質上還是內存溢出,只不過是錯誤的內存溢出),指的是程序在運行過程中無法釋放不再使用的內存,導致內存占用不斷增加,最終耗盡系統資源,這種情況就被稱為內存泄漏。
這一次,我提前搶答了, 常見導致內存泄漏的情況包括:
對象的引用未被正確釋放:如果在使用完一個對象后,忘記將其引用置為 null 或者從數據結構中移除,那么該對象將無法被垃圾回收,導致內存泄漏。比如 ThreadLocal。
長生命周期的對象持有短生命周期對象的引用:如果一個長生命周期的對象持有了一個短生命周期對象的引用,即使短生命周期對象不再使用,由于長生命周期對象的引用仍然存在,短生命周期對象也無法被垃圾回收,從而造成內存泄漏。
過度使用第三方庫:某些第三方庫可能存在內存泄漏或者資源未正確釋放的問題,如果使用不當或者沒有適當地管理這些庫,可能會導致內存溢出。
集合類使用不當:在使用集合類時,如果沒有正確地清理元素,當集合不再需要時,集合中的對象也不會被釋放,導致內存泄漏。
資源未正確釋放:如果程序使用了諸如文件、數據庫連接、網絡連接等資源,在不再需要這些資源時沒有正確釋放,會導致資源泄漏,最終導致內存泄漏。
下面的這個例子就是長生命周期的對象持有短生命周期對象的引用, 導致內存泄漏。
還有其他情況嗎
面試官:你說的都是堆的內存溢出,還有其他情況嗎?
遞歸調用導致棧溢出
當遞歸調用的層級過深,棧空間無法容納更多的方法調用信息時,會引發 StackOverflowError 異常,這也是一種 OOM 異常。例如,以下示例中的無限遞歸調用會導致棧溢出。
元空間(Metaspace)耗盡
元空間是 Java 8 及以后版本中用來存儲類元數據的區域。它取代了早期版本中的永久代(PermGen)。元空間主要用于存儲類的結構信息、方法信息、靜態變量以及編譯后的代碼等。
當程序加載和定義大量類、動態生成類、使用反射頻繁操作類等情況下,可能會導致元空間耗盡。常見導致元空間耗盡的情況包括:
類加載過多:如果應用程序動態加載大量的類或者使用動態生成類的方式,會導致元空間的使用量增加。如果無法及時卸載這些類,元空間可能會耗盡。
字符串常量過多:Java中的字符串常量會被存儲在元空間中。如果應用程序中使用了大量的字符串常量,尤其是較長的字符串,可能會導致元空間的耗盡。
頻繁使用反射:反射操作需要大量的元數據信息,會占用較多的元空間。如果應用程序頻繁使用反射進行類的操作,可能會導致元空間耗盡。
大量動態代理:動態代理是一種使用反射創建代理對象的技術。如果應用程序大量使用動態代理,將會生成大量的代理類,占用較多的元空間。
未正確限制元空間大小:默認情況下,元空間的大小是不受限制的,它會根據需要動態擴展。如果沒有正確設置元空間的大小限制,或者限制過小,可能會導致元空間耗盡。
下面的這個例子就是類加載過多導致的內存泄漏。
終極問題
面試官滿意地點了點頭,表示我對Java線程處理請求時的情況了解得很多。接著,他提出了一個問題:“當Java線程在處理請求時,發生了OOM異常,整個進程還能繼續處理請求嗎?”這個Java面試題可不簡單哦!
在我正準備回答的時候,面試官卻提醒我這個問題涉及的內容較為復雜,不是簡單的是與否問題。他建議我先整理一下思路。
面試官的眼神透露出這道題目有些深意。經過一番思考,我給出了我的答案:“我認為OOM并不會導致整個進程崩潰。”
面試官隨即追問:“你是怎么理解的?OOM難道不是內存不足的表現嗎?既然內存不足,進程還能繼續處理請求嗎?”
我解釋道:“盡管出現OOM,但通過垃圾回收機制仍有可能釋放一些內存。”
面試官卻反駁:“不是因為在垃圾回收后,發現內存依然不足才會拋出OOM異常嗎?這難道不意味著垃圾回收已經無法繼續執行,導致內存不足,進而觸發了OOM異常嗎?整個流程是內存不足,垃圾回收無效,最終OOM。”
我只能在內心默默吐槽,面對這樣的組合拳,我有些措手不及。很遺憾,面試最終以失敗告終。面試官在我離開時似乎在暗示:“這種情況我也不愿意發生,只能怪編程經驗不夠豐富。”
實戰
回到家,我馬上去進行了代碼實戰,用來測試 OOM。
環境是:OpenJdk 11 -Xms100m -Xmx100m -XX:+PrintGCDetails
堆內存溢出
首先我們創建一個方法,調用它,每隔一秒不停的循環打印控制臺信息,它的主要作用是模擬其他線程處理請求。
接著再創建一個死循環往 List 中放入對象的方法,它的主要作用是模擬導致OOM的那個線程。
最終結果是headOOM拋出了 OOM 異常,但是控制臺還在不停的打印。【這邊截圖太大了,就不貼出來了】
這就是答案嗎?其實不是,在第一步中,僅僅是在控制臺打印出了日志,并沒有創建明確的對象。將它稍微改動下,加一行,每次打印前先創建 10M 的對象。
結果依舊會繼續打印。看到這里有些人可能會說,答案確實是”還能繼續執行”,我只能說你是 Too Young Too Simple 。往下看
堆內存泄漏
老規矩,還是上面的方法
創建一個內存泄漏的方法,list2 作用域是在類對象級別,從而產生內存泄漏
然后繼續執行,結果首先是headOOM2這個方法對應的線程拋出 OOM。
接著是 WriteInfo這個方法對應的線程拋出OOM,所以我猜測現在整個進程基本都不能處理請求了。
為了印證這個猜測,再去調用下 writeInfo這個方法,直接拋出 OOM 異常。說明我們的猜測是對的。
這時候你如果把那個 10M 改成1M,writeInfo 這個方法就又能執行下去了,不信的話就去試試看吧。
這說明內存泄漏的情況,其他線程能否繼續執行下去,取決于這些線程的執行邏輯是否會占用大量內存。
不發生內存泄漏的情況下,為什么頻繁創建對象會導致OOM,GC 不是會把對象給回收嗎
- 堆內存限制:Java程序的堆內存有大小限制。如果頻繁創建對象且無法及時回收,堆空間可能被耗盡。垃圾回收器會嘗試回收不再使用的對象,但若創建速度超過回收速度,堆內存不足將導致OOM。
- 垃圾回收開銷:盡管垃圾回收器回收不再使用的對象,但垃圾回收本身消耗時間和計算資源。頻繁創建臨時對象會增加回收時間,降低應用程序效率。
- 內存碎片化:頻繁創建和銷毀對象會導致內存空間碎片化。即使總的空閑內存足夠,若沒有足夠的連續大塊內存分配給新對象,也會發生OOM。
通過觀察GC日志,可能發現OOM發生時,堆大小未達到閾值,進一步說明其他因素導致了OOM異常。
總結
首先,我們為你闡述了何為OOM以及其發生場景,包括內存溢出和內存泄漏,然后引出了問題:當Java線程在處理請求時,拋出OOM異常,整個進程是否仍能處理請求。
隨后,我們進行了代碼實驗,模擬了內存溢出和內存泄漏兩種情況,得出以下結論:
- 內存溢出情況下,若GC速度跟不上內存分配速度,導致OOM并殺死線程,通常整個進程仍能繼續處理請求。
- 內存泄漏情況下,未能回收的內存可能引發OOM,繼而殺死線程以防止無法回收對象繼續產生。此時,那些不占用大量內存的線程可能會繼續執行,但那些耗費大量內存的線程可能會無法執行。極端情況下,進程可能會崩潰。
以上結論反映了OOM對進程的影響,并指出了在不同情況下,進程是否仍能處理請求。

浙公網安備 33010602011771號