摘要:前言最近在讀周志明老師的深入理解虛擬機感覺一下?lián)Q了一個角度來看待代碼,有必要整理一些內(nèi)容,更清楚實際的流程,這一篇就記錄下內(nèi)存區(qū)域與相關的一些內(nèi)存溢出的異常。除了這些以外,直接內(nèi)存的不合理分配也會影響到虛擬機動態(tài)擴展內(nèi)存時出現(xiàn)內(nèi)存溢出。
前言
最近在讀周志明老師的《深入理解Java虛擬機》,感覺一下?lián)Q了一個角度來看待Java代碼,有必要整理一些內(nèi)容,更清楚實際的流程,這一篇就記錄下Java內(nèi)存區(qū)域與相關的一些內(nèi)存溢出的異常。
內(nèi)存區(qū)域Java虛擬機在執(zhí)行Java程序的過程會把它管理的內(nèi)存劃分為各個不同的區(qū)域,這些區(qū)域都有著各自的生命周期,總的來說Java虛擬機管理的內(nèi)存將會包括一下的數(shù)據(jù)區(qū)域
圖中可以很清晰的看出區(qū)域里面各個實體的關系,然后簡單介紹一3下各個實體。
1.程序計數(shù)器線程私有,可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器,字節(jié)碼解釋器的工作就是通過改變計數(shù)器的值來進行后續(xù)的操作。
2.虛擬機棧線程私有,描述的是Java方法執(zhí)行的內(nèi)存模型,每個方法在執(zhí)行的同時會創(chuàng)建一個棧幀用于儲存局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。
3.本地方法棧與虛擬機棧發(fā)揮的作用類似,但虛擬機棧主要是為執(zhí)行Java方法服務,而本地方法棧則為虛擬機使用到的native方法服務。
4.Java堆是Java虛擬機所管理的內(nèi)存中最大的一塊,也是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建,此區(qū)域唯一目的是存放對象實例。
5.方法區(qū)也是各個線程共享,用于儲存已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù),為堆的一個邏輯部分。其中的運行時常量池相對于Class文件常量池而言具備動態(tài)性,運行期間也可將新的常量放入池中。
除了這些以外,直接內(nèi)存的不合理分配也會影響到Java虛擬機動態(tài)擴展內(nèi)存時出現(xiàn)內(nèi)存溢出。
Java對象的創(chuàng)建我們無時無刻都在使用著,這里從虛擬機層面來看待對象的創(chuàng)建。
對象的創(chuàng)建1.當虛擬機收到一條new指令時,首先去檢查這個指令的參數(shù)能否在常量池中定位到對應的符號引用,并且檢查符號引用代表的類是否加載過、解析和初始化過,沒有則進行對應的類加載過程。
2.在類檢查通過后,虛擬機為新生對象從Java堆中分配內(nèi)存。而內(nèi)存的分配又有兩種方式。一種是指針碰撞,就是把空閑和正在使用的內(nèi)存中間放一個指針隔開,所以這種方式實際就是把指針進行移動,當內(nèi)存出現(xiàn)相互交錯時。該方式自然就行不通了;另一種方式是空閑列表,由虛擬機維護一個列表,分配內(nèi)存后就更新這個列表的記錄。至于使用哪種方式就要看是基于何種垃圾回收器而言。除了怎么劃分可用空間之外,還需要考慮虛擬機分配內(nèi)存的頻率,分配頻率就會涉及到并發(fā)中的一些問題了。JVM采用CAS(比較并交換)方式進行失敗重試保證操作的原子性;另一種是每個線程在Java堆中預先分配一小塊內(nèi)存,也就是本地線程分配緩沖(TLAB)
3.內(nèi)存分配完成后,虛擬機將分配到的內(nèi)存空間都初始化為零值,使用TLAB,則可以提前到分配時進行。
4.虛擬機對對象進行必要的設置,也就是把該對象相關的信息存儲在對象頭之中,這些工作都完成后,一個新的對象已經(jīng)產(chǎn)生了,接下來就是對象的初始化了。
對象的訪問定位對象的訪問目前主流的方式有句柄和直接指針兩種。
1.使用句柄訪問,Java堆會劃分出一塊內(nèi)存作為句柄池,對象的引用中存儲的就是其句柄的地址,句柄包含了對象實例數(shù)據(jù)和類型數(shù)據(jù)的具體地址信息。
2.使用直接指針,堆就要考慮如何放置訪問類型數(shù)據(jù)相關的信息,引用中存儲的直接就是對象地址。
很顯然,因為是直接指向地址,所以直接指針的方式更加快,但對象訪問在Java中太過頻繁,積少成多后也會造成較高的執(zhí)行成本。
public class HeapOOM { static class OOMObject { } public static void main(String[] args) { Listlist = new ArrayList<>(); while (true) { list.add(new OOMObject()); } } }
這里可以設置下虛擬機相關的參數(shù),
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=82.虛擬機和本地方法棧溢出
public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) { JavaVMStackSOF stackSOF = new JavaVMStackSOF(); try { stackSOF.stackLeak(); } catch (Throwable e) { System.out.println("Stack length: "+stackSOF.stackLength ); throw e; } } }3.創(chuàng)建線程導致的內(nèi)存溢出
public class JavaVMStackOOM { private void doStop() { while (true) { } } public void stackLeakByThread() { while (true) { Thread thread = new Thread(()->{ doStop(); }); thread.start(); } } public static void main(String[] args) { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } }
這里的話,在Windows平臺的虛擬機中,Java的線程是映射到操作系統(tǒng)的內(nèi)核線程上,這里可能導致機器假死。
4.方法和運行時常量池溢出public class RuntimeConstantPoolOOM { public static void main(String[] args) { List5.本機直接內(nèi)存溢出list = new ArrayList<>(); int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } } }
public class DirectMemoryOOM { public static final int _1MB = 1024 * 1024; public static void main(String[] args) { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); try { Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB); } } catch (IllegalAccessException e) { e.printStackTrace(); } } }總結
了解了這些不得不佩服設計者,一個看似簡單的東西底層要解決的問題、處理的細節(jié)也是非常多的,從Java虛擬機層面看到了不一樣的東西,也是非常有意思的,理解底層實現(xiàn)的原理有助于寫成更健壯的代碼,更快速的debug,就到這里了。
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://www.ezyhdfw.cn/yun/71073.html
摘要:內(nèi)存溢出的情況就是從類加載器加載的時候開始出現(xiàn)的,內(nèi)存溢出分為兩大類和。以下舉出個內(nèi)存溢出的情況,并通過實例代碼的方式講解了是如何出現(xiàn)內(nèi)存溢出的。內(nèi)存溢出問題描述元空間的溢出,系統(tǒng)會拋出。這樣就會造成棧的內(nèi)存溢出。 導言: 對于java程序員來說,在虛擬機自動內(nèi)存管理機制的幫助下,不需要自己實現(xiàn)釋放內(nèi)存,不容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出的問題,由虛擬機管理內(nèi)存這一切看起來非常美好,但是一旦...
摘要:小結程序計數(shù)器和虛擬機棧是線程私有的,而堆和方法區(qū)是線程共享的除了虛擬機運行時內(nèi)存,在中使用類可以直接操作本機內(nèi)存。 Java的內(nèi)存區(qū)域 Java虛擬機在執(zhí)行Java程序中會把它所管理的內(nèi)存劃分為若干個數(shù)據(jù)區(qū)域,這些區(qū)域有各自的用途,以及生命周期,有些依賴虛擬機進程啟動而存在,有些依賴用戶線程的啟動和結束而建立和銷毀 運行時內(nèi)存 showImg(/img/bVqUpc); 程序計數(shù)器(...
摘要:直接通過可以造成本機內(nèi)存溢出。小節(jié)內(nèi)存區(qū)域描述異常程序計數(shù)器略略略虛擬機棧存放編譯器可知的各種基本類型,對象引用和類型每個線程的棧大小堆存放對象實例最大值最小值運行時常亮池存放編譯期生成的字面量和符號引用,運行期也能放入常量池。 堆溢出 Java堆用于存儲對象實例,只要不斷地創(chuàng)建對象,并且保證GC Roots到對象之間有可達路徑避免垃圾回收,當?shù)竭_最大堆的容量限制后就會產(chǎn)生Java.l...
摘要:一內(nèi)存區(qū)域虛擬機在運行時,會把內(nèi)存空間分為若干個區(qū)域,根據(jù)虛擬機規(guī)范版的規(guī)定,虛擬機所管理的內(nèi)存區(qū)域分為如下部分方法區(qū)堆內(nèi)存虛擬機棧本地方法棧程序計數(shù)器。前言 在JVM的管控下,Java程序員不再需要管理內(nèi)存的分配與釋放,這和在C和C++的世界是完全不一樣的。所以,在JVM的幫助下,Java程序員很少會關注內(nèi)存泄露和內(nèi)存溢出的問題。但是,一旦JVM發(fā)生這些情況的時候,如果你不清楚JVM內(nèi)存的...
閱讀 3423·2023-04-26 00:57
閱讀 675·2021-10-08 10:05
閱讀 1422·2021-09-08 09:36
閱讀 4275·2021-08-12 13:31
閱讀 2628·2019-08-30 15:55
閱讀 2278·2019-08-30 15:55
閱讀 1085·2019-08-30 15:55
閱讀 2747·2019-08-29 13:17