前言
Java虛擬機(Java Virtual Machine簡稱JVM)是運行所有Java程序的抽象計算機,是Java語言的運行環境,其主要任務為將字節碼裝載到內部,解釋/編譯為對應平臺上的機器指令執行。
Java虛擬機規范定義了一個抽象的——而非實際的——機器或處理器。這個規范描述了一個指令集,一組寄存器,一個堆棧,一個“垃圾堆”,和一個方法區。一旦一個Java虛擬機在給定的平臺上運行,任何Java程序(編譯之后的程序,稱作字節碼)都能在這個平臺上運行。Java虛擬機(JVM)可以以一次一條指令的方式來解釋字節碼(把它映射到實際的處理器指令),或者字節碼也可以由實際處理器中稱作just-in-time的編譯器進行進一步的編譯。
JVM的內存結構
我們可以把運行時數據區分為線程私有和共享數據區兩大類
-
線程私有的數據區包含 程序計數器、虛擬機棧、本地方法棧,即為本地區(native area)
-
線程共享的數據區包含Java堆、方法區,在方法區內有一個常量池。
本地區(native area)
程序計數器(PC Register)
記錄正在執行的虛擬機字節碼的地址。和計算機組成原理中提到的程序計數器PC概念類似,是線程私有的,用來記錄當前執行的字節碼位置。程序計數器會存儲當前線程正在執行的Java方法的JVM指令地址;或者,如果是在執行本地方法,則是未指定值(undefined)。
程序計數器是唯一一個不會發生OOM的區域。
虛擬機棧(JVM Stack)
也就是我們常常所說的棧。
方法執行的內存區,每個方法執行時會在虛擬機棧中創建棧幀,用于存儲局部變量表(局部變量表需要的內存在編譯期間就確定了所以在方法運行期間不會改變大小),操作數棧,動態鏈接,方法出口等信息。每一個方法從調用開始至執行完成的過程,就對應著棧幀在虛擬機棧中從入棧到出棧的過程。
這個區域有兩種異常情況:
-
StackOverflowError:線程請求的棧深度大于虛擬機所允許的深度
-
OutOfMemoryError:虛擬機棧擴展到無法申請足夠的內存時
本地方法棧(Native Method Stack)
本地方法棧則為虛擬機使用到的Native方法提供內存空間。
棧幀
每一個棧幀都包括了局部變量表,操作數棧,動態連接,方法返回地址和一些額外的附加信息。
在編譯代碼的時候,棧幀中需要多大的局部變量表,多深的操作數棧都已經完全確定了,并且寫入到了方法表的Code屬性中,因此一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決于具體虛擬機的實現。
一個線程中的方法調用鏈可能會很長,很多方法都同時處理執行狀態。對于執行引擎來講,活動線程中,只有虛擬機棧頂的棧幀才是有效的,稱為當前棧幀(Current Stack Frame),這個棧幀所關聯的方法稱為當前方法(Current Method)。執行引用所運行的所有字節碼指令都只針對當前棧幀進行操作。棧幀的概念結構如下圖所示:
方法區(method area)
方法區屬于是 JVM 運行時數據區域的一塊邏輯區域,是各個線程共享的內存區域。
當虛擬機要使用一個類時,它需要讀取并解析 Class 文件獲取相關信息,再將信息存入到方法區。方法區會存儲已被虛擬機加載的 類信息、字段信息、方法信息、常量、靜態變量、即時編譯器編譯后的代碼緩存等數據。
可以這樣理解,方法區存的是類的模版。比如說方法,其實就是行為,一個類的行為都是一致的,所以存在方法區;而變量,就是數據,每一個對象的數據都是不一樣的,所以存在堆里。
符號引用和直接引用
-
符號引用:字符串,能根據這個字符串定位到指定的數據,比如java/lang/StringBuilder,包含三種:類和接口的全限定名、字段的名稱和描述符、方法的名稱和描述符。
-
直接引用:內存地址
虛擬機棧的動態鏈接就是將符號引用(這些符號引用的集合就是常量池)轉換為直接引用(符號引用對應的具體信息,這些具體信息的集合就是運行時常量池,存在方法區中)的過程。
常量池
常量池表(Constant Pool Table)
我們寫的每一個Java類被編譯后,就會形成一份class文件(每個class文件都有一個class常量池)。 class文件中除了包含類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池(constant pool table),用于存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References)。
-
字面量,即通過字面我們就能知道其值的含義。包括:1.文本字符串 2.八種基本類型的值 3.被聲明為final的常量等;
-
符號引用包括:1.類和方法的全限定名 2.字段的名稱和描述符 3.方法的名稱和描述符。
常量池表會在類加載后存放到方法區的運行時常量池中。
運行時常量池(Runtime Constant Pool)
jvm在執行某個類的時候,必須經過加載、連接、初始化,而連接又包括驗證、準備、解析三個階段。而當類加載到內存中后,jvm就會將 class常量池 中的內容存放到 運行時常量池 中,由此可知,運行時常量池 也是每個類都有一個。
在上面我也說了,class常量池 中存的是字面量和符號引用,也就是說他們存的并不是對象的實例,而是對象的符號引用值。而經過解析(resolve)之后,也就是把符號引用替換為直接引用,解析的過程會去查詢 字符串常量池 ,以保證 運行時常量池所 引用的字符串與 字符串常量池 中所引用的是一致的。
字符串常量池(String Pool)
字符串常量池存的是 引用值,而不是具體的實例對象,具體的實例對象是在堆中開辟的一塊空間存放的。
是在類加載完成,經過驗證,準備階段之后 在 堆 中生成字符串對象實例,然后 將該字符串對象實例的 引用值 存到 String Pool 中