導言:java
對於java程序員來講,在虛擬機自動內存管理機制的幫助下,不須要本身實現釋放內存,不容易出現內存泄漏和內存溢出的問題,由虛擬機管理內存這一切看起來很是美好,可是一旦出現內存溢出或者內存泄漏的問題,對於不熟悉jvm虛擬機是怎麼使用內存的話,那麼排查錯誤將會是一項很是艱鉅的任務。因此在瞭解內存溢出以前先要搞明白JVM的內存模型。程序員
JVM(Java虛擬機)是一個抽象的計算模型。就如同一臺真實的機器,它有本身的指令集和執行引擎,能夠在運行時操控內存區域。目的是爲構建在其上運行的應用程序提供一個運行環境。JVM能夠解讀指令代碼並與底層進行交互:包括操做系統平臺和執行指令並管理資源的硬件體系結構。shell
根據 JVM8 規範,JVM 運行時內存共分爲虛擬機棧、堆、元空間、程序計數器、本地方法棧五個部分。還有一部份內存叫直接內存,屬於操做系統的本地內存,也是能夠直接操做的。
數據庫
元空間的本質和永久代相似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。數組
2.虛擬機棧(JVM Stacks)緩存
每一個線程有一個私有的棧,隨着線程的建立而建立。棧裏面存着的是一種叫「棧幀」的東西,每一個方法會建立一個棧幀,棧幀中存放了局部變量表(基本數據類型和對象引用)、操做數棧、方法出口等信息。棧的大小能夠固定也能夠動態擴展。數據結構
與虛擬機棧相似,區別是虛擬機棧執行java方法,本地方法站執行native方法。在虛擬機規範中對本地方法棧中方法使用的語言、使用方法與數據結構沒有強制規定,所以虛擬機能夠自由實現它。併發
程序計數器能夠當作是當前線程所執行的字節碼的行號指示器。在任何一個肯定的時刻,一個處理器(對於多內核來講是一個內核)都只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要一個獨立的程序計數器,咱們稱這類內存區域爲「線程私有」內存。框架
5.堆內存(Heap)jvm
堆內存是 JVM 全部線程共享的部分,在虛擬機啓動的時候就已經建立。全部的對象和數組都在堆上進行分配。這部分空間可經過 GC 進行回收。當申請不到空間時會拋出 OutOfMemoryError。堆是JVM內存佔用最大,管理最複雜的一個區域。其惟一的用途就是存放對象實例:全部的對象實例及數組都在對上進行分配。jdk1.8後,字符串常量池從永久代中剝離出來,存放在隊中。
6.直接內存(Direct Memory)
直接內存並非虛擬機運行時數據區的一部分,也不是Java 虛擬機規範中農定義的內存區域。在JDK1.4 中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O 方式,它可使用native 函數庫直接分配堆外內存,而後通脫一個存儲在Java堆中的DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在Java堆和Native堆中來回複製數據。
JVM運行時首先須要類加載器(classLoader)加載所需類的字節碼文件。加載完畢交由執行引擎執行,在執行過程當中須要一段空間來存儲數據(類比CPU與主存)。這段內存空間的分配和釋放過程正是咱們須要關心的運行時數據區。內存溢出的狀況就是從類加載器加載的時候開始出現的,內存溢出分爲兩大類:OutOfMemoryError和StackOverflowError。如下舉出10個內存溢出的狀況,並經過實例代碼的方式講解了是如何出現內存溢出的。
當出現java.lang.OutOfMemoryError:Java heap space異常時,就是堆內存溢出了。
1.問題描述
1.設置的jvm內存過小,對象所需內存太大,建立對象時分配空間,就會拋出這個異常。
2.流量/數據峯值,應用程序自身的處理存在必定的限額,好比必定數量的用戶或必定數量的數據。而當用戶數量或數據量忽然激增並超過預期的閾值時,那麼就會峯值中止前正常運行的操做將中止並觸發java . lang.OutOfMemoryError:Java堆空間錯誤
2.示例代碼
編譯如下代碼,執行時jvm參數設置爲-Xms20m -Xmx20m
以上這個示例,若是一次請求只分配一次5m的內存的話,請求量不多垃圾回收正常就不會出錯,可是一旦併發上來就會超出最大內存值,就會拋出內存溢出。
3.解決方法
首先,若是代碼沒有什麼問題的狀況下,能夠適當調整-Xms和-Xmx兩個jvm參數,使用壓力測試來調整這兩個參數達到最優值。
其次,儘可能避免大的對象的申請,像文件上傳,大批量從數據庫中獲取,這是須要避免的,儘可能分塊或者分批處理,有助於系統的正常穩定的執行。
最後,儘可能提升一次請求的執行速度,垃圾回收越早越好,不然,大量的併發來了的時候,再來新的請求就沒法分配內存了,就容易形成系統的雪崩。
1.問題描述
Java中的內存泄漏是一些對象再也不被應用程序使用但垃圾收集沒法識別的狀況。所以,這些未使用的對象仍然在Java堆空間中無限期地存在。不停的堆積最終會觸發java . lang.OutOfMemoryError。
2.示例代碼
當執行上面的代碼時,可能會指望它永遠運行,不會出現任何問題,假設單純的緩存解決方案只將底層映射擴展到10,000個元素,而不是全部鍵都已經在HashMap中。然而事實上元素將繼續被添加,由於key類並無重寫它的equals()方法。
隨着時間的推移,隨着不斷使用的泄漏代碼,「緩存」的結果最終會消耗大量Java堆空間。當泄漏內存填充堆區域中的全部可用內存時,垃圾收集沒法清理它,java . lang.OutOfMemoryError。
3.解決辦法
相對來講對應的解決方案比較簡單:重寫equals方法便可:
一、問題描述
當應用程序耗盡全部可用內存時,GC開銷限制超過了錯誤,而GC屢次未能清除它,這時便會引起java.lang.OutOfMemoryError。當JVM花費大量的時間執行GC,而收效甚微,而一旦整個GC的過程超過限制便會觸發錯誤(默認的jvm配置GC的時間超過98%,回收堆內存低於2%)。
2.示例代碼
3.解決方法
要減小對象生命週期,儘可能能快速的進行垃圾回收。
1.問題描述
元空間的溢出,系統會拋出java.lang.OutOfMemoryError: Metaspace。出現這個異常的問題的緣由是系統的代碼很是多或引用的第三方包很是多或者經過動態代碼生成類加載等方法,致使元空間的內存佔用很大。
2.示例代碼
如下是用循環動態生成class的方式來模擬元空間的內存溢出的。
3.解決辦法
默認狀況下,元空間的大小僅受本地內存限制。可是爲了整機的性能,儘可能仍是要對該項進行設置,以避免形成整機的服務停機。
1)優化參數配置,避免影響其餘JVM進程
-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:若是釋放了大量的空間,就適當下降該值;若是釋放了不多的空間,那麼在不超過MaxMetaspaceSize時,適當提升該值。
-XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。
除了上面兩個指定大小的選項之外,還有兩個與 GC 相關的屬性:
-XX:MinMetaspaceFreeRatio,在GC以後,最小的Metaspace剩餘空間容量的百分比,減小爲分配空間所致使的垃圾收集 。
-XX:MaxMetaspaceFreeRatio,在GC以後,最大的Metaspace剩餘空間容量的百分比,減小爲釋放空間所致使的垃圾收集。
2)慎重引用第三方包
對第三方包,必定要慎重選擇,不須要的包就去掉。這樣既有助於提升編譯打包的速度,也有助於提升遠程部署的速度。
3)關注動態生成類的框架
對於使用大量動態生成類的框架,要作好壓力測試,驗證動態生成的類是否超出內存的需求會拋出異常。
1.問題描述
在使用ByteBuffer中的allocateDirect()的時候會用到,不少javaNIO(像netty)的框架中被封裝爲其餘的方法,出現該問題時會拋出java.lang.OutOfMemoryError: Direct buffer memory異常。
若是你在直接或間接使用了ByteBuffer中的allocateDirect方法的時候,而不作clear的時候就會出現相似的問題。
2.示例代碼
3.解決辦法
若是常常有相似的操做,能夠考慮設置參數:-XX:MaxDirectMemorySize,並及時clear內存。
1.問題描述
當一個線程執行一個Java方法時,JVM將建立一個新的棧幀而且把它push到棧頂。此時新的棧幀就變成了當前棧幀,方法執行時,使用棧幀來存儲參數、局部變量、中間指令以及其餘數據。
當一個方法遞歸調用本身時,新的方法所產生的數據(也能夠理解爲新的棧幀)將會被push到棧頂,方法每次調用本身時,會拷貝一份當前方法的數據並push到棧中。所以,遞歸的每層調用都須要建立一個新的棧幀。這樣的結果是,棧中愈來愈多的內存將隨着遞歸調用而被消耗,若是遞歸調用本身一百萬次,那麼將會產生一百萬個棧幀。這樣就會形成棧的內存溢出。
2.示例代碼
3.解決辦法
若是程序中確實有遞歸調用,出現棧溢出時,能夠調高-Xss大小,就能夠解決棧內存溢出的問題了。遞歸調用防止造成死循環,不然就會出現棧內存溢出。
1.問題描述
線程基本只佔用heap之外的內存區域,也就是這個錯誤說明除了heap之外的區域,沒法爲線程分配一塊內存區域了,這個要麼是內存自己就不夠,要麼heap的空間設置得太大了,致使了剩餘的內存已經很少了,而因爲線程自己要佔用內存,因此就不夠用了。
2.示例代碼
3.解決方法
首先檢查操做系統是否有線程數的限制,使用shell也沒法建立線程,若是是這個問題就須要調整系統的最大可支持的文件數。
平常開發中儘可能保證線程最大數的可控制的,不要隨意使用線程池。不能無限制的增加下去。
1.問題描述
在Java應用程序啓動過程當中,能夠經過-Xmx和其餘相似的啓動參數限制指定的所需的內存。而當JVM所請求的總內存大於可用物理內存的狀況下,操做系統開始將內容從內存轉換爲硬盤。
通常來講JVM會拋出Out of swap space錯誤,表明應用程序向JVM native heap請求分配內存失敗而且native heap也即將耗盡時,錯誤消息中包含分配失敗的大小(以字節爲單位)和請求失敗的緣由。
2.解決辦法
增長系統交換區的大小,我我的認爲,若是使用了交換區,性能會大大下降,不建議採用這種方式,生產環境儘可能避免最大內存超過系統的物理內存。其次,去掉系統交換區,只使用系統的內存,保證應用的性能。
1.問題描述
有的時候會碰到這種內存溢出的描述Requested array size exceeds VM limit,通常來講java對應用程序所能分配數組最大大小是有限制的,只不過不一樣的平臺限制有所不一樣,但一般在1到21億個元素之間。當Requested array size exceeds VM limit錯誤出現時,意味着應用程序試圖分配大於Java虛擬機能夠支持的數組。JVM在爲數組分配內存以前,會執行特定平臺的檢查:分配的數據結構是否在此平臺是可尋址的。
2.示例代碼
如下就是代碼就是數組超出了最大限制。
3.解決方法
所以數組長度要在平臺容許的長度範圍以內。不過這個錯誤通常少見的,主要是因爲Java數組的索引是int類型。 Java中的最大正整數爲2 ^ 31 - 1 = 2,147,483,647。 而且平臺特定的限制能夠很是接近這個數字,例如:個人環境上(64位macOS,運行Jdk1.8)能夠初始化數組的長度高達2,147,483,645(Integer.MAX_VALUE-2)。如果在將數組的長度再增長1達到nteger.MAX_VALUE-1會出現的OutOfMemoryError。
1.問題概述
在描述該問題以前,先熟悉一點操做系統的知識:操做系統是創建在進程的概念之上,這些進程在內核中做業,其中有一個很是特殊的進程,稱爲「內存殺手(Out of memory killer)」。當內核檢測到系統內存不足時,OOM killer被激活,檢查當前誰佔用內存最多而後將該進程殺掉。
通常Out of memory:Kill process or sacrifice child錯會在當可用虛擬虛擬內存(包括交換空間)消耗到讓整個操做系統面臨風險時,會被觸發。在這種狀況下,OOM Killer會選擇「流氓進程」並殺死它。
2.示例代碼
3.解決方法
雖然增長交換空間的方式能夠緩解Java heap space異常,仍是建議最好的方案就是升級系統內存,讓java應用有足夠的內存可用,就不會出現這種問題。
經過以上的10種出現內存溢出狀況,你們在實際碰到問題時也就會知道怎麼解決了,在實際編碼中也要記得:
1.第三方jar包要慎重引入,堅定去掉沒有用的jar包,提升編譯的速度和系統的佔用內存。
2.對於大的對象或者大量的內存申請,要進行優化,大的對象要分片處理,提升處理性能,減小對象生命週期。
3.儘可能固定線程的數量,保證線程佔用內存可控,同時須要大量線程時,要優化好操做系統的最大可打開的鏈接數。
4.對於遞歸調用,也要控制好遞歸的層級,不要過高,超過棧的深度。
5.分配給棧的內存並非越大越好,由於棧內存越大,線程多,留給堆的空間就很少了,容易拋出OOM。JVM的默認參數通常狀況沒有問題(包括遞歸)。