java内存溢出 栈溢出的原因与排查方法
java內(nèi)存溢出 原因與排查方法
1、 內(nèi)存溢出的原因是什么?
內(nèi)存溢出是由于沒被引用的對象(垃圾)過多造成JVM沒有及時回收,導(dǎo)致剩余的內(nèi)存不夠用,造成的內(nèi)存溢出。如果出現(xiàn)這種現(xiàn)象可行代碼排查:
一)是否應(yīng)用中的類中和引用變量過多使用了Static修飾 如public staitc Student s;在類中的屬性中使用 static修飾的最好只用基本類型或字符串。如public static int i = 0; //public static String str;
二)是否 應(yīng)用 中使用了大量的遞歸或無限遞歸(遞歸中用到了大量的建新的對象)
三)是否App中使用了大量循環(huán)或死循環(huán)(循環(huán)中用到了大量的新建的對象)
四)檢查 應(yīng)用 中是否使用了向數(shù)據(jù)庫查詢所有記錄的方法。即一次性全部查詢的方法,如果數(shù)據(jù)量超過10萬多條了,就可能會造成內(nèi)存溢出。所以在查詢時應(yīng)采用“分頁查詢”。
五)檢查是否有數(shù)組,List,Map中存放的是對象的引用而不是對象,因為這些引用會讓對應(yīng)的對象不能被釋放。會大量存儲在內(nèi)存中。
六)檢查是否使用了“非字面量字符串進(jìn)行+”的操作。因為String類的內(nèi)容是不可變的,每次運(yùn)行"+"就會產(chǎn)生新的對象,如果過多會造成新String對象過多,從而導(dǎo)致JVM沒有及時回收而出現(xiàn)內(nèi)存溢出。
如String s1 = "My name";
String s2 = "is";
String s3 = "xuwei";
String str = s1 + s2 + s3 +.........;這是會容易造成內(nèi)存溢出的
但是String str =??"My name" + " is " + " xuwei" + " nice " + " to " + " meet you"; //但是這種就不會造成內(nèi)存溢出。因為這是”字面量字符串“,在運(yùn)行"+"時就會在編譯期間運(yùn)行好。不會按照J(rèn)VM來執(zhí)行的。
在使用String,StringBuffer,StringBuilder時,如果是字面量字符串進(jìn)行"+"時,應(yīng)選用String性能更好;如果是String類進(jìn)行"+"時,在不考慮線程安全時,應(yīng)選用StringBuilder性能更好。
public class Test { public void testHeap(){ for(;;){ //死循環(huán)一直創(chuàng)建對象,堆溢出ArrayList list = new ArrayList (2000); } } int num=1; public void testStack(){ //無出口的遞歸調(diào)用,棧溢出num++; this.testStack(); } public static void main(String[] args){ Test t = new Test (); t.testHeap(); t.testStack(); } }?
2、棧溢出的原因
?一)、是否有遞歸調(diào)用
二)、是否有大量循環(huán)或死循環(huán)
三)、全局變量是否過多
四)、 數(shù)組、List、map數(shù)據(jù)是否過大
五)使用DDMS工具進(jìn)行查找大概出現(xiàn)棧溢出的位置
后續(xù)持續(xù)更新 請看到的及時補(bǔ)充到評論區(qū)……
下面是摘自掘金中的一篇文章,在項目過程中或多或少遇到過,由于本人不想再一一做測試用例,就摘錄過來了,最后附帶地址鏈接 ,可以方便大家去看原文(尊重原著)
?
JVM系列之實戰(zhàn)內(nèi)存溢出異常
實戰(zhàn)內(nèi)存溢出異常
大家好,相信大部分Javaer在code時經(jīng)常會遇到本地代碼運(yùn)行正常,但在生產(chǎn)環(huán)境偶爾會莫名其妙的報一些關(guān)于內(nèi)存的異常,StackOverFlowError,OutOfMemoryError異常是最常見的。今天就基于上篇文章JVM系列之Java內(nèi)存結(jié)構(gòu)詳解講解的各個內(nèi)存區(qū)域重點實戰(zhàn)分析下內(nèi)存溢出的情況。在此之前,我還是想多余累贅一些其他關(guān)于對象的問題,具體內(nèi)容如下:
文章結(jié)構(gòu)
對象的創(chuàng)建過程
對象的內(nèi)存布局
對象的訪問定位
實戰(zhàn)內(nèi)存異常
1 . 對象的創(chuàng)建過程
關(guān)于對象的創(chuàng)建,第一反應(yīng)是new關(guān)鍵字,那么本文就主要講解new關(guān)鍵字創(chuàng)建對象的過程。
Student stu =new Student("張三","18");就拿上面這句代碼來說,虛擬機(jī)首先會去檢查Student這個類有沒有被加載,如果沒有,首先去加載這個類到方法區(qū),然后根據(jù)加載的Class類對象創(chuàng)建stu實例對象,需要注意的是,stu對象所需的內(nèi)存大小在Student類加載完成后便可完全確定。內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間的實例數(shù)據(jù)部分初始化為零值,這也就是為什么我們在編寫Java代碼時創(chuàng)建一個變量不需要初始化。緊接著,虛擬機(jī)會對對象的對象頭進(jìn)行必要的設(shè)置,如這個對象屬于哪個類,如何找到類的元數(shù)據(jù)(Class對象),對象的鎖信息,GC分代年齡等。設(shè)置完對象頭信息后,調(diào)用類的構(gòu)造函數(shù)。
其實講實話,虛擬機(jī)創(chuàng)建對象的過程遠(yuǎn)不止這么簡單,我這里只是把大致的脈絡(luò)講解了一下,方便大家理解。
2 . 對象的內(nèi)存布局
剛剛提到的實例數(shù)據(jù),對象頭,有些小伙伴也許有點陌生,這一小節(jié)就詳細(xì)講解一下對象的內(nèi)存布局,對象創(chuàng)建完成后大致可以分為以下幾個部分:
對象頭
實例數(shù)據(jù)
對齊填充
對象頭: 對象頭中包含了對象運(yùn)行時一些必要的信息,如GC分代信息,鎖信息,哈希碼,指向Class類元信息的指針等,其中對Javaer比較有用的是鎖信息與指向Class對象的指針,關(guān)于鎖信息,后期有機(jī)會講解并發(fā)編程JUC時再擴(kuò)展,關(guān)于指向Class對象的指針其實很好理解。比如上面那個Student的例子,當(dāng)我們拿到stu對象時,調(diào)用Class stuClass=stu.getClass();的時候,其實就是根據(jù)這個指針去拿到了stu對象所屬的Student類在方法區(qū)存放的Class類對象。雖然說的有點拗口,但這句話我反復(fù)琢磨了好幾遍,應(yīng)該是說清楚了。
實例數(shù)據(jù):實例數(shù)據(jù)部分是對象真正存儲的有效信息,就是程序代碼中所定義的各種類型的字段內(nèi)容。
對齊填充:虛擬機(jī)規(guī)范要求對象大小必須是8字節(jié)的整數(shù)倍。對齊填充其實就是來補(bǔ)全對象大小的。
3 . 對象的訪問定位
談到對象的訪問,還拿上面學(xué)生的例子來說,當(dāng)我們拿到stu對象時,直接調(diào)用stu.getName();時,其實就完成了對對象的訪問。但這里要累贅說一下的是,stu雖然通常被認(rèn)為是一個對象,其實準(zhǔn)確來說是不準(zhǔn)確的,stu只是一個變量,變量里存儲的是指向?qū)ο蟮闹羔?#xff0c;(如果干過C或者C++的小伙伴應(yīng)該比較清楚指針這個概念),當(dāng)我們調(diào)用stu.getName()時,虛擬機(jī)會根據(jù)指針找到堆里面的對象然后拿到實例數(shù)據(jù)name.需要注意的是,當(dāng)我們調(diào)用stu.getClass()時,虛擬機(jī)會首先根據(jù)stu指針定位到堆里面的對象,然后根據(jù)對象頭里面存儲的指向Class類元信息的指針再次到方法區(qū)拿到Class對象,進(jìn)行了兩次指針尋找。具體講解圖如下:
?
4 .實戰(zhàn)內(nèi)存異常
內(nèi)存異常是我們工作當(dāng)中經(jīng)常會遇到問題,但如果僅僅會通過加大內(nèi)存參數(shù)來解決問題顯然是不夠的,應(yīng)該通過一定的手段定位問題,到底是因為參數(shù)問題,還是程序問題(無限創(chuàng)建,內(nèi)存泄露)。定位問題后才能采取合適的解決方案,而不是一內(nèi)存溢出就查找相關(guān)參數(shù)加大。
概念:
內(nèi)存泄露:代碼中的某個對象本應(yīng)該被虛擬機(jī)回收,但因為擁有GCRoot引用而沒有被回收。關(guān)于GCRoot概念,下一篇文章講解。
內(nèi)存溢出: 虛擬機(jī)由于堆中擁有太多不可回收對象沒有回收,導(dǎo)致無法繼續(xù)創(chuàng)建新對象。
在分析問題之前先給大家講一講排查內(nèi)存溢出問題的方法,內(nèi)存溢出時JVM虛擬機(jī)會退出,那么我們怎么知道JVM運(yùn)行時的各種信息呢,Dump機(jī)制會幫助我們,可以通過加上VM參數(shù)-XX:+HeapDumpOnOutOfMemoryError讓虛擬機(jī)在出現(xiàn)內(nèi)存溢出異常時生成dump文件,然后通過外部工具(作者使用的是VisualVM)來具體分析異常的原因。
下面從以下幾個方面來配合代碼實戰(zhàn)演示內(nèi)存溢出及如何定位:
Java堆內(nèi)存異常
Java棧內(nèi)存異常
方法區(qū)內(nèi)存異常
Java堆內(nèi)存異常
/** VM Args: //這兩個參數(shù)保證了堆中的可分配內(nèi)存固定為20M -Xms20m -Xmx20m //文件生成的位置,則生成在桌面的一個目錄 -XX:+HeapDumpOnOutOfMemoryError //文件生成的位置,則生成在桌面的一個目錄 //文件生成的位置,則生成在桌面的一個目錄 -XX:HeapDumpPath=/Users/zdy/Desktop/dump/ */ public class HeapOOM {//創(chuàng)建一個內(nèi)部類用于創(chuàng)建對象使用static class OOMObject {}public static void main(String[] args) {List<OOMObject> list = new ArrayList<OOMObject>();//無限創(chuàng)建對象,在堆中while (true) {list.add(new OOMObject());}} }Run起來代碼后爆出異常如下:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to /Users/zdy/Desktop/dump/java_pid1099.hprof ...
可以看到生成了dump文件到指定目錄。并且爆出了OutOfMemoryError,還告訴了你是哪一片區(qū)域出的問題:heap space
打開VisualVM工具導(dǎo)入對應(yīng)的heapDump文件(如何使用請讀者自行查閱相關(guān)資料),相應(yīng)的說明見圖:
分析dump文件后,我們可以知道,OOMObject這個類創(chuàng)建了810326個實例。所以它能不溢出嗎?接下來就在代碼里找這個類在哪new的。排查問題。(我們的樣例代碼就不用排查了,While循環(huán)太兇猛了)
Java棧內(nèi)存異常
老實說,在棧中出現(xiàn)異常(StackOverFlowError)的概率很小,小到和去蘋果專賣店買手機(jī),買回來后發(fā)現(xiàn)是Android系統(tǒng)的概率是一樣的。因為作者確實沒有在生產(chǎn)環(huán)境中遇到過,除了自己作死寫樣例代碼測試。先說一下異常出現(xiàn)的情況,前面講到過,方法調(diào)用的過程就是方法幀進(jìn)虛擬機(jī)棧和出虛擬機(jī)棧的過程,那么有兩種情況可以導(dǎo)致StackOverFlowError,當(dāng)一個方法幀(比如需要2M內(nèi)存)進(jìn)入到虛擬機(jī)棧(比如還剩下1M內(nèi)存)的時候,就會報出StackOverFlow.這里先說一個概念,棧深度:指目前虛擬機(jī)棧中沒有出棧的方法幀。虛擬機(jī)棧容量通過參數(shù)-Xss來控制,下面通過一段代碼,把棧容量人為的調(diào)小一點,然后通過遞歸調(diào)用觸發(fā)異常。
/** * VM Args: //設(shè)置棧容量為160K,默認(rèn)1M -Xss160k */ public class JavaVMStackSOF {private int stackLength = 1;public void stackLeak() {stackLength++;//遞歸調(diào)用,觸發(fā)異常stackLeak();}public static void main(String[] args) throws Throwable {JavaVMStackSOF oom = new JavaVMStackSOF();try {oom.stackLeak();} catch (Throwable e) {System.out.println("stack length:" + oom.stackLength);throw e;}} }結(jié)果如下:
stack length:751
Exception in thread "main" java.lang.StackOverflowError
可以看到,遞歸調(diào)用了751次,棧容量不夠用了。
默認(rèn)的棧容量在正常的方法調(diào)用時,棧深度可以達(dá)到1000-2000深度,所以,一般的遞歸是可以承受的住的。如果你的代碼出現(xiàn)了StackOverflowError,首先檢查代碼,而不是改參數(shù)。
這里順帶提一下,很多人在做多線程開發(fā)時,當(dāng)創(chuàng)建很多線程時,容易出現(xiàn)OOM(OutOfMemoryError),
這時可以通過具體情況,減少最大堆容量,或者棧容量來解決問題,這是為什么呢。請看下面的公式:
線程數(shù) * (最大棧容量) + 最大堆值 + 其他內(nèi)存(忽略不計或者一般不改動) = 機(jī)器最大內(nèi)存
當(dāng)線程數(shù)比較多時,且無法通過業(yè)務(wù)上削減線程數(shù),那么再不換機(jī)器的情況下,你只能把最大棧容量設(shè)置小一點,或者把最大堆值設(shè)置小一點。
方法區(qū)內(nèi)存異常
寫到這里時,作者本來想寫一個無限創(chuàng)建動態(tài)代理對象的例子來演示方法區(qū)溢出,避開談?wù)揓DK7與JDK8的內(nèi)存區(qū)域變更的過渡,但細(xì)想一想,還是把這一塊從始致終的說清楚。在上一篇文章中JVM系列之Java內(nèi)存結(jié)構(gòu)詳解講到方法區(qū)時提到,JDK7環(huán)境下方法區(qū)包括了(運(yùn)行時常量池),其實這么說是不準(zhǔn)確的。因為從JDK7開始,HotSpot團(tuán)隊就想到開始去"永久代",大家首先明確一個概念,方法區(qū)和"永久代"(PermGen space)是兩個概念,方法區(qū)是JVM虛擬機(jī)規(guī)范,任何虛擬機(jī)實現(xiàn)(J9等)都不能少這個區(qū)間,而"永久代"只是HotSpot對方法區(qū)的一個實現(xiàn)。為了把知識點列清楚,我還是才用列表的形式:
JDK7之前(包括JDK7)擁有"永久代"(PermGen space),用來實現(xiàn)方法區(qū)。但在JDK7中已經(jīng)逐漸在實現(xiàn)中把永久代中把很多東西移了出來,比如:符號引用(Symbols)轉(zhuǎn)移到了native heap,運(yùn)行時常量池(interned strings)轉(zhuǎn)移到了java heap;類的靜態(tài)變量(class statics)轉(zhuǎn)移到了java heap。
所以這就是為什么我說上一篇文章中說方法區(qū)中包含運(yùn)行時常量池是不正確的,因為已經(jīng)移動到了java heap;
在JDK7之前(包括7)可以通過-XX:PermSize -XX:MaxPermSize來控制永久代的大小。
JDK8正式去除"永久代",換成Metaspace(元空間)作為JVM虛擬機(jī)規(guī)范中方法區(qū)的實現(xiàn)。
元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制,但仍可以通過參數(shù)控制:-XX:MetaspaceSize與-XX:MaxMetaspaceSize來控制大小。
下面作者還是通過一段代碼,來不停的創(chuàng)建Class對象,在JDK8中可以看到metaSpace內(nèi)存溢出:
在JDK8的環(huán)境下將報出異常:
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
這是因為在調(diào)用CGLib的創(chuàng)建代理時會生成動態(tài)代理類,即Class對象到Metaspace,所以While一下就出異常了。
提醒一下:雖然我們?nèi)粘=?#34;堆Dump",但是dump技術(shù)不僅僅是對于"堆"區(qū)域才有效,而是針對OOM的,也就是說不管什么區(qū)域,凡是能夠報出OOM錯誤的,都可以使用dump技術(shù)生成dump文件來分析。
在經(jīng)常動態(tài)生成大量Class的應(yīng)用中,需要特別注意類的回收狀況,這類場景除了例子中的CGLib技術(shù),常見的還有,大量JSP,反射,OSGI等。需要特別注意,當(dāng)出現(xiàn)此類異常,應(yīng)該知道是哪里出了問題,然后看是調(diào)整參數(shù),還是在代碼層面優(yōu)化。
附加-直接內(nèi)存異常
直接內(nèi)存異常非常少見,而且機(jī)制很特殊,因為直接內(nèi)存不是直接向操作系統(tǒng)分配內(nèi)存,而且通過計算得到的內(nèi)存不夠而手動拋出異常,所以當(dāng)你發(fā)現(xiàn)你的dump文件很小,而且沒有明顯異常,只是告訴你OOM,你就可以考慮下你代碼里面是不是直接或者間接使用了NIO而導(dǎo)致直接內(nèi)存溢出。
?
?
原文地址https://my.oschina.net/u/2401092/blog/1621850
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
總結(jié)
以上是生活随笔為你收集整理的java内存溢出 栈溢出的原因与排查方法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 程序江湖:第十八章 察颜观色的伙伴
- 下一篇: 分享:笔记本电脑有杂音解决技巧