當位元組碼通過前面的:類的載入-> 驗證 -> 準備 -> 解析 -> 初始化
這幾個階段完成後,就會用到執行引擎對類進行使用,同時執行引擎將會使用到我們執行時數據區
記憶體是非常重要的系統資源,是硬碟和CPU的中間倉庫及橋樑,承載着操作系統和應用程式的實時執行 JVM 記憶體佈局規定了Java在執行過程中記憶體申請、分配、管理的策略,保證了 JVM 的高效穩定執行。
不同的JVM對於記憶體的劃分方式和管理機制 機製存在着部分差異。結合JVM虛擬機器規範,來探討一下經典的JVM記憶體佈局。
我們通過磁碟或者網路 IO 得到的數據,都需要先載入到記憶體中,然後CPU從記憶體中獲取數據進行讀取,也就是說記憶體充當了CPU和磁碟之間的橋樑
run()
方法。run()
方法能夠正常執行完,以及對產生的異常能夠進行處理,就算是正常的執行完畢;否則就終止 Java執行緒JVM 系統執行緒
public static void main(String[])
的main
執行緒以及所有這個main
執行緒自己建立的執行緒。stop-the-world
"的垃圾收集,執行緒棧收集,執行緒掛起以及偏向鎖復原。Program Counter Register
)中,Register的命名源於CPU的暫存器,暫存器儲存指令相關的現場資訊。作用
介紹
native
方法,則是未指定值(undefined
)。OutOfMemoryError
情況的區域,也沒有GC垃圾回收機制 機製作用。即:PC暫存器既沒有OOM,也沒有GC。舉例演示
public class PCRegisterTest {
public static void main(String[] args) {
int i = 10;
int j = 20;
int k = i + j;
}
}
將程式碼編譯成位元組碼檔案,使用 jclasslib 檢視;在位元組碼的左邊有一個行號標識,它其實就是指令地址,用於指向當前執行到哪裏;右邊就是操作指令
// 拿到 10
0: bipush 10
// 存入索引爲 1 的地方
2: istore_1
// 拿到 20
3: bipush 20
// 存入索引爲 2 的地方
5: istore_2
// 載入索引爲 1 的值
6: iload_1
// 載入索引爲 2 的值
7: iload_2
// 執行加法運算
8: iadd
// 放入索引爲 3 的地方
9: istore_3
// 返回
10: return
可以看到有一些操作指令的後面有一個#2
這樣的字元,這就是對應的參照物件地址,表示下一步從常數池中的#2
這個地方獲取值
面試考點
使用PC暫存器儲存位元組碼指令地址有什麼用呢?
PC暫存器爲什麼被設定爲私有的?
CPU 時間片
序列
.棧和堆的區別
棧是執行時的單位,而堆是儲存的單位
Stack Frame
),對應着一次次的 Java方法 呼叫。棧的特點,爲什麼要使用棧結構呢?
開發過程中,在棧中可能出現的異常
Java 虛擬機器規範允許Java棧的大小是動態的或者是固定不變的。
StackOverflowError
異常。常見的就是遞回:當遞回層次非常多,或遞回沒有出口的時候,就會產生這個異常。OutOfMemoryError
異常。【拓展】怎麼設定棧記憶體大小?
官網介紹:https://docs.oracle.com/en/java/javase/11/tools/java.html(Main Tools to Create and Build Applications -> java -> java Command-Line Argument Files -> 搜尋 -Xss
)
我們可以使用參數 -Xss
選項來設定執行緒的最大棧空間,棧的大小直接決定了函數呼叫的最大可達深度
-Xss1m
-Xss1k
// 其他命令
-Xms設定堆的最小空間大小。
-Xmx設定堆的最大空間大小。
-XX:NewSize設定新生代最小空間大小。
-XX:MaxNewSize設定新生代最大空間大小。
-XX:PermSize設定永久代最小空間大小。
-XX:MaxPermSize設定永久代最大空間大小。
-Xss設定每個執行緒的堆疊大小。
// 可以通過輸出遞回層數來檢視是否設定成功
// count 記錄層數
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count++);
main(args);
}
}
棧的儲存單位:棧幀
Stack Frame
)的格式存在。棧執行原理
JVM直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧,遵循先進後出
/ 後進先出
原則。
在一條活動執行緒中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱爲當前棧幀(Current Frame
),與當前棧幀相對應的方法就是當前方法(Current Method
),定義這個方法的類就是當前類(Current Class
)。
執行引擎執行的所有位元組碼指令只針對當前棧幀進行操作。
如果在該方法中呼叫了其他方法,對應的新的棧幀會被建立出來,放在棧的頂端,成爲新的當前幀。
程式碼演示
public class StackFrameTest {
public static void main(String[] args) {
method01();
}
private static int method01() {
System.out.println("方法1的開始");
int i = method02();
System.out.println("方法1的結束");
return i;
}
private static int method02() {
System.out.println("方法2的開始");
int i = method03();;
System.out.println("方法2的結束");
return i;
}
private static int method03() {
System.out.println("方法3的開始");
int i = 30;
System.out.println("方法3的結束");
return i;
}
}
/* 輸出結果
方法1的開始
方法2的開始
方法3的開始
方法3的結束
方法2的結束
方法1的結束
*/
不同線程中所包含的棧幀是不允許存在相互參照的,即不可能在一個棧幀之中參照另外一個執行緒的棧幀。
如果當前方法呼叫了其他方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接着,虛擬機器會丟棄當前棧幀,使得前一個棧幀重新成爲當前棧幀。
Java方法有兩種返回函數的方式,一種是正常的函數返回,使用return
指令;另外一種是拋出異常。不管使用哪種方式,都會導致棧幀被彈出。
棧幀的內部結構
每個棧幀中儲存着:
Local Variables
)Operand Stack
)(或表達式棧)DynamicLinking
)(或指向執行時常數池的方法參照)Return Address
)(或方法正常退出或者異常退出的定義)並行每個執行緒下的棧都是私有的,因此每個執行緒都有自己各自的棧,並且每個棧裏面都有很多棧幀,棧幀的大小主要由 區域性變數表 和 運算元棧決定的,棧幀的大小也影響了棧能存放的棧幀的個數。
區域性變數表:Local Variables
,被稱之爲區域性變數陣列或本地變數表
定義爲一個數位陣列,主要用於儲存方法參數和定義在方法體內的區域性變數,這些數據型別包括各類基本數據型別、物件參照(reference),以及ReturnAddress
型別。
【一些基本數據型別可以轉換成數位,然後存入】
由於區域性變數表是建立線上程的棧上,是執行緒的私有數據,因此不存在數據安全問題
區域性變數表所需的容量大小是在編譯期確定下來的,並儲存在方法的Code屬性的maximum local variables
數據項中。在方法執行期間是不會改變 區域性變數表的大小的。
方法巢狀呼叫的次數由棧的大小決定。一般來說,棧越大,方法巢狀呼叫次數越多
。對一個函數而言,它的參數和區域性變數越多,使得區域性變數表膨脹,它的棧幀就越大,以滿足方法呼叫所需傳遞的資訊增大的需求。進而函數呼叫就會佔用更多的棧空間,導致其巢狀呼叫次數就會減少。
區域性變數表中的變數只在當前方法呼叫中有效。在方法執行時,虛擬機器通過使用區域性變數表完成參數值到參數變數列表的傳遞過程。當方法呼叫結束後,隨着方法棧幀的銷燬,區域性變數表也會隨之銷燬。
理解 Slot(變數槽) ---- 區域性變數表的最基本的儲存單位
參數值的存放總是在區域性變數陣列的index0
開始,到陣列長度-1
的索引結束。
區域性變數表中存放編譯期可知的各種基本數據型別(8種),參照型別(reference
),returnAddress
型別的變數。
在區域性變數表裏,32位元
以內的型別只佔用一個slot
(包括returnAddress
型別),64位元
的型別(1ong和double)佔用兩個slot
。
byte、short、char
在儲存前被轉換爲int
,boolean
也被轉換爲int
,0
表示false
,非0
表示true
。 long
和double
則佔據兩個slot
。
JVM會爲區域性變數表中的每一個Slot都分配一個存取索引,通過這個索引即可成功存取到區域性變數表中指定的區域性變數值
當一個實體方法被呼叫的時候,它的方法參數和方法體內部定義的區域性變數將會按照順序被複製到區域性變數表中的每一個slot上
如果需要存取區域性變數表中一個64bit
的區域性變數值時,只需要使用前一個索引即可。(比如:存取long或double型別變數)
如果當前幀是由構造方法或者實體方法建立的,那麼該物件參照this
將會存放在index
爲0
的slot
處,其餘的參數按照參數表順序繼續排列。【這也就是普通實體方法中無法使用 this 關鍵字的原因(因爲 this 變數不存在於當前方法的區域性變數表中)】
舉例:靜態變數和區域性變數的對比
變數的分類
三者的區別
linking
的prepare
階段,給類變數預設賦值 ====>
init
階段給類變數顯示賦值,即靜態程式碼塊賦值堆空間
中分配範例變數空間,並進行預設賦值
使用前必須進行顯式賦值
,不然編譯不通過。在棧幀中,與效能調優關係最爲密切的部分就是區域性變數表。在方法執行時,虛擬機器使用區域性變數表完成方法的傳遞。
區域性變數表中的變數也是重要的垃圾回收根節點,只要被區域性變數表直接或間接參照的物件都不會被回收,也就是如果物件在棧中的地址被清除掉,那麼在堆中的物件就會被當成垃圾清除
Last - In - First -Out
)的 運算元棧,也可以稱之爲 表達式棧(Expression Stack
)運算元棧,主要用於儲存計算過程的中間結果,同時作爲計算過程中變數臨時的儲存空間。
運算元棧就是 JVM 執行引擎的一個工作區,當一個方法剛開始執行的時候,一個新的棧幀也會隨之被建立出來,這個方法的運算元棧是空的,這個時候陣列是有長度的,而且陣列一旦建立,那麼就是不可變的
每一個運算元棧都會擁有一個明確的棧深度用於儲存數值,其所需的最大深度在編譯期就定義好了,儲存在方法的Code
屬性中,爲max_stack
的值。
棧中的任何一個元素都可以是任意的 Java數據型別【32bit
的型別佔用一個棧單位深度;64bit
的型別佔用兩個棧單位深度】
運算元棧儘管使用的是陣列,但是並非採用存取索引的方式來進行數據存取的,而是隻能通過標準的入棧和出棧操作來完成一次數據存取,只是通過陣列實現的而已。
如果被呼叫的方法帶有返回值的話,其返回值將會被壓入當前棧幀的運算元棧中,並更新PC暫存器中下一條需要執行的位元組碼指令。
運算元棧中元素的數據型別必須與位元組碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行驗證,同時在類載入過程中的類檢驗階段的數據流分析階段要再次驗證。
另外,我們說Java虛擬機器的解釋引擎是基於棧的執行引擎,其中的棧指的就是運算元棧。
詳細分析一下位元組碼每個步驟對應的區域性變數表和運算元棧的變化過程
4
,運算元棧的最大深度是2
bipush
表示把byte
型別轉換成int
型別,因爲10、20、30都可以使用byte
來儲存,但是存入區域性變數表的時候,就自動轉換成int
型別了,例如istore
拓展: 通過分析位元組碼檔案瞭解
i++
和++i
的區別
棧頂快取技術:Top Of Stack Cashing
instruction dispatch
)次數和記憶體讀/寫
次數。ToS
,Top-of-Stack Cashing
)技術,將棧頂元素全部快取在物理CPU的暫存器中,以此降低對記憶體的 讀 / 寫 次數,提升執行引擎的執行效率。動態鏈接,也稱 指向執行時常數池的方法參照
每一個棧幀內部都包含一個指向執行時常數池中該棧幀所屬方法的參照,包含這個參照的目的就是爲了支援當前方法的程式碼能夠實現動態鏈接(Dynamic Linking)。比如:invokedynamic
指令
在Java原始檔被編譯到位元組碼檔案中時,所有的變數和方法參照都作爲符號參照(Symbolic Reference
)儲存在 class檔案 的常數池裏。
比如:描述一個方法呼叫了另外的其他方法時,就是通過常數池中指向方法的符號參照來表示的,那麼動態鏈接的作用就是爲了將這些符號參照轉換爲呼叫方法的直接參照。
兩種鏈接方式:靜態鏈接和動態鏈接
靜態鏈接
動態鏈接
系結機制 機製
Early Binding
)和晚期系結(Late Binding
)。系結是一個欄位、方法或者類在符號參照被替換爲直接參照的過程,這僅僅發生一次。早期系結
晚期系結
早晚期系結的發展歷史
隨着高階語言的橫空出世,類似於 Java 一樣的基於物件導向的程式語言如今越來越多,儘管這類程式語言在語法風格上存在一定的差別,但是它們彼此之間始終保持着一個共性,那就是都支援封裝、繼承和多型等物件導向特性,既然這一類的程式語言具備多型特性,那麼自然也就具備早期系結和晚期系結兩種系結方式。
Java 中任何一個普通的方法其實都具備虛擬函式的特徵(體現爲執行期才能 纔能確定下來),它們相當於C++ 語言中的虛擬函式( C++ 中則需要使用關鍵字virtual
來顯式定義)。如果在 Java 程式中不希望某個方法擁有虛擬函式的特徵時,則可以使用關鍵字final
來標記這個方法。
虛方法和非虛方法
如果方法在編譯期就確定了具體的呼叫版本,這個版本在執行時是不可變的。這樣的方法稱爲非虛方法。
final
方法、範例構造器、父類別方法都是非虛方法。其他方法稱爲虛方法。
虛擬機器中提供了以下幾條方法呼叫指令:
普通呼叫指令:
invokestatic
:呼叫靜態方法,解析階段確定唯一方法版本invokespecial
:呼叫方法、私有及父類別方法,解析階段確定唯一方法版本invokevirtual
:呼叫所有虛方法invokeinterface
:呼叫介面方法動態呼叫指令:
invokedynamic
:動態解析出需要呼叫的方法,然後執行前四條指令固化在虛擬機器內部,方法的呼叫執行不可人爲幹預,而invokedynamic
指令則支援由使用者確定方法版本。其中invokestatic
指令和invokespecial
指令呼叫的方法稱爲非虛方法
,其餘的(final
修飾的除外)稱爲虛方法。
關於
invokednamic
指令
JVM位元組碼指令集一直比較穩定,一直到 Java7 中才增加了一個invokedynamic
指令,這是Java爲了實現【動態型別語言】支援而做的一種改進。
但是在 Java7 中並沒有提供直接生成invokedynamic
指令的方法,需要藉助 ASM 這種底層位元組碼工具來產生invokedynamic
指令。直到 Java8 的 Lambda 表達式的出現,invokedynamic
指令的生成,在Java中纔有了直接的生成方式。
Java7 中增加的動態語言型別支援的本質是對 Java 虛擬機器規範的修改,而不是對Java語言規則的修改,這一塊相對來講比較複雜,增加了虛擬機器中的方法呼叫,最直接的受益者就是執行在 Java 平臺的動態語言的編譯器。
動態型別語言和靜態型別語言
動態型別語言和靜態型別語言兩者的區別就在於對型別的檢查是在編譯期還是在執行期,如果是在編譯期就是靜態型別語言,執行期則是動態型別語言。
說的再直白一點就是,靜態型別語言是判斷變數自身的型別資訊;動態型別語言是判斷變數值的型別資訊,變數沒有型別資訊,變數值纔有型別資訊,這是動態語言的一個重要特徵。
Java:String info = "abc"; (Java是靜態型別語言的,會先編譯就進行型別檢查)
JS:var name = "abc"; var age = 10; (執行時才進行檢查, Python 也類似)
方法重寫
Java 語言中方法重寫的本質:
java.lang.IllegalAccessError
異常。java.lang.AbstractMethodsError
異常。IllegalAccessError
介紹
虛方法表
存放呼叫該方法的PC暫存器的值。
一個方法的結束,有兩種方式:
無論通過哪種方式退出,在方法退出後都返回到該方法被呼叫的位置。方法正常退出時,呼叫者的PC暫存器的值作爲返回地址,即:呼叫該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會儲存這部分資訊。
當一個方法開始執行後,只有兩種方式可以退出這個方法:
(1)執行引擎遇到任意一個方法返回的位元組碼指令(return
),會有返回值傳遞給上層的方法呼叫者,簡稱正常完成出口;
ireturn
(當返回值是boolean,byte,char,short和int型別
時使用),lreturn
(Long型別),freturn
(Float型別),dreturn
(Double型別),areturn
。另外還有一個return
指令宣告爲void的方法,範例初始化方法,類和介面的初始化方法
使用。(2)在方法執行過程中遇到異常(Exception),並且這個異常沒有在方法內進行處理,也就是隻要在本方法的異常表中沒有搜尋到匹配的例外處理器,就會導致方法退出,簡稱異常完成出口。
方法執行過程中,拋出異常時的例外處理,儲存在一個異常處理表,方便在發生異常的時候找到處理異常的程式碼
本質上,方法的退出就是當前棧幀出棧的過程。此時,需要恢復上層方法的區域性變數表、運算元棧、將返回值壓入呼叫者棧幀的運算元棧、設定PC暫存器值等,讓呼叫者方法繼續執行下去。
舉例棧溢位的情況?(StackOverflowError)
調整棧大小,就能保證不出現溢位麼?
分配的棧記憶體越大越好麼?
垃圾回收是否涉及到虛擬機器棧?
方法中定義的區域性變數是否執行緒安全?
/**
* 面試題
* 方法中定義區域性變數是否執行緒安全?具體情況具體分析
* 何爲執行緒安全?
* 如果只有一個執行緒纔可以操作此數據,則必是執行緒安全的
* 如果有多個執行緒操作,則此數據是共用數據,如果不考慮共用機制 機製,則爲執行緒不安全
*/
public class StringBuilderTest {
// s1的宣告方式是執行緒安全的
public static void method01() {
// 執行緒內部建立的,屬於區域性變數
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
// 這個也是執行緒不安全的,因爲有返回值,有可能被其它的程式所呼叫
public static StringBuilder method04() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder;
}
// stringBuilder 是執行緒不安全的,操作的是共用數據
public static void method02(StringBuilder stringBuilder) {
stringBuilder.append("a");
stringBuilder.append("b");
}
/**
* 同時併發的執行,會出現執行緒不安全的問題
*/
public static void method03() {
StringBuilder stringBuilder = new StringBuilder();
new Thread(() -> {
stringBuilder.append("a");
stringBuilder.append("b");
}, "t1").start();
method02(stringBuilder);
}
// 是執行緒安全的,因爲 toString 會 new 一個 String 物件範例
// 而且 String 是 final 修飾的類,也是執行緒安全的
public static String method05() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder.toString();
}
}
執行時數據區,是否存在 Error 和 GC ?
簡單地講,一個Native Method
是一個Java呼叫非Java程式碼
的接囗。
一個Native Method
是這樣一個Java方法:該方法的實現由非Java語言實現,比如C
。這個特徵並非Java所特有,很多其它的程式語言都有這一機制 機製,比如在C++中,你可以用extern
「C」 告知C++編譯器去呼叫一個 C 的函數。
"A native method is a Java method whose implementation is provided by non-java code."
(本地方法是一個非Java的方法,它的具體實現是非Java程式碼的實現)
在定義一個native method
時,並不提供實現體(有些像定義一個Java Interface),因爲其實現體是由非java語言在外面實現的。
本地介面的作用是融合不同的程式語言爲Java所用,它的初衷是融合C/C++程式。
程式碼演示如何編寫 Native 方法
/* 識別符號 native 可以與其它 java 識別符號連用,但是 abstract 除外 */
public class IhaveNatives {
public native void Native1(int x);
native static public long Native2();
native synchronized private float Native3(Object o);
native void Natives(int[] ary) throws Exception;
}
爲什麼使用Native Method?
Java使用起來非常方便,然而有些層次的任務用 Java 實現起來不容易,或者我們對程式的效率很在意時,問題就來了。
在 Java 剛發行的時候,在執行效率上遠達不到 C/C++ 的水平,但是現在 Java 的執行效率大體上已經跟 C/C++ 差不多了。
與Java環境的互動
與操作系統的互動
Sun’s Java
java.lang.Thread
的setPriority()
方法是用 Java實現的,但是它實現呼叫的是該類裡的本地方法setPriority0()
。這個本地方法是用C實現的,並被植入JVM內部,在Windows 95的平臺上,這個本地方法最終將呼叫Win32 SetPriority()
ApI。這是一個本地方法的具體實現由JVM直接提供,更多的情況是本地方法由外部的動態鏈接庫(External Dynamic Link Library
)提供,然後被JVM呼叫。目前該方法使用的越來越少了,除非是與硬體有關的應用,比如通過Java程式驅動印表機或者Java系統管理生產裝置,在企業級應用中已經比較少見。因爲現在的異構領域間的通訊很發達,比如可以使用 Socket 通訊,也可以使用 Web Service 等等,不多做介紹。
StackOverflowError
異常。OutOfMemoryError
異常。Native Method Stack
中登記native
方法,在Execution Engine
執行時載入本地方法庫。當某個執行緒呼叫一個本地方法時,它就進入了一個全新的並且不再受虛擬機器限制的世界。它和虛擬機器擁有同樣的許可權。
並不是所有的 JVM 都支援本地方法。因爲Java虛擬機器規範並沒有明確要求本地方法棧的使用語言、具體實現方式、數據結構等。如果JVM產品不打算支援native
方法,也可以無需實現本地方法棧。
在
Hotspot JVM
中,直接將本地方法棧和虛擬機器棧合二爲一
由於堆區和方法區篇幅較長,放在另一篇文章中