內(nèi)存溢出(OutofMemoryError)
簡述
java doc 中對 Out Of Memory Error 的解釋是,沒有空閑內(nèi)存,并且垃圾收集器也無法提供更多內(nèi)存。
JVM 提供的內(nèi)存管理機(jī)制和自動垃圾回收極大的解放了用戶對于內(nèi)存的管理,由于 GC(垃圾回收)一直在發(fā)展,所有一般情況下,除非應(yīng)用程序占用的內(nèi)存增長速度非???,造成垃圾回收已經(jīng)跟不上內(nèi)存消耗的速度,否則不太容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出問題。但是基本不會出現(xiàn)并不等于不會出現(xiàn),所以掌握 Java 內(nèi)存模型原理和學(xué)會分析出現(xiàn)的內(nèi)存溢出或內(nèi)存泄漏仍然十分重要。
大多數(shù)情況下,GC 會進(jìn)行各種年齡段的垃圾回收,實在不行了就放大招,來一次獨占式的 Full GC 操作,這時候會回收大量的內(nèi)存,供應(yīng)用程序繼續(xù)使用。
在拋出 OutofMemoryError 之前,通常垃圾收集器會被觸發(fā),盡其所能去清理出空間。例如:在引用機(jī)制分析中,涉及到 JVM 會去嘗試回收軟引用指向的對象等。在 java.nio.BIts.reserveMemory() 方法中,System.gc() 會被調(diào)用,以清理空間。
當(dāng)然,也不是在任何情況下垃圾收集器都會被觸發(fā)的。比如,分配了一個超大對象,類似一個超大數(shù)組超過堆的最大值,JVM 可以判斷出垃圾收集并不能解決這個問題,所以直接拋出 OutofMemoryError。
內(nèi)存溢出的常見情形
不同的內(nèi)存溢出錯誤可能會發(fā)生在內(nèi)存模型的不同區(qū)域,因此,需要根據(jù)出現(xiàn)錯誤的代碼具體分析來找出可能導(dǎo)致錯誤發(fā)生的地方,并想辦法進(jìn)行解決。
-
棧內(nèi)存溢出(StackOverflowError)
棧內(nèi)存可以分為虛擬機(jī)棧(VM Stack)和本地方法棧(Native Method Stack),除了它們分別用于執(zhí)行 Java 方法(字節(jié)碼)和本地方法,其余部分原理是類似的。
以虛擬機(jī)棧為例說明,Java 虛擬機(jī)棧是線程私有的,當(dāng)線程中方法被調(diào)度時,虛擬機(jī)會創(chuàng)建用于保存局部變量表、操作數(shù)棧、動態(tài)連接和方法出口等信息的棧幀(Stack Frame)。
具體來說,當(dāng)線程執(zhí)行某個方法時,JVM 會創(chuàng)建棧幀并壓棧,此時剛壓棧的棧幀就成為了當(dāng)前棧幀。如果該方法進(jìn)行遞歸調(diào)用時,JVM 每次都會將保存了當(dāng)前方法數(shù)據(jù)的棧幀壓棧,每次棧幀中的數(shù)據(jù)都是對當(dāng)前方法數(shù)據(jù)的一份拷貝。如果遞歸的次數(shù)足夠多,多到棧中棧幀所使用的內(nèi)存超出了棧內(nèi)存的最大容量,此時 JVM 就會拋出 StackOverflowError。
總之,不論是因為棧幀太大還是棧內(nèi)存太小,當(dāng)新的棧幀內(nèi)存無法被分配時,JVM 就會拋出 StackOverFlowError。
優(yōu)化方案:
-
可以通過設(shè)置 JVM 啟動參數(shù)
-Xss
參數(shù)來改變棧內(nèi)存大小。注:分配給棧的內(nèi)存并不是越大越好,因為棧內(nèi)存越大,線程多,留給堆的空間就不多了,容易拋出OOM。JVM的默認(rèn)參數(shù)一般情況沒有問題(包括遞歸)。
-
遞歸調(diào)用要控制好遞歸的層級,不要太高,超過棧的深度。
-
遞歸調(diào)用要防止形成死循環(huán),否則就會出現(xiàn)棧內(nèi)存溢出。
-
-
堆內(nèi)存溢出(OutOfMemoryError:java heap space)
堆內(nèi)存的唯一作用就是存放數(shù)組和對象實例,即通過 new 指令創(chuàng)建的對象,包括數(shù)組和引用類型。
堆內(nèi)存溢出又分為兩種情況:
-
Java 虛擬機(jī)的堆內(nèi)存設(shè)置不夠
如果堆的大小不合理(沒有顯式指定 JVM 堆大小或者指定數(shù)值偏小),對象所需內(nèi)存太大,創(chuàng)建對象時分配空間,JVM 就會拋出
OutOfMemoryError:java heap space
異常。優(yōu)化方案:
-
如果要處理比較可觀的數(shù)據(jù)量,可以通過修改 JVM 啟動參數(shù) -Xms 、-Xmx 來調(diào)整。使用壓力測試來調(diào)整這兩個參數(shù)達(dá)到最優(yōu)值。
-
盡量避免大的對象的申請,例如文件上傳,大批量從數(shù)據(jù)庫中獲取等。
盡量分塊或者分批處理,有助于系統(tǒng)的正常穩(wěn)定的執(zhí)行。
-
盡量提高一次請求的執(zhí)行速度,垃圾回收越早越好。
否則,大量的并發(fā)來了的時候,再來新的請求就無法分配內(nèi)存了,就容易造成系統(tǒng)的雪崩。
-
-
堆內(nèi)存泄露最終導(dǎo)致堆內(nèi)存溢出
當(dāng)堆中一些對象不再被引用但垃圾回收器無法識別時,這些未使用的對象就會在堆內(nèi)存空間中無限期存在,不斷的堆積就會造成內(nèi)存泄漏。不停的堆積最終會觸發(fā) java . lang.OutOfMemoryError。
優(yōu)化方案:如果發(fā)生了內(nèi)存泄漏,則可以先找出導(dǎo)致泄漏發(fā)生的對象是如何被 GC ROOT 引用起來的,然后通過分析引用鏈找到發(fā)生泄漏的地方,進(jìn)行代碼優(yōu)化。
-
-
永久代溢出(OutOfMemoryError:PermGen sapce)
對于老版本的 oracle JDK,因為永久代的大小是有限的,并且 JVM 對永久代垃圾回收(例如常量池回收、卸載不再需要的類型)非常不積極,所以當(dāng)不斷添加新類型的時候,永久代出現(xiàn) OutOfMemoryError 也非常多見,尤其是在運行時存在大量動態(tài)類型生成的場合;類似 intern 字符串緩存占用太多空間,也會導(dǎo)致 OOM 問題,對應(yīng)的異常信息,會標(biāo)記出來和永久代相關(guān):“java.lang.OutOfMemoryError:PermGen space"。
隨著元數(shù)據(jù)區(qū)的引入,方法區(qū)內(nèi)存已經(jīng)不再那么窘迫,所以相應(yīng)的 OOM 有所改觀,出現(xiàn) OOM,異常信息則變成了:“java.lang.OutofMemoryError:Metaspace"。
-
元空間內(nèi)存溢出(OutOfMemoryError: Metaspace)
元空間的溢出,系統(tǒng)會拋出 java.lang.OutOfMemoryError: Metaspace
出現(xiàn)這個異常的問題的原因是系統(tǒng)的代碼非常多或引用的第三方包非常多或者通過動態(tài)代碼生成類加載等方法,導(dǎo)致元空間的內(nèi)存占用很大。
優(yōu)化方案:
-
默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制。
但是為了整機(jī)的性能,盡量還是要對該項進(jìn)行設(shè)置,優(yōu)化參數(shù)配置,以免造成整機(jī)的服務(wù)停機(jī)。
-
慎重引用第三方包
對第三方包,一定要慎重選擇,不需要的包就去掉。
這樣既有助于提高編譯打包的速度,也有助于提高遠(yuǎn)程部署的速度。
-
關(guān)注動態(tài)生成類的框架
對于使用大量動態(tài)生成類的框架,要做好壓力測試,驗證動態(tài)生成的類是否超出內(nèi)存的需求會拋出異常。
-
-
直接內(nèi)存溢出
如果直接或間接(很多 java NIO,例如在 netty 的框架中被封裝為其他的方法)使用了 ByteBuffer 中的 allocateDirect() 方法,而又不做 clear 的時候,就會拋出 java.lang.OutOfMemoryError: Direct buffer memory 異常。
如果經(jīng)常有類似的操作,可以考慮設(shè)置 JVM 參數(shù):-XX:MaxDirectMemorySize,并及時 clear 內(nèi)存。
-
創(chuàng)建本地線程內(nèi)存溢出
除了堆以外的區(qū)域,無法為線程分配一塊內(nèi)存區(qū)域了(線程基本只占用堆以外的內(nèi)存區(qū)域),要么是內(nèi)存本身就不夠,要么堆的空間設(shè)置得太大了,導(dǎo)致了剩余的內(nèi)存已經(jīng)不多了,而由于線程本身要占用內(nèi)存,所以就不夠用了。
優(yōu)化方案:
- 首先檢查操作系統(tǒng)是否有線程數(shù)的限制,如果使用 shell 也無法創(chuàng)建線程,就需要調(diào)整系統(tǒng)的最大可支持的文件數(shù)。
- 日常開發(fā)中盡量保證線程最大數(shù)的可控制的,不要隨意使用可以無限制增長的線程池。
-
數(shù)組超限內(nèi)存溢出
JVM 在為數(shù)組分配內(nèi)存之前,會執(zhí)行特定平臺的檢查:分配的數(shù)據(jù)結(jié)構(gòu)是否在此平臺是可尋址的。
一般來說 java 對應(yīng)用程序所能分配數(shù)組最大大小是有限制的,只不過不同的平臺限制有所不同,但通常在1到21億個元素之間。當(dāng)應(yīng)用程序試圖分配大于 Java 虛擬機(jī)可以支持的數(shù)組時會報 Requested array size exceeds VM limit 錯誤。
不過這個錯誤一般少見的,主要是由于 Java 數(shù)組的索引是 int 類型。 Java 中的最大正整數(shù)為 2 ^ 31 - 1 = 2,147,483,647。 并且平臺特定的限制可以非常接近這個數(shù)字,例如:Jdk1.8 可以初始化數(shù)組的長度高達(dá) 2,147,483,645(Integer.MAX_VALUE-2)。若是在將數(shù)組的長度再增加 1 達(dá)到 nteger.MAX_VALUE-1 ,就會出現(xiàn) OutOfMemoryError 了。
優(yōu)化方案:數(shù)組長度要在平臺允許的長度范圍之內(nèi)。
-
超出交換區(qū)內(nèi)存溢出
在 Java 應(yīng)用程序啟動過程中,可以通過 -Xmx 和其他類似的啟動參數(shù)限制指定的所需的內(nèi)存。而當(dāng) JVM 所請求的總內(nèi)存大于可用物理內(nèi)存的情況下,操作系統(tǒng)開始將內(nèi)容從內(nèi)存轉(zhuǎn)換為硬盤。
當(dāng)應(yīng)用程序向 JVM native heap 請求分配內(nèi)存失敗并且 native heap 也即將耗盡時, JVM 會拋出Out of swap space 錯誤, 錯誤消息中包含分配失敗的大?。ㄒ宰止?jié)為單位)和請求失敗的原因。
優(yōu)化方案:
-
增加系統(tǒng)交換區(qū)的大小。
但如果使用了交換區(qū),性能會大大降低,不建議采用這種方式。
生產(chǎn)環(huán)境盡量避免最大內(nèi)存超過系統(tǒng)的物理內(nèi)存。其次,去掉系統(tǒng)交換區(qū),只使用系統(tǒng)的內(nèi)存,保證應(yīng)用的性能。
-
-
系統(tǒng)殺死進(jìn)程內(nèi)存溢出
操作系統(tǒng)是建立在進(jìn)程的概念之上,這些進(jìn)程在內(nèi)核中作業(yè),其中有一個非常特殊的進(jìn)程,稱為“內(nèi)存殺手(Out of memory killer)”。當(dāng)內(nèi)核檢測到系統(tǒng)內(nèi)存不足時,OOM killer 被激活,檢查當(dāng)前誰占用內(nèi)存最多然后將該進(jìn)程殺掉。
一般 Out of memory:Kill process or sacrifice child 報錯會在當(dāng)可用虛擬內(nèi)存(包括交換空間)消耗到讓整個操作系統(tǒng)面臨風(fēng)險內(nèi)存不足時,會被觸發(fā)。在這種情況下,OOM Killer 會選擇“流氓進(jìn)程”并殺死它。
優(yōu)化方案:
- 增加交換空間的方式可以緩解 Java heap space 異常
- 但還是建議最好的方案就是升級系統(tǒng)內(nèi)存,讓 java 應(yīng)用有足夠的內(nèi)存可用,就不會出現(xiàn)這種問題。
內(nèi)存泄漏(memory leak)
簡述
-
也稱作“存儲滲漏”。
-
嚴(yán)格來說,只有對象不會再被程序用到了,但是 GC 又不能回收它們的情況,才叫內(nèi)存泄漏。
但實際情況很多時候一些不太好的實踐(或疏忽)會導(dǎo)致對象的生命周期變得很長甚至導(dǎo)致 OOM,也可以叫做寬泛意義上的“內(nèi)存泄漏”。
-
盡管內(nèi)存泄漏并不會立刻引起程序崩潰,但是一旦發(fā)生內(nèi)存泄漏,程序中的可用內(nèi)存就會被逐步蠶食,直至耗盡所有內(nèi)存,最終出現(xiàn) OutOfMemory 異常,導(dǎo)致程序崩潰。
注意:這里的可用內(nèi)存并不是指物理內(nèi)存,而是指虛擬內(nèi)存大小,這個虛擬內(nèi)存大小取決于磁盤交換區(qū)設(shè)定的大小。
-
Java 使用可達(dá)性分析算法,最上面的數(shù)據(jù)不可達(dá),就是需要被回收的。
后期有一些對象不用了,按道理應(yīng)該斷開引用,但是存在一些鏈沒有斷開,從而導(dǎo)致沒有辦法被回收
可達(dá)性分析算法
可達(dá)性分析算法:判斷對象是否是不再使用的對象,本質(zhì)都是判斷一個對象是否還被引用。那么對于這種情況下,由于代碼的實現(xiàn)不同就會出現(xiàn)很多種內(nèi)存泄漏問題(讓 JVM 誤以為此對象還在引用中,無法回收,造成內(nèi)存泄漏)。
舉例說明:
- 對象 X 引用對象 Y,X 的生命周期比 Y 的生命周期長;
- 那么當(dāng) Y 生命周期結(jié)束的時候,X 依然引用著 Y,這時候,垃圾回收期是不會回收對象 Y 的;
- 如果對象 X 還引用著生命周期比較短的 A、B、C,對象 A 又引用著對象 a、b、c,這樣就可能造成大量無用的對象不能被回收,進(jìn)而占據(jù)了內(nèi)存資源,造成內(nèi)存泄漏,直到內(nèi)存溢出。
Java 中內(nèi)存泄漏的 8 種情況
-
靜態(tài)集合類,如 HashMap、LinkedList 等等。
如果這些容器為靜態(tài)的,那么它們的生命周期與 JVM 程序一致,則容器中的對象在程序結(jié)束之前將不能被釋放,從而造成內(nèi)存泄漏。
簡而言之,長生命周期的對象持有短生命周期對象的引用,盡管短生命周期的對象不再使用,但是因為長生命周期對象持有它的引用而導(dǎo)致不能被回收。
-
單例模式
單例模式,和靜態(tài)集合導(dǎo)致內(nèi)存泄露的原因類似,因為單例的靜態(tài)特性,它的生命周期和 JVM 的生命周期一樣長,所以如果單例對象如果持有外部對象的引用,那么這個外部對象也不會被回收,那么就會造成內(nèi)存泄漏。
-
內(nèi)部類持有外部類的引用
在 Java 中內(nèi)部類的定義與使用一般為成員內(nèi)部類與匿名內(nèi)部類,他們的對象都會隱式持有外部類對象的引用,影響外部類對象的回收。
可以通過反編譯可以來驗證這個理論:
-
java 代碼
public class Outer { private String name; class Inner{ private String test; } }
-
反編譯后的代碼
class Outer$Inner { private String test; final Outer this$0; Outer$Inner() { this.this$0 = Outer.this; super(); } }
可以清楚的發(fā)現(xiàn),內(nèi)部類的屬性中有這個外部類,并且在內(nèi)部類的構(gòu)造函數(shù)中有這個外部類屬性的初始化。
如果一個外部類的實例對象的方法返回了一個內(nèi)部類的實例對象,而這個內(nèi)部類對象被長期引用了,那么即使那個外部類實例對象不再被使用,但由于內(nèi)部類持有外部類的實例對象引用,這個外部類對象將不會被垃圾回收,這也會造成內(nèi)存泄漏。
-
-
各種連接,如數(shù)據(jù)庫連接、網(wǎng)絡(luò)連接和 IO 連接等
在對數(shù)據(jù)庫進(jìn)行操作的過程中,首先需要建立與數(shù)據(jù)庫的連接,當(dāng)不再使用時,需要調(diào)用 close 方法來釋放與數(shù)據(jù)庫的連接。只有連接被關(guān)閉后,垃圾回收器才會回收對應(yīng)的對象。
否則,如果在訪問數(shù)據(jù)庫的過程中,**對 Connection、Statement 或 ResultSet 不顯性地關(guān)閉,將會造成大量的對象無法被回收,**從而引起內(nèi)存泄漏。
-
變量不合理的作用域
一般而言,一個變量的定義的作用范圍大于其使用范圍,很有可能會造成內(nèi)存泄漏。另一方面,如果沒有及時地把對象設(shè)置為 null,很有可能導(dǎo)致內(nèi)存泄漏的發(fā)生。
public class UsingRandom { private String msg; public void receiveMsg(){ //private String msg; readFromNet(); // 從網(wǎng)絡(luò)中接受數(shù)據(jù)保存到msg中 saveDB(); // 把msg保存到數(shù)據(jù)庫中 //msg = null; } }
如上面這個偽代碼,通過 readFromNet 方法把接受的消息保存在變量 msg 中,然后調(diào)用 saveDB 方法把 msg 的內(nèi)容保存到數(shù)據(jù)庫中,此時 msg 已經(jīng)就沒用了,由于 msg 的生命周期與對象的生命周期相同,此時 msg 還不能回收,因此造成了內(nèi)存泄漏。
優(yōu)化方案:
- 方案1:這個 msg 變量可以放在方法內(nèi)部,當(dāng)方法使用完,那么 msg 的生命周期也就結(jié)束,就可以回收了。
- 方案2:在使用完 msg 后,把 msg 設(shè)置為 null,這樣垃圾回收器也會回收 msg 的內(nèi)存空間。
-
改變哈希值
當(dāng)一個對象被存儲進(jìn) HashSet 集合中以后,就不能修改這個對象中的那些參與計算哈希值的字段了。
否則,對象修改后的哈希值與最初存儲進(jìn) HashSet 集合中時的哈希值就不同了,在這種情況下,即使在 contains 方法使用該對象的當(dāng)前引用作為的參數(shù)去 HashSet 集合中檢索對象,也將返回找不到對象的結(jié)果,這也會導(dǎo)致無法從 HashSet 集合中單獨刪除當(dāng)前對象,造成內(nèi)存泄漏。
這也是 String 為什么被設(shè)置成了不可變類型,可以放心地把 String 存入 HashSet,或者把 String 當(dāng)做 HashMap 的 key 值;
當(dāng)想把自己定義的類保存到散列表的時候,需要保證對象的 hashCode 不可變。
/** * 演示內(nèi)存泄漏 */ public class ChangeHashCode1 { public static void main(String[] args) { HashSet hs = new HashSet(); Point cc = new Point(); cc.setX(10);//hashCode = 41 hs.add(cc); cc.setX(20);//hashCode = 51 System.out.println("hs.remove = " + hs.remove(cc));//false hs.add(cc); System.out.println("hs.size = " + hs.size());//size = 2 } } class Point { int x; public int getX() return x; public void setX(int x) this.x = x; @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + x; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Point other = (Point) obj; if (x != other.x) return false; return true; } }
-
對象緩存泄漏
一旦把對象引用放入到緩存中,就很容易遺忘。
比如:代碼中會加載一個表中的數(shù)據(jù)到緩存(內(nèi)存)中,測試環(huán)境只有幾百條數(shù)據(jù),但是生產(chǎn)環(huán)境則可能會有幾百萬的數(shù)據(jù)。
優(yōu)化方案:可以使用 WeakHashMap 代表緩存,此種 Map 的特點是,當(dāng)除了自身有對 key 的引用外,此 key 沒有其他引用那么此 map 會自動丟棄此值。
/** * 演示內(nèi)存泄漏 */ public class MapTest { static Map wMap = new WeakHashMap(); static Map map = new HashMap(); public static void main(String[] args) { init(); testWeakHashMap(); testHashMap(); } public static void init() { String ref1 = new String("obejct1"); String ref2 = new String("obejct2"); String ref3 = new String("obejct3"); String ref4 = new String("obejct4"); wMap.put(ref1, "cacheObject1"); wMap.put(ref2, "cacheObject2"); map.put(ref3, "cacheObject3"); map.put(ref4, "cacheObject4"); System.out.println("String引用ref1,ref2,ref3,ref4 消失"); } public static void testWeakHashMap() { System.out.println("WeakHashMap GC之前"); for (Object o : wMap.entrySet()) { System.out.println(o); } try { System.gc(); TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("WeakHashMap GC之后"); for (Object o : wMap.entrySet()) { System.out.println(o); } } public static void testHashMap() { System.out.println("HashMap GC之前"); for (Object o : map.entrySet()) { System.out.println(o); } try { System.gc(); TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("HashMap GC之后"); for (Object o : map.entrySet()) { System.out.println(o); } } } /** * 結(jié)果 * String引用ref1,ref2,ref3,ref4 消失 * WeakHashMap GC之前 * obejct2=cacheObject2 * obejct1=cacheObject1 * WeakHashMap GC之后 * HashMap GC之前 * obejct4=cacheObject4 * obejct3=cacheObject3 * Disconnected from the target VM, address: '127.0.0.1:51628', transport: 'socket' * HashMap GC之后 * obejct4=cacheObject4 * obejct3=cacheObject3 **/
上面代碼演示 WeakHashMap 如何自動釋放緩存對象:當(dāng) init 函數(shù)執(zhí)行完成后,局部變量字符串引用 weakd1,weakd2,d1,d2 都會消失,此時只有靜態(tài) map 中保存中對字符串對象的引用,可以看到,調(diào)用 gc 之后,HashMap 的沒有被回收,而 WeakHashMap 里面的緩存被回收了。
-
監(jiān)聽器和回調(diào)
內(nèi)存泄漏另一個常見來源是監(jiān)聽器和其他回調(diào),如果客戶端在實現(xiàn)的 API 中注冊回調(diào),卻沒有顯式的取消,那么就會積聚。文章來源:http://www.zghlxwxcb.cn/news/detail-812590.html
需要確?;卣{(diào)立即被當(dāng)作垃圾回收的最佳方法是只保存它的弱引用,例如將它們保存成為 WeakHashMap 中的鍵。文章來源地址http://www.zghlxwxcb.cn/news/detail-812590.html
到了這里,關(guān)于內(nèi)存溢出、內(nèi)存泄露的概述及常見情形的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!