JVM的主要結構如下圖所示,圖片引用自舒の隨想日記。
方法區和堆由所有線程共享,其他區域都是線程私有的
程序計數器(Program Counter Register)
類似于PC寄存器,是一塊較小的內存區域,通過程序計數器中的值尋找要執行的指令的字節碼,由于多線程間切換時要恢復每一個線程的當前執行位置,所以每個線程都有自己的程序計算器。這一個區域不會有OutOfMemeryError。當執行Java方法時,這里存儲的執行的指令的地址,如果執行的是本地方法,這里的值是Undefined。
虛擬機棧(Java Stack)
虛擬機棧也是線程私有的,每創建一個線程,虛擬機就會為這個線程創建一個虛擬機棧,虛擬機棧表示Java方法執行的內存模型,每調用一個方法,就會生成一個棧幀(Stack Frame)用于存儲方法的本地變量表、操作棧、方法出口等信息,當這個方法執行完后,就會彈出相應的棧幀。
如果請求的棧的深度過大,虛擬機可能會拋出StackOverflowError異常,如果虛擬機的實現中允許虛擬機棧動態擴展,當內存不足以擴展棧的時候,會拋出OutOfMemoryError異常。
棧幀(Stack Frame)
棧幀分為三部分:局部變量區(Local Variables)、操作數棧(Operand Stack)和幀數據區(Frame Data)。
局部變量區(Loca Variables)
局部變量區被組織一個一個從0開始的字數組,byte、short、char在存儲前被轉換為int,boolean也被轉換為int,0表示false,非0表示true,long和double占據兩個字長。
操作數棧(Operand Stack)
操作數棧也被組織為一個字數組,但不同于局部變量區,它不是通過數組下標訪問的,而是能過棧的Push和Pop操作,前一個操作Push進的數據可以被下一個操作Pop出來使用。
幀數據區(Frame Data)
這部分的作用主要有三部分:
- 常量池中數據的解析
- 方法執行完后處理方法返回,恢復調用方現場
- 方法執行過程中拋出異常時的異常處理,存儲有一個異常表,當出現異常時虛擬機查找相應的異常表看是否有對應的Catch語句,如果沒有就拋出異常終止這個方法調用
本地方法棧(Native Method Stack)
與虛擬機棧類似,只是是執行本地方法時使用的。
方法區(Method Area)
用于存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯后的代碼等信息。
方法區是線程間共享的,當兩個線程同時需要加載一個類型時,只有一個類會請求ClassLoader加載,另一個線程會等待。
對于每一個加載的類型,會在方法區中保存以下信息:
- 類及其父類的全限定名(java.lang.Object沒有父類)
- 類的類型(Class or Interface)
- 訪問修飾符(public, abstract, final)
- 實現的接口的全限定名的列表
- 常量池
- 字段信息
- 方法信息
- 除常量外的靜態變量
- ClassLoader引用
- Class引用
對于每一個字段,會在方法區中保存以下信息(字段聲明順序也會保存):
- 字段名
- 字段的類型
- 字段的修飾符(public, private , protected, static, final, volatile, transient)
對于每一個方法,會在方法區中保存以下信息(方法聲明順序也會保存):
- 方法名
- 方法返回類型(或void)
- 參數信息
- 方法修飾符(public, private, protected , static, final, synchronized, native, abstract)
如果方法不是抽象方法并不是本地方法(Native Method),還會保存以下信息:
- 方法的字節碼
- 本地變量表及操作數棧的大小
- 異常表
虛擬機需要存儲一些數據,用來快速地訪問一個類對象中的方法,一般實現為一個方法表。
方法區中還有一部分是運行時常量池,主要用來存儲編譯時生成的字面量和符號引用,常量也可以在運行時產生,如String的intern方法。
方法區中也可能存在GC,但虛擬機規范對此不做要求,主要是回收一些常量和卸載一些不用的類型信息,不過要卸載一個類的條件很難達到,而且些處GC其實也回收不了多少內存。
堆(Heap)
虛擬機中用于存放對象與數組實例的地方,垃圾回收的主要區域就是這里(還可能有方法區)。
如果垃圾收集算法采用按代收集(目前大都是這樣),這部分還可以細分為新生代和老年代。
新生代又可能分為Eden區,From Survivor區和To Survivor區,主要是為了垃圾回收。所有的線程共享Java堆,在這里還可以劃分線程私有的緩沖區(Thread Local Allocation Buffer,TLAB)。
Java堆只要求邏輯上是連續的,在物理空間上可以不連續。
直接內存
JDK1.4中引用了NIO,并引用了Channel與Buffer,可以使用Native函數庫直接分配堆外內存,并通過一個存儲在Java堆里面的DirectByteBuffer對象作為這塊內存的引用進行操作。
對象訪問
當新建一個對象時,會在堆中為這個對象分配內存,并在棧中有一個對這個對象引用,除此之外,在Java堆中還要能通過這個對象找到它的類型信息(對象類型,父類,實現的接口,包含的字段與方法等)。
Reference在Java虛擬機中定義為指向對象的引用,但沒有定義這個Reference應該有怎么實現。
一種實現是Reference直接存儲對象在堆內的地址,對象的類型信息可以在對象在堆中的內存布局中存儲,如存儲在對象內存的開頭等。
另一種實現是Reference指向一個句柄表中的一個位置,句柄中保存了對象的實際位置及它對應的類型信息。
使用句柄的好處是當在內存中移動對象的位置時,只需要更新句柄表中的內容,不需要改變引用值,但會多一次內存訪問開銷,直接引用的優缺點與此相反。
參考資料:
- 深入Java虛擬機
- 深入理解Java虛擬機-JVM高級特性與最佳實踐
- 淺析Java虛擬機結構與機制
文章列表