JVM自動記憶體管理-Java記憶體地區與記憶體溢出異常,jvm-java
摘要: JVM記憶體的劃分,導致記憶體溢出異常的可能地區。
1. JVM運行時記憶體地區
JVM在執行Java程式的過程中會把它所管理的記憶體劃分為以下幾個地區:
1.1 程式計數器
程式計數器是一塊較小的記憶體空間,在Java虛擬機器規範中是唯一一個未規定OutOfMemoryError的記憶體地區。
程式計數器可看作當前線程所執行位元組碼的行號指標。位元組碼解譯器工作時通過改變計數器的值來選取下一條待執行的位元組碼指令,分支、迴圈、跳轉、異常處理、線程恢複等基礎功能都需要依賴計數器來完成。
每個線程均有一個獨立的程式計數器。若線程執行的是一個Java方法,計數器記錄的值是正在執行虛擬機器位元組碼地址;若執行的是Native方法,計數器值為空白(Undefined)。
1.2 Java虛擬機器棧
Java虛擬機器棧也就是我們經常說的"棧"(其實這個說法不夠嚴謹<^_^>),它是線程私人的,生命週期與線程相同。
虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法執行時都會建立一個棧幀,用於儲存局部變數棧、運算元棧、動態連結、方法出口等資訊。每個方法從調用到執行完成的過程,都對應者一個棧幀在虛擬機器棧中入棧到出棧的過程。
局部變數表存放了編譯期可知的8種基礎資料型別 (Elementary Data Type)、對象引用(reference類型,指向對象起始地址的引用指標或代表對象的控制代碼或其他與此對象相關的位置)、returnAddress類型(指向一條位元組碼指令的地址)。
Java虛擬機器規範對這塊地區規定了兩種異常情況:
線程請求的棧深度大於虛擬機器所允許的深度,將拋出此異常;
若虛擬機器棧可以動態擴充,但在擴充時無法申請到足夠的記憶體,將拋出此異常;
1.3 本地方法棧
與虛擬機器棧類似,但它執行的Native方法服務。
1.4 Java堆
"GC"堆,虛擬機器啟動時建立,為所有線程共用的一塊地區。在Java虛擬機器規範描述中,所有對象執行個體和數組都要在堆上分配(隨著JIT編譯器的發展和逃逸技術的逐漸成熟,所有對象在堆上分配也變得不是這麼絕對)。
如果堆中剩餘記憶體不足以完成執行個體的分配,且無法再擴充,將拋出OutOfMemoryError。
1.5 方法區
"Non-Heap",儲存已被虛擬機器載入的類資訊、常量、靜態變數,即時編譯器編譯後的代碼等資料。
根據Java虛擬機器規範的規定,當方法區無法滿足記憶體配置需求時,將拋出OutOfMemoryError。
1.6 運行時常量池
方法區的一部分,在類載入後進入方法區時,存放編譯期產生的各種字面量和符號引用。
在運行期間,也可能將新的常量放入池中,如String常用的intern()方法。
當無法滿足記憶體配置需求時,將拋出OutOfMemoryError。
1.7 直接記憶體
這部分地區並不是虛擬機器運行時資料區的一部分,也非Java虛擬機器規範中定義的記憶體地區,但也被頻繁使用,亦可能拋出OutOfMemoryError。
直接記憶體主要為NIO(JDK1.4 加入的類)使用,通過儲存在Java堆中的DirectByteBuffer對象對這塊記憶體的引用進行操作。在某些場合能顯著提高效能,避免了Java堆和Native堆中來回複製資料。
2. HotSpot虛擬機器對象
討論Java堆中普通對象分配、布局和訪問的過程。
2.1 對象的建立
- 虛擬機器遇到一條new命令時,首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並檢查這個符號引用代表的類是否已被載入、解析和初始化過,若沒有,則先執行相應的類載入過程;
- 在類載入檢查通過後,虛擬機器將為新生對象分配記憶體:
若Java堆中的記憶體是絕對規整的,所有用過的和閒置記憶體都各放一邊,中間放著一個指標作為分界點的指標,那分配記憶體僅僅是將指標向空閑空間方向移動一段與對象大小相等的距離;
Java堆中的記憶體並不規整,此時虛擬機器必須維護一個列表,記錄哪些記憶體塊是可用的,在分配的時候從列表上找到一塊足夠大的空間劃分給對象執行個體,並更新列表上的記錄。
並發情況下,在堆上分配記憶體,可能出現記憶體配置的同步問題,解決方案有兩個,一個就是同步記憶體(CAS)分配動作;另一個就是採用TLAB(本地線程分配緩衝),即在Java堆中針對每個線程先預先分配一小塊記憶體。這樣當線程需要分配記憶體時就在自己的TLAB上進行,從而避免同步的開銷。但是當TLAB分配滿重新分配TLAB時仍需要同步。在記憶體配置完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值(不包括對象頭)。
- 虛擬機器將要對對象進行必要的設定,如這個對象是哪個類的執行個體,如何找到類的中繼資料資訊、對象的hashCode和GC分代年齡等資訊。這些資訊存放在對象的對象頭中。
最後執行init方法,從而產生真正可用的對象。
2.2 對象的記憶體布局
在HotSpot虛擬機器中,對象在記憶體的儲存的布局可分為3塊地區:對象頭、執行個體資料、對齊填充。
儲存物件自身的運行時資料,如hashCode、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳記等;
對象指向它的類別中繼資料的指標,虛擬機器通過這個指標確定這個對象是哪個類的執行個體。
程式碼中所定義的各種類型的欄位內容,包括從父類繼承下來的。
並不是必然存在,僅僅起著預留位置作用,以滿足HotSpot VM 自動記憶體管理系統對對象起始地址必須是8位元組的整數倍要求。
2.3 對象的訪問定位
Java程式通過棧上的reference資料來操作堆上的具體對象。主流的訪問方式:
* 控制代碼
Java堆中將劃分一塊記憶體用作控制代碼池,reference中儲存的就是對象的控制代碼地址,而控制代碼包含了對象執行個體資料與類型資料各自的具體地址資訊。
優點:當對象移動時,只需要改變控制代碼的指標即可,而相應的reference則不需要做變動;
reference儲存的是對象地址,通過reference就能訪問資料。
優點:訪問速度快,相對控制代碼的方式而言少了一次指標定位的開銷。HotSpot VM使用的是此種方式。
參考文獻
深入理解Java虛擬機器:JVM進階特性與最佳實務(第2版)