內存泄漏達到一定程度會引發OOM。
內存泄漏是指編寫的代碼中含有bug,不是指會引發程序執行結果錯誤那種,而是不可達的對象停留在堆中,即代碼中含有對象的強引用沒有沒釋放掉,導致該無用的對象無法被垃圾收集器收集。
假設內存足夠大,而內存泄漏的情況並不嚴重,只要還有足夠的空間分配給新的對象,那樣即使內存泄漏也不會引發OOM。
常見的幾種內存泄漏原因
靜態集合類引起的內存泄漏
靜態變量隨着類的加載被分配到方法區中,只要類還沒被卸載,靜態變量就不會被回收,而靜態的集合對象(指Collection對象,List、Set、Map),添加進去集合里的每個對象,集合都會持有其引用,只要我們沒有在代碼里顯示的remove。
HashSet中的對象屬性被修改后,再調用remove不起作用。
HashSet(內部封裝了HashMap)。首先我們要知道Map集合是通過鍵值對來儲存對象的。經過多次嘗試和查看源碼,發現HashSet儲存對象時是把add時候對象hashCode()返回的值和引用作為鍵值對儲存進去。
也就是說,如果我們想要修改對象屬性后仍然能用其引用將其從HashSet中移除(HashSet.remove(Object obj)),我們需要確保無論對象的屬性如何修改,它的hashCode()方法返回的值都始終如一。
監聽器
我們經常需要設置監聽器去監聽某個控件的狀態,需要留意我們在釋放控件的時候有沒有把對應的監聽器也釋放掉(監聽器持有對控件的引用)。
各種連接
比如數據庫連接(dataSourse.getConnection()),網絡連接(socket)和io連接,除非其顯式的調用了其close()方法將其連接關閉,否則是不會自動被GC 回收的。對於Resultset 和Statement 對象可以不進行顯式回收,但Connection 一定要顯式回收,因為Connection 在任何時候都無法自動回收,而Connection一旦回收,Resultset 和Statement 對象就會立即為NULL。但是如果使用連接池,情況就不一樣了,除了要顯式地關閉連接,還必須顯式地關閉Resultset Statement 對象(關閉其中一個,另外一個也會關閉),否則就會造成大量的Statement 對象無法釋放,從而引起內存泄漏。這種情況下一般都會在try里面去的連接,在finally里面釋放連接。
外部模塊的調用
最直接的例子就是回調。比如我們在代碼中創建一個A類對象和一個B類對象,在A類對象中中調用B類對象的某個方法並需要B類對象進行回調時,A、B類對象會相互持有對方的引用,最后即使我們在代碼中釋放了對兩個對象的引用,GC也無法回收這兩個對象,導致內存泄漏。
單例模式
之前的文章里也稍微提到過,首先明確單例對象是靜態的,且是自己持有對自己的強引用,也就是說生命周期可以相當於應用進程的生命周期,運行時不會被GC回收,那么很顯然如果單例對象中持有對其他對象的強引用,那被引用的對象也是無法被回收的,特別是單例對象中含有對集合對象的引用的時候,要注意引用的釋放。
常見的內存溢出異常
Java堆溢出
java.lang.OutOfMemoryError: Java heap space
堆空間無法給新的對象分配內存空間且GC一次后仍然無法分配足夠的空間時會導致堆溢出。
棧溢出
java.lang.StackOverflowError
這是由於線程請求的棧深度大於虛擬機所允許的最大深度,無法壓入新的棧幀,這時可以檢查一下代碼中是否存在死循環的遞歸調用。
方法區溢出
java.lang.OutOfMemoryError: PermGen space
這里還可以分為運行時常量池溢出和方法區存放Class過多導致溢出。過去的話方法區的大小是設定好的,Java8之后將Class信息放到了元空間,並且元空間是可以動態申請擴展的,詳細的描述小伙伴可以自行搜索永久代到元空間的相關博客。
無法創建新進程
java.lang.OutOfMemoryError:unable to create natvie thread
就是棧空間不足了無法創建新的進程。
注:轉自簡書