你們好,相信大部分Javaer在code時常常會遇到本地代碼運行正常,但在生產環境偶爾會莫名其妙的報一些關於內存的異常,StackOverFlowError,OutOfMemoryError異常是最多見的。今天就基於上篇文章JVM系列之Java內存結構詳解講解的各個內存區域重點實戰分析下內存溢出的狀況。在此以前,我仍是想多餘累贅一些其餘關於對象的問題,具體內容以下:java
文章結構編程
- 對象的建立過程
- 對象的內存佈局
- 對象的訪問定位
- 實戰內存異常
關於對象的建立,第一反應是new關鍵字,那麼本文就主要講解new關鍵字建立對象的過程。多線程
Student stu =new Student("張三","18");複製代碼
就拿上面這句代碼來講,虛擬機首先會去檢查Student這個類有沒有被加載,若是沒有,首先去加載這個類到方法區,而後根據加載的Class類對象建立stu實例對象,須要注意的是,stu對象所需的內存大小在Student類加載完成後即可徹底肯定。內存分配完成後,虛擬機須要將分配到的內存空間的實例數據部分初始化爲零值,這也就是爲何咱們在編寫Java代碼時建立一個變量不須要初始化。緊接着,虛擬機會對對象的對象頭進行必要的設置,如這個對象屬於哪一個類,如何找到類的元數據(Class對象),對象的鎖信息,GC分代年齡等。設置完對象頭信息後,調用類的構造函數。
其實講實話,虛擬機建立對象的過程遠不止這麼簡單,我這裏只是把大體的脈絡講解了一下,方便你們理解。併發
剛剛提到的實例數據,對象頭,有些小夥伴也許有點陌生,這一小節就詳細講解一下對象的內存佈局,對象建立完成後大體能夠分爲如下幾個部分:函數
對象頭: 對象頭中包含了對象運行時一些必要的信息,如GC分代信息,鎖信息,哈希碼,指向Class類元信息的指針等,其中對Javaer比較有用的是鎖信息與指向Class對象的指針,關於鎖信息,後期有機會講解併發編程JUC時再擴展,關於指向Class對象的指針其實很好理解。好比上面那個Student的例子,當咱們拿到stu對象時,調用Class stuClass=stu.getClass();的時候,其實就是根據這個指針去拿到了stu對象所屬的Student類在方法區存放的Class類對象。雖說的有點拗口,但這句話我反覆琢磨了好幾遍,應該是說清楚了。^_^工具
實例數據:實例數據部分是對象真正存儲的有效信息,就是程序代碼中所定義的各類類型的字段內容。佈局
對齊填充:虛擬機規範要求對象大小必須是8字節的整數倍。對齊填充其實就是來補全對象大小的。post
談到對象的訪問,還拿上面學生的例子來講,當咱們拿到stu對象時,直接調用stu.getName();時,其實就完成了對對象的訪問。但這裏要累贅說一下的是,stu雖然一般被認爲是一個對象,其實準確來講是不許確的,stu只是一個變量,變量裏存儲的是指向對象的指針,(若是幹過C或者C++的小夥伴應該比較清楚指針這個概念),當咱們調用stu.getName()時,虛擬機會根據指針找到堆裏面的對象而後拿到實例數據name.須要注意的是,當咱們調用stu.getClass()時,虛擬機會首先根據stu指針定位到堆裏面的對象,而後根據對象頭裏面存儲的指向Class類元信息的指針再次到方法區拿到Class對象,進行了兩次指針尋找。具體講解圖以下:
測試
內存異常是咱們工做當中常常會遇到問題,但若是僅僅會經過加大內存參數來解決問題顯然是不夠的,應該經過必定的手段定位問題,究竟是由於參數問題,仍是程序問題(無限建立,內存泄露)。定位問題後才能採起合適的解決方案,而不是一內存溢出就查找相關參數加大。優化
概念
- 內存泄露:代碼中的某個對象本應該被虛擬機回收,但由於擁有GCRoot引用而沒有被回收。關於GCRoot概念,下一篇文章講解。
- 內存溢出: 虛擬機因爲堆中擁有太多不可回收對象沒有回收,致使沒法繼續建立新對象。
在分析問題以前先給你們講一講排查內存溢出問題的方法,內存溢出時JVM虛擬機會退出,那麼咱們怎麼知道JVM運行時的各類信息呢,Dump機制會幫助咱們,能夠經過加上VM參數-XX:+HeapDumpOnOutOfMemoryError讓虛擬機在出現內存溢出異常時生成dump文件,而後經過外部工具(做者使用的是VisualVM)來具體分析異常的緣由。
下面從如下幾個方面來配合代碼實戰演示內存溢出及如何定位:
/** VM Args: //這兩個參數保證了堆中的可分配內存固定爲20M -Xms20m -Xmx20m //文件生成的位置,做則生成在桌面的一個目錄 -XX:+HeapDumpOnOutOfMemoryError //文件生成的位置,做則生成在桌面的一個目錄 //文件生成的位置,做則生成在桌面的一個目錄 -XX:HeapDumpPath=/Users/zdy/Desktop/dump/ */
public class HeapOOM {
//建立一個內部類用於建立對象使用
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
//無限建立對象,在堆中
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,還告訴了你是哪一片區域出的問題:heap space
打開VisualVM工具導入對應的heapDump文件(如何使用請讀者自行查閱相關資料),相應的說明見圖:
分析dump文件後,咱們能夠知道,OOMObject這個類建立了810326個實例。因此它能不溢出嗎?接下來就在代碼裏找這個類在哪new的。排查問題。(咱們的樣例代碼就不用排查了,While循環太兇猛了)
老實說,在棧中出現異常(StackOverFlowError)的機率小到和去蘋果專賣店買手機,買回來後發現是Android系統的機率是同樣的。由於做者確實沒有在生產環境中遇到過,除了本身做死寫樣例代碼測試。先說一下異常出現的狀況,前面講到過,方法調用的過程就是方法幀進虛擬機棧和出虛擬機棧的過程,那麼有兩種狀況能夠致使StackOverFlowError,當一個方法幀(好比須要2M內存)進入到虛擬機棧(好比還剩下1M內存)的時候,就會報出StackOverFlow.這裏先說一個概念,棧深度:指目前虛擬機棧中沒有出棧的方法幀。虛擬機棧容量經過參數-Xss來控制,下面經過一段代碼,把棧容量人爲的調小一點,而後經過遞歸調用觸發異常。
/** * VM Args: //設置棧容量爲160K,默認1M -Xss160k */
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
//遞歸調用,觸發異常
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;
}
}
}複製代碼
結果以下:
stack length:751
Exception in thread "main" java.lang.StackOverflowError
能夠看到,遞歸調用了751次,棧容量不夠用了。
默認的棧容量在正常的方法調用時,棧深度能夠達到1000-2000深度,因此,通常的遞歸是能夠承受的住的。若是你的代碼出現了StackOverflowError,首先檢查代碼,而不是改參數。
這裏順帶提一下,不少人在作多線程開發時,當建立不少線程時,容易出現OOM(OutOfMemoryError),這時能夠經過具體狀況,減小最大堆容量,或者棧容量來解決問題,這是爲何呢。請看下面的公式:
線程數*(最大棧容量)+最大堆值+其餘內存(忽略不計或者通常不改動)=機器最大內存
當線程數比較多時,且沒法經過業務上削減線程數,那麼再不換機器的狀況下,你只能把最大棧容量設置小一點,或者把最大堆值設置小一點。
寫到這裏時,做者原本想寫一個無限建立動態代理對象的例子來演示方法區溢出,避開談論JDK7與JDK8的內存區域變動的過渡,但細想想,仍是把這一塊從始致終的說清楚。在上一篇文章中JVM系列之Java內存結構詳解講到方法區時提到,JDK7環境下方法區包括了(運行時常量池),其實這麼說是不許確的。由於從JDK7開始,HotSpot團隊就想到開始去"永久代",你們首先明確一個概念,方法區和"永久代"(PermGen space)是兩個概念,方法區是JVM虛擬機規範,任何虛擬機實現(J9等)都不能少這個區間,而"永久代"只是HotSpot對方法區的一個實現。爲了把知識點列清楚,我仍是才用列表的形式:
下面做者仍是經過一段代碼,來不停的建立Class對象,在JDK8中能夠看到metaSpace內存溢出:
/** 做者準備在JDK8下測試方法區,因此設置了Metaspace的大小爲固定的8M -XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m */
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
//無限建立動態代理,生成Class對象
enhancer.create();
}
}
static class OOMObject {
}
}複製代碼
在JDK8的環境下將報出異常:
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
這是由於在調用CGLib的建立代理時會生成動態代理類,即Class對象到Metaspace,因此While一下就出異常了。
提醒一下:雖然咱們平常叫"堆Dump",可是dump技術不只僅是對於"堆"區域纔有效,而是針對OOM的,也就是說無論什麼區域,凡是可以報出OOM錯誤的,均可以使用dump技術生成dump文件來分析。
在常常動態生成大量Class的應用中,須要特別注意類的回收情況,這類場景除了例子中的CGLib技術,常見的還有,大量JSP,反射,OSGI等。須要特別注意,當出現此類異常,應該知道是哪裏出了問題,而後看是調整參數,仍是在代碼層面優化。
直接內存異常很是少見,並且機制很特殊,由於直接內存不是直接向操做系統分配內存,並且經過計算獲得的內存不夠而手動拋出異常,因此當你發現你的dump文件很小,並且沒有明顯異常,只是告訴你OOM,你就能夠考慮下你代碼裏面是否是直接或者間接使用了NIO而致使直接內存溢出。
好了,"JVM系列之實戰內存溢出異常"到這裏就給你們介紹完了,Have a good day .歡迎留言指錯。
往期入口: