前言簡介
class文件是源代碼經過編譯后的一種平台中立的格式
里面包含了虛擬機運行所需要的所有信息,相當於 JVM的機器語言
JVM全稱是Java Virtual Machine ,既然是虛擬機,他終歸要運行在物理機上
在操作系統中體現出來的也就是一個進程
操作系統會給他分配資源,割一塊內存作為他的地盤
class文件是靜態的,想要運行程序,JVM需要將class文件中的信息加載到加載到他的地盤
然后處理他可以處理的數據類型的數據
JVM將這塊內存按照功能進行了更細的划分,不過終究是一個規范,虛擬機的廠商在實現的時候仍舊有很大的自由度
接下來將會從兩個方面 虛擬機可以處理的數據類型 以及 運行時的數據區的內存模型
對虛擬機進行簡單的介紹
數據類型
數據類型分類
|
虛擬機可以處理的數據類型分為:基本類型和引用類型兩類
所以對於值,也就存在基本類型值 和 引用值 兩種類型的值
|
|
基本類型又分為 數值類型/boolean/returnAddress 三種
數值類型又分為整數類型和浮點數類型
整數類型(byte short int long char) 與浮點數(float double) 與java語言中的值域在任何地方都是一致的,比如 取值范圍表示含義
boolean編譯后使用Java虛擬機中的int 數據類型代替,不過Java虛擬機支持boolean類型的數組,0表示false 1表示true
returnAddress 在Java語言中並不存在相應的類型 也就是程序員不能使用這個類型 ,而且也無法在程序運行期間更改
|
| 引用類型分為三種 類類型 接口類型 數組類型 值都是動態創建對象的引用 類類型的值是對類實例的引用 數組類型的值是對數組對象的引用 接口類型的值 是對實現了該接口的某個類實例的引用 另外還有一個特殊的引用null |
取值范圍
| byte | 8位 有符號 二進制補碼整數 默認值零(-2^7到2^7-1 包括兩端的值在內) |
| short | 16位 有符號 二進制補碼整數 默認值零(-2^15到2^15-1 包括兩端的值在內) |
| int |
32位 有符號 二進制補碼整數 默認值零(-2^31到2^31-1 包括兩端的值在內)
|
| long |
64位 有符號 二進制補碼整數 默認值零(-2^64到2^64-1 包括兩端的值在內)
|
| char | 16位 無符號 Unicode字符 默認值為null的碼點 '\u0000' (0 到2^16-1 包括兩端的值在內) |
| float | 32位 IEEE754標准單精度浮點數 默認值正數0 |
| double | 64位 IEEE754標准雙精度浮點數 默認值正數0 |
| returnAddress | 同一方法中某操作碼的地址 |
| reference | 堆中堆某對象的引用,或者是null |
內存結構
內存結構組成部分
上面說過,程序運行,必然需要裝載數據到內存
class文件會經由classLoader加載到JVM的運行時數據區域
JVM的內存結構為下圖右側部分
從圖中可以看得出來
大致分為 方法區/堆/程序計數器/虛擬機棧/本地方法棧 五部分
接下來逐個進行介紹
|
ps:在抽象一點,邏輯上來說其實可以理解為堆/棧/程序計數器 三類
程序的運行 , 需要數據還需要方法,還需要說明從哪個指令位置開始執行
程序計數器就是指向要執行的指令地址,標志從哪個位置開始執行
棧是方法調用概念的具體化數據結構,描述了怎么執行
堆用於保存程序運行需要用到的數據對象等,描述了 執行什么,操作什么
|
內存結構各部分詳情
一個運行時的java虛擬機實例就是負責一個java程序的運行
啟動一個Java程序一個虛擬機實例也就誕生,當這個Java程序關閉,這個虛擬機實例就銷毀
每個Java程序都運行於他自己的Java虛擬機實例中
一個虛擬機實例中,堆和方法區是這個Java程序所有線程共享的
Java虛擬機棧和本地方法棧和程序計數器 是線程隔離獨有的
下面所有說到的都是基於一個Java程序內的場景
方法區
|
方法區是可供各個線程共享的運行時內存區域,存儲了每一個類的結構信息,如:
運行時常量池/字段和方法數據/構造函數和普通方法的字節碼內容/類實例接口初始化時用到的特殊方法
方法區在虛擬機啟動的時候創建
方法區也可以被垃圾收集
方法區大小不必是固定的 可以根據需要動態擴展
方法區空間也不必是連續的
具體存儲的信息包括:
|
|
類型信息
類的全限定名
類型的直接超類全限定名
類型 類還是接口
訪問修飾符
直接超接口的全限定名
|
| 字段信息 字段名 字段類型 字段的修飾符 |
| 方法信息 方法名 方法的返回類型 方法的參數數量和類型 方法的修飾符 方法的字節碼(有方法體的) 操作數棧和該方法棧幀中的局部變量表 的大小(其實也還是class文件屬性表的內容 靜態的) |
| 常量池--下面的運行時常量池區域 |
| 除了常量以外的所有類變量 類變量是所有類實例共享的,即使沒有任何類實例,他也可以被訪問,這些變量僅僅和類有關 所以 類變量總是作為類型信息的一部分存儲在方法區 除了在類中聲明的編譯時常量外,虛擬機使用某個類之前 必須在方法區中為這些類分配空間 編譯時常量指的是final聲明以及用編譯時已知的值初始化的類變量 這種和一般的類變量還不一樣,每個使用編譯時常量的類型,都會復制他的所有常量到自己的常量池中 或者嵌入到他的字節碼流中 說白了對於這種值不變的,直接復制過去 |
| 類ClassLoader的引用/Class類的引用 每個類被裝載后都必須跟蹤他是由哪個類加載器加載的 對於每個被裝載的類型,不管是類還是接口,虛擬機都會相應的為他創建一個java.lang.Class類的實例 而且虛擬機還必須以某種方式把這個實例和存儲在方法區中的類型數據關聯起來 |
運行時常量池
|
運行時常量池屬於方法區的一部分
class文件中每一個類或者接口的常量池表 constant_pool table 運行時的表示形式
只需要記住與class文件中的constant_pool相對應即可理解所包含的內容
包括了若干種不同的常量
從編譯器可知的數值字面量到必須在運行期解析后才能獲得的方法或字段引用
運行時常量在Java虛擬機的方法區分配 加載類或者接口到虛擬機后,就創建對應的運行時常量池
|
總結:
所有的類型信息,靜態數據信息,都加載到方法區中
另外類加載器以及當前Class對象這種運行時必須的信息,也被保存在方法區
Java堆
|
一個java程序獨占一個虛擬機實例,也就是每個java程序一個獨立的堆空間
但是對於同一個java程序 堆是各個線程共享的運行時內存區域
是所有類實例和數組對象分配內存的區域
Java堆在虛擬機啟動時就被創建了
存儲了被自動內存管理系統 也就是GC( garbage Collector) 所管理的各種對象
這些受管理的對象不需要也也不能顯式的銷毀
之所以這么說是因為有分配新對象的指令,卻沒有釋放內存的指令,所以就不能顯式的銷毀
堆是垃圾收集器工作的主要區域
堆空間不必連續也可以動態擴展或者收縮
對象的內部表示形式,規范並沒有規定 實現者可以按需發揮
|
Java虛擬機棧
|
java虛擬機棧,是對方法調用這一抽象概念的具體化描述,方法執行的內存模型
啟動一個新線程 Java虛擬機就會為他分配一個Java棧,用於保存棧幀
虛擬機只會直接對Java棧執行兩種操作 以棧幀為單位的出棧或者入棧
每個方法在執行的同時都會創建一個棧幀 棧幀用於存儲局部變量表 操作數棧 動態鏈接 方法出口等信息
每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機中從入棧到出棧的過程
棧上所有的數據都是線程私有的任何線程都不能訪問另一個線程的棧數據
也就是說,完全無需考慮多線程情況下數據的訪問同步問題
當一個線程調用另一個方法時,方法的局部變量保存在調用線程的Java虛擬機棧的棧幀中
只有一個線程總是能訪問那些局部變量即調用方法的線程
|
棧幀
|
三部分組成: 局部變量表 操作數棧 以及棧幀數據區
當虛擬機調用一個方法時,從對應類的類型信息中得到此方法的局部變量表和操作數棧的大小(code 屬性)
並以此分配棧幀內存,然后壓入Java棧中
棧幀隨着方法的調用而創建隨着方法結束而銷毀,無論方法正常完成還是異常完成都算作方法結束
局部變量表 長度由編譯期決定,通過方法code屬性提供 除了long和double 使用兩個局部變量外,其余類型均為一個
保存了對應方法的參數和局部變量
操作數棧 后進先出,操作數棧最大深度編譯期決定通過code屬性保存提供 每個位置可以保存一個java虛擬機中定義的任意數據類型的值包括long double
操作數棧作為虛擬機的工作區,大多數指令都要從這里彈出數據執行計算然后把結果壓回操作數棧
棧幀數據區 除了局部變量和操作數棧外,還需要一些其他的數據,比如 常量池的入口信息,每當虛擬機要執行某個需要用到常量池數據的指令時
都會通過棧幀數據區中指向常量池的指針來訪問他
|
本地方法棧
|
本地方法棧並不是虛擬機明確定義的,是可選的
Java虛擬機實現可能會使用到傳統的棧(通常稱之為C stack) 來支持native方法(指 用Java以外的其他語言編寫的方法)
這個棧就是本地方法棧 當Java虛擬機使用其他語言比如C語言,來實現指令集解釋器的時候,也可以使用本地方法棧
如果Java虛擬機本身不支持native方法,或是本身不依賴傳統棧,那么可以不提供本地方法棧
如果支持本地方法棧 那這個棧一般會在創建線程的時候按線程分配
|
程序計數器
|
程序計數器 又叫做 PC寄存器 PC為program counter
不管稱呼如何,其本意是保存當前正在執行的指令的地址,
此處,我們可以看做是當前線程所執行的字節碼的行號指示器
也就是程序的運行完全依賴PC寄存器,需要依靠他獲取下一條需要執行的字節碼指令
JVM的多線程時通過線程輪流切換並分配處理器執行時間片的方式實現的,在任何一個確定的時刻
一個處理器(一個內核) 都只能執行一條線程中的指令,為了線程切換后能恢復到正確的位置
所以每個線程都需要一個獨立的程序計數器,所以程序計數器是線程私有的 線程啟動時創建
如果執行的是Java方法,值為正在執行的虛擬機的字節碼指令地址
如果是Native方法,計數器值為空 Undefined ,此區域 沒有OOM
|
直接內存
|
直接內存並不是虛擬機運行時的數據區,也不是Java虛擬機規范中定義的內存區
但是這部分內存也被頻繁的調用,也可能導致OOM
是引入NIO后,引入的一種基於通道與緩沖區的IO方式
可以使用native 函數庫直接分配堆外內存,然后通過Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作
能在一些場景中顯著提高性能
既然不屬於java堆,自然不受制於Java堆大小的限制,但是,必須運行於物理機
自然受制於本機總內存大小
|
總結
JVM運行時的內存結構,就是為了執行字節碼文件,而將class文件中的信息加載到內存中的一個邏輯映射
class文件是源代碼的靜態抽象的數據結構描述
運行時內存結構是對於class文件的執行行為的結構描述
以上所有的要求說明都是屬於規范上的並不要求所有的實現與規范中定義的抽象元素完全的對應起來
抽象的內部組件和行為的描述,僅僅是定義Java虛擬機所應該呈現出來的外部行為
也就是說,一個具體的虛擬機實現,可能與我們說過的規范相同,也可能與規范有出入
但是只要他的外部行為是一致的,正確識別class文件,遵守class文件中包含的Java代碼的語義,能夠按照規定所需要呈現出來的行為結果
執行字節碼文件即可
至於方法區到底應該如何分配空間,對象的內部表現形式如何,垃圾收集器如何運作,如何加載類都是由設計者來決定實現的.
|
舉一個淺顯的例子
你去超市購物,為了方便攜帶,你可能會按照他們的形狀或者類別組織放到購物袋里面
比如 生鮮放到一個袋子,零食放到一個袋子,或者放置稍大商品的購物袋里面的縫隙處,放置一些小的商品
這是屬於所有商品的靜態描述組織
回到家需要做飯,可能你會把魚拿出來放到盤子里,可能你會把青菜放到水槽中浸泡清洗,然后你可能會准備作料,洗鍋准備做菜等等
一切都按照你下廚的習慣來放置食材以及步驟進行做菜
這就是屬於動態執行行為的結構描述
我們的內存結構 程序計數器 堆 棧 就是對於代碼執行行為過程的一種描述
可以理解你想要先做那道菜? 程序計數器((如果把想要做的菜都列一個清單,程序計數器就是從什么位置開始做,就是先做哪道菜)
都有哪些食材? 台面上有青菜 魚 豆腐... 這都是存放在堆中
具體的怎么做? 紅燒還是清蒸?這些具體的行為封裝在虛擬機棧的棧幀中 每次做一道菜就是入棧,做好了刷鍋就是出棧
而每道菜所需要的調味料和配菜可能是獨有的,不能亂放,這些就相當於棧幀中的局部變量和操作數棧
|




