JVM學習筆記(一)——內存模型
對於Java程序員來說,他們相比C++程序員最幸福的一點就是不用自己管理內存,內存的分配和回收都由虛擬機完成。然而,正是由於該原因,一旦虛擬機內存管理出現問題,比如出現內存泄漏或溢出,排查起來將是非常困難的。所以盡管不用親自動手管理內存,但是了解虛擬機的內存管理機製還是很有必要的。
- 運行時數據區域
首先來看下Java虛擬機在運行時的數據區域劃分,Java虛擬機在Java程序運行時會將內存區域劃分成若幹個不同的區域,各自負責不同的職責。
1.1 程序計數器
程序計數器(PC)的概念比較簡單,記錄當前程序所執行的指令行號,需要注意的是每個線程各自擁有一個程序計數器,各自記錄各自線程的指令執行地址。如果當前線程正在執行一個Java方法,則程序計數器記錄的是當前指令地址;如果當前線程執行的是Native方法則程序計數器值為0。
1.2 虛擬機棧
Java虛擬機棧也是線程私有的,用於記錄Java方法的內存模型,虛擬機棧可以看做一個容器用於盛放一個個棧幀,每一個方法在開始執行之前都會創建一個棧幀用於存儲方法執行期間的局部變量、操作數、動態鏈接、方法出口等,所以一個方法從調用到返回的過程也可以看成是一個棧幀從入棧到出棧的過程。
當一個方法中調用另一個方法時,Java虛擬機會將被調用方法對應的棧幀壓入虛擬機棧頂,當該方法執行完成後該方法出棧,調用方法重新回到棧頂。如果方法調用深度超過了虛擬機規定的最大深度時,將會拋出StackOverflowError。
1.3 本地方法棧
本地方法棧作用和虛擬機棧類似,隻是針對的是Native方法而已。
1.4 堆
堆是Java運行時最大的一塊空間,Java程序運行時幾乎所有創建出來的對象都是在堆區分配的,存儲實例數據是堆區唯一的職責。由於用戶創建對象幾乎全部在堆上,所以垃圾收集器GC的主要工作區域。另外堆區是線程共享的,各個線程均可訪問堆區中的數據,所以在多線程環境下訪問堆區數據需要進行線程同步。
1.5 方法區
方法區同堆區一樣,也是線程共享的。主要用於存放被虛擬機加載的類信息、常量、靜態變量以及及時編譯器編譯的代碼等數據。
- 實例對象內存模型
看完了虛擬機的整體內存布局,接下來看下實例對象的內存模型。實例對象均存儲在堆上,盡管每個實例對象的所占用空間大小不一,但其內存布局上遵守同樣的標準,具體分為以下幾部分:
- 對象頭
- 實例數據
- 對齊填充
2.1 對象頭
對象頭主要分為兩部分:
- 運行時數據
- 類型指針
運行時數據主要包括:HashCode、GC分代年齡、鎖狀態標誌、線程持有的鎖等等;類型指針是對象執行其類元數據的指針,虛擬機通過類型指針確定該對象具體是哪個類的對象。
2.2 實例數據
實例數據即為對象實例真正存儲的真實有效的數據,即類定義中相關字段的內容,這其中既包括從父類繼承的也包括子類定義的。
2.3 對齊填充
對齊填充不是必然存在的,其作用僅僅是為讓實例數據部分對齊。如虛擬機要求事項起始地址必須是8字節的整倍數,由於對象頭所占空間本身就是8字節整倍數,如果實例數據部分不是8字節整倍數就通過對齊填充來補全。
2.4 對象訪問定位
Java中通過棧上的reference類型數據在訪問和操作對象,具體reference數據如何訪問對象有兩種方式:
- 通過句柄訪問對象:Java堆中會專門空出一塊空間作為句柄池,reference保存的即是對應對象的句柄地址。句柄的具體內容包括對象實例數據和對象類型數據
- 通過指針訪問對象:reference保存的是對象實例數據的地址,而對象類型數據包含在對象實例數據中
兩種訪問方式各有優劣,具體來說:
- 句柄訪問:靈活性更強,如果對象數據地址改變隻需修改句柄即可,無需修改reference
- 指針訪問:效率更高,訪問對象實例數據隻需要一次訪問即可
- 對象創建
對象創建主要涉及四個步驟:
- 類加載
- 內存分配
- 對象頭設置
- 初始化
3.1 類加載
如果對象所屬類並未被加載時,虛擬機需要首先執行對應類的加載過程,其中包括類的加載、校驗、準備、解析、初始化等過程,具體細節將在後續博文中詳細展開。
3.2 內存分配
相應類加載完成後,實例對象的所占空間大小亦完全確定了,所以可以在堆上對齊進行空間分配。
3.3 對象頭設置
接下來需要設置實例對象的對象頭信息,包括類元數據、HashCode、GC分代等。
3.4 對象初始化
執行對象方法,完成類對象的初始化操作。
最後更新:2017-07-11 01:02:45