JVM04-JVM中內(nèi)存溢出(包括內(nèi)存泄露)以及其處理方法
文章目錄
前言
一些基本的設(shè)置說明
堆內(nèi)存溢出
Java棧內(nèi)存異常
方法區(qū)和運(yùn)行時(shí)常量池溢出
直接內(nèi)存溢出
內(nèi)存泄露
內(nèi)存泄露的定義:
解決辦法
內(nèi)存溢出的原因分析:
線上內(nèi)存溢出的處理方法
總結(jié)
前言
上一篇我們介紹了JVM03–JVM垃圾收集機(jī)制的一些基本概念,這一篇介紹一下JVM中各種內(nèi)存溢出(包括內(nèi)存泄露)及其處理方法。
本文會(huì)按照J(rèn)VM中內(nèi)存劃分來介紹各種內(nèi)存溢出的例子。
一些基本的設(shè)置說明
為了模擬出內(nèi)存溢出的效果,我們需要手動(dòng)設(shè)置內(nèi)存區(qū)域的內(nèi)存大小,下面就是設(shè)置值部分設(shè)置值及其說明。
一個(gè)項(xiàng)目的啟動(dòng)設(shè)置
nohup java -Xms2048m -Xmx2048m -jar api.jar --spring.profiles.active=prod > log.file 2>&1 &
堆內(nèi)存溢出
Java堆用于存儲(chǔ)對象的實(shí)例,我們只要不斷地創(chuàng)建對象,并且保證GC Roots到對象之間有可達(dá)路徑來避免垃圾回收機(jī)制清除這些對象,那么隨著對象數(shù)量的增加,總?cè)萘坑|及最大堆的容量限制后就會(huì)產(chǎn)生內(nèi)存溢出。Java堆內(nèi)存的OutOfMemoryError異常是實(shí)際應(yīng)用中最常見的內(nèi)存溢出異常情況。出現(xiàn)Java堆內(nèi)存溢出時(shí),異常堆棧信息“java.lang.OutOfMemoryError”會(huì)跟隨進(jìn)一步提示“Java heap space”。下面舉個(gè)例子來模擬堆內(nèi)存溢出。這里將-Xms和-Xmx都設(shè)置成20M,保證了Java堆內(nèi)存不可擴(kuò)展。然后,通過-XX:HeapDumpPath指定dump文件的保存位置。這里通過while循環(huán)不斷的創(chuàng)建對象,然后保存到集合中。
/**
- Java堆內(nèi)存異常
VM Args:
//這兩個(gè)參數(shù)保證了堆中的可分配內(nèi)存固定為20M
-Xms20m
-Xmx20m
-XX:+HeapDumpOnOutOfMemoryError //自動(dòng)生成dump文件
//文件生成的位置,作為生成在桌面的一個(gè)目錄
-XX:HeapDumpPath=D:\srv\dump
*
-
@author xiang.wei
-
@date 2020/5/27 13:35
/
public class HeapOOM {
/*- 創(chuàng)建一個(gè)內(nèi)部類用于創(chuàng)建對象使用
*/
static class OOMObject {
}
public static void main(String[] args) {
List list = new ArrayList<>();
//無限的創(chuàng)建對象放在堆中
while (true) {
list.add(new OOMObject());
}
}
} - 創(chuàng)建一個(gè)內(nèi)部類用于創(chuàng)建對象使用
下面簡單的說一下在Idea中設(shè)置應(yīng)用運(yùn)行內(nèi)存的方法,我們只需要在 Run---->Edit Configurations—>找到需要設(shè)置的主類,然后在VM options中添加
-Xms20M -Xmx20M -Xmn20M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\srv\dump
如下圖所示:
上面程序運(yùn)行結(jié)果如下:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to D:\srv\dump\java_pid23624.hprof …
Heap dump file created [28062091 bytes in 0.143 secs]
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at com.jay.service.HeapOOM.main(HeapOOM.java:31)
Java HotSpot? 64-Bit Server VM warning: MaxNewSize (20480k) is equal to
or greater than the entire heap (20480k). A new max generation size of
19968k will be used.
我們可以看到程序生成了dump文件到指定目錄,打開jvisualvm.exe工具導(dǎo)入heapDump文件,相應(yīng)的說明如下圖所示:
一共創(chuàng)建了802858個(gè)實(shí)例,消耗了 98.9%的運(yùn)行內(nèi)存。
切換到實(shí)例數(shù)如下圖所示:
如何解決堆內(nèi)存的OOM異常呢,首先需要確認(rèn)內(nèi)存中導(dǎo)致OOM的對象是否是必要的,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄露(Memory Leak)還是內(nèi)存溢出(Memory Overflow)。
Java棧內(nèi)存異常
說完了Java堆內(nèi)存異常,下面我們來看看Java棧內(nèi)存異常,在實(shí)際開發(fā)中發(fā)生棧內(nèi)存異常的情況比較少。Java棧內(nèi)存異常發(fā)生的兩種情況是:
如果線程請求的棧深度(棧深度:指目前虛擬機(jī)棧中沒有出棧的方法幀)大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError異常。這種情況在遞歸的場景下可能會(huì)出現(xiàn),如果在使用遞歸算法時(shí)沒有控制好遞歸的跳出條件,就有可能會(huì)出現(xiàn)這種情況,下面的例子就是一個(gè)沒有跳出條件的遞歸調(diào)用。根據(jù)前面說明,我們可以通過-Xss160k設(shè)置棧容量為160K。
如果虛擬機(jī)的棧內(nèi)存允許動(dòng)態(tài)擴(kuò)展,當(dāng)擴(kuò)展容量無法申請到足夠的內(nèi)存時(shí),將拋出OutOfMemoryError異常。
如下這個(gè)例子:
/**
-
VM Args:
-
設(shè)置棧容量為160K,默認(rèn)1M
-
-Xss160k
-
@author xiang.wei
-
@date 2020/5/27 16:09
*/
public class JavaVMStackSOF {private int stackLength = 1;
public void stackLeak() {
stackLength++;
System.out.println("*********棧的深度是:" + stackLength);
//遞歸調(diào)用
stackLeak();
}public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Exception e) {
throw e;
}
}
}
運(yùn)行結(jié)果如下:
*********棧的深度是:1287
Exception in thread “main” java.lang.StackOverflowError
at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)
可以看到,遞歸調(diào)用了1287次,棧容量不夠用了。
默認(rèn)的棧容量在正常的方法調(diào)用時(shí),棧深度可以達(dá)到1000-2000深度,所以,一般的遞歸可以承受的住,如果代碼中出現(xiàn)了StackOverflowError,首先需要檢查代碼,看看是不是遞歸寫的不對。不能只是調(diào)參數(shù)。
當(dāng)創(chuàng)建很多線程時(shí),容易出現(xiàn)OOM(OutOfMemoryError),這時(shí)可以通過具體情況,減少最大堆容量,或者棧容量來解決問題。
線程數(shù)(最大棧容量)+最大堆值+其他內(nèi)存(忽略不計(jì)或者一般不改動(dòng))=機(jī)器最大內(nèi)存*
當(dāng)線程數(shù)比較多時(shí),且無法通過業(yè)務(wù)上減少線程數(shù),再不換機(jī)器的情況下,我們只能把最大棧容量設(shè)置小一點(diǎn),或者把最大堆值設(shè)置小一點(diǎn)。
方法區(qū)和運(yùn)行時(shí)常量池溢出
由于運(yùn)行時(shí)常量池是方法區(qū)的一部分,所以這兩個(gè)區(qū)域的溢出測試可以放在一起進(jìn)行。需要注意的是HotSpot從JDK7開始逐步“去永久代”的計(jì)劃,并在JDK8中完全使用元空間代替永久代,使用"永久代"還是"元空間"來實(shí)現(xiàn)方法區(qū),對程序的影響是不同的。
元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存,因此,默認(rèn)情況下元空間的大小僅受本地內(nèi)存限制,但仍可以通過參數(shù)控制:
-XX:MetaspaceSize與-XX:MaxMetaspaceSize來控制大小。
方法區(qū)溢出也是一種常見的內(nèi)存溢出異常,一個(gè)類如果要被垃圾收集器回收,要達(dá)成的條件是比較苛刻的。在經(jīng)常運(yùn)行時(shí)生成大量動(dòng)態(tài)類的應(yīng)用場景里,就應(yīng)該特別關(guān)注這些類的回收狀況,這類場景除了之前提到的程序使用了CGLib字節(jié)碼增強(qiáng)和動(dòng)態(tài)語言外,常見的還有:大量JSP或動(dòng)態(tài)產(chǎn)生JSP文件的應(yīng)用(JSP第一次運(yùn)行時(shí)需要編譯為Java類)、基于OSGI的應(yīng)用(即使是同一個(gè)類文件,被不同的加載器加載也會(huì)被視為不同的類)。
方法區(qū)的主要職責(zé)是用于存放類型的相關(guān)信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。 對于這部分區(qū)域的測試,基本的思路是運(yùn)行時(shí)產(chǎn)生大量的類去填滿方法區(qū),直到溢出為止。
這是因?yàn)樵谡{(diào)用CGLib創(chuàng)建代理時(shí)會(huì)生成動(dòng)態(tài)代理類,即Class對象到Metaspace,所以while一下就出異常了。
下面這個(gè)例子就是通過設(shè)置元空間大小,然后,通過動(dòng)態(tài)代理生成大量的類,來模擬元空間內(nèi)存溢出的情況。
/**
-
在JDK8下測試方法區(qū),所以設(shè)置了Metaspace的大小為固定的8M
-XX:MetaspaceSize=8m
-XX:MaxMetaspaceSize=8m -
@author xiang.wei
-
@date 2020/5/27 17:29
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(JOOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, objects, methodProxy) -> methodProxy.invokeSuper(obj,objects));
//無限創(chuàng)建動(dòng)態(tài)代理,生成class對象
enhancer.create();
}
}
static class JOOMObject{}
運(yùn)行結(jié)果:
Exception in thread “main” java.lang.OutOfMemoryError: Metaspace
at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:538)
at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
直接內(nèi)存溢出
直接內(nèi)存導(dǎo)致的內(nèi)存溢出,一個(gè)明顯的特征是在Heap
Dump文件中不會(huì)看見有什么明顯的異常情況,如果發(fā)現(xiàn)內(nèi)存溢出之后產(chǎn)生的Dump文件很小,而程序中又直接或者間接使用了DirectMemory(典型的的間接使用就是NIO),那就可以考慮重點(diǎn)檢查一下直接內(nèi)存方面的原因了。
直接內(nèi)存溢出的舉例;
Exception in thread “main” java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
--------------------------------------------------------------------------------------------面試總結(jié)---------------------------------------------------------------------------------------------------------------------
內(nèi)存泄露
內(nèi)存泄露的定義:
內(nèi)存泄露是指程序在申請內(nèi)存時(shí),無法釋放已經(jīng)申請的內(nèi)存空間,這就造成了內(nèi)存泄露,一次內(nèi)存泄露似乎不會(huì)有大的影響,但是內(nèi)存泄露堆積的后果就是內(nèi)存溢出。
解決辦法
資源性對象,使用完之后需要手動(dòng)的close,例如:輸入流InputStream等等。
集合容器中的內(nèi)存泄露,我們通常把一些對象加入到集合容器中(例如:ArrayList)中,當(dāng)我們不需要該對象時(shí)該對象時(shí),并沒有把它的引用從集合中清理掉,這樣這個(gè)集合就會(huì)越來越大,如果這個(gè)集合是static的話,那情況就更加嚴(yán)重了,前面的HeapOOM就是這種情況,最終內(nèi)存溢出。
使用ThreaLocal時(shí),當(dāng)Thread長時(shí)間不結(jié)束,存在大量廢棄的ThreadLocal,而又不再添加新的ThreadLocal時(shí)會(huì)發(fā)生內(nèi)存泄露。
內(nèi)存溢出的原因分析:
內(nèi)存中加載的數(shù)據(jù)量過于龐大,如一次從數(shù)據(jù)庫中取出過多數(shù)據(jù)
集合中有對象的引用,使用完后未清空,產(chǎn)生了堆積,使得JVM不能回收
代碼中存在死循環(huán)或者循環(huán)產(chǎn)生過多重復(fù)的對象實(shí)體
使用的第三方的軟件的BUG。
啟動(dòng)參數(shù)內(nèi)存值設(shè)定的過小。
線上內(nèi)存溢出的處理方法
將內(nèi)存對賬的信息dump下來,使用:
jmap -dump:format=b,file=文件名 [pid]
將內(nèi)存堆棧的信息導(dǎo)入VisualVM中進(jìn)行分析。
總結(jié)
本文首先介紹了堆內(nèi)存溢出(OutOfMemoryError)發(fā)生的場景以及處理方式,OutOfMemoryError發(fā)生的場景主要就是系統(tǒng)創(chuàng)建了大量的對象,并且這些對象是有效的(即保證GC Roots到對象之間有可達(dá)路徑)。然后,介紹了棧內(nèi)存異常(StackOverflowError)的發(fā)生場景以及處理方式,StackOverflowError發(fā)生的場景主要是線程調(diào)用棧深度超過了虛擬機(jī)運(yùn)行的棧深度。最后,介紹了方法區(qū)的內(nèi)存溢出。需要注意的是JDK1.8中完全移除了永久代,取而代之的是元空間。
作者:碼農(nóng)飛哥
微信公眾號(hào):碼農(nóng)飛哥