JVM-02-執行時資料區(一)程式計數器 虛擬機器棧
綜述
類載入階段完成後,就會用到執行引擎對我們的類進行使用,同時執行引擎將會使用到我們的執行時資料區,就好比是大廚做飯,我們把大廚後面的東西(切好的菜,刀,調料),比作是執行時資料區。而廚師可以類比於執行引擎,將通過準備的東西進行製作成精美的菜品。
記憶體是非常重要的系統資源,是硬碟和CPU的中間倉庫及橋樑,承載著作業系統和應用程式的實時執行。JVM記憶體佈局規定了Java在執行過程中記憶體申請、分配、管理的策略,保證了JVM的高效穩定執行。不同的JVM對於記憶體的劃分方式和管理機制存在著部分差異。結合JVM虛擬機器規範,來探討一下經典的JVM記憶體佈局。
我們通過磁碟或者網路IO得到的資料,都需要先載入到記憶體中,然後CPU從記憶體中獲取資料進行讀取,也就是說記憶體充當了CPU和磁碟之間的橋樑
執行時資料區完整結構
Java虛擬機器定義了若干種程式執行期間會使用到的執行時資料區,其中有一些會隨著虛擬機器啟動而建立,隨著虛擬機器退出而銷燬。另外一些則是與執行緒一一對應的,這些與執行緒對應的資料區域會隨著執行緒開始和結束而建立和銷燬。
-
每個執行緒獨立:程式計數器、棧、本地棧。
-
執行緒間共享:堆、堆外記憶體(永久代或元空間、程式碼快取)
執行緒間共享的說明:每個JVM只有一個Runtime例項,即為執行時環境。
執行緒
-
執行緒是一個程式裡的執行單元。JVM允許一個應用有多個執行緒並行的執行。 在Hotspot JVM裡,每個執行緒都與作業系統的本地執行緒直接對映。
- 當一個Java執行緒準備好執行以後,此時一個作業系統的本地執行緒也同時建立。Java執行緒執行終止後,本地執行緒也會回收。
-
作業系統負責所有執行緒的安排排程到任何一個可用的CPU上。一旦本地執行緒初始化成功,它就會呼叫Java執行緒中的run()方法。
JVM系統執行緒
如果使用jonsole或者是任何一個除錯工具,都能看到在後臺有許多執行緒在執行。這些後臺執行緒不包括呼叫public static void main(String[])的main執行緒以及所有這個main執行緒自己建立的執行緒。這些主要的後臺系統執行緒在Hotspot JVM裡主要是以下幾個:
- 虛擬機器執行緒:這種執行緒的操作是需要JVM達到安全點才會出現。這些操作必須在不同的執行緒中發生的原因是他們都需要JVM達到安全點,這樣堆才不會變化。這種執行緒的執行型別包括"stop-the-world"的垃圾收集,執行緒棧收集,執行緒掛起以及偏向鎖撤銷。
- 週期任務執行緒:這種執行緒是時間週期事件的體現(比如中斷),他們一般用於週期性操作的排程執行。
- GC執行緒:這種執行緒對在JVM裡不同種類的垃圾收集行為提供了支援。
- 編譯執行緒:這種執行緒在執行時會將位元組碼編譯成到原生代碼。
- 訊號排程執行緒:這種執行緒接收訊號併發送給JVM,在它內部通過呼叫適當的方法進行處理。
程式計數器
介紹
JVM中的程式計數暫存器(Program Counter Register)中,Register的命名源於CPU的暫存器,暫存器儲存指令相關的現場資訊。CPU只有把資料裝載到暫存器才能夠執行。這裡,並非是廣義上所指的物理暫存器,或許將其翻譯為PC計數器(或指令計數器)會更加貼切(也稱為程式鉤子),並且也不容易引起一些不必要的誤會。JVM中的PC暫存器是對物理PC暫存器的一種抽象模擬。
- 它是一塊很小的記憶體空間,幾乎可以忽略不記。也是執行速度最快的儲存區域。
- 在JVM規範中,每個執行緒都有它自己的程式計數器,是執行緒私有的,生命週期與執行緒的生命週期保持一致。
- 任何時間一個執行緒都只有一個方法在執行,也就是所謂的當前方法。程式計數器會儲存當前執行緒正在執行的Java方法的JVM指令地址;或者,如果是在執行native方法,則是未指定值(undefned)。
- 它是程式控制流的指示器,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。
- 它是唯一一個在Java虛擬機器規範中沒有規定任何OutofMemoryError情況的區域。
作用
PC暫存器用來儲存指向下一條指令的地址,也就是即將要執行的指令程式碼。由執行引擎讀取下一條指令。
例子:以下是對應程式碼的位元組碼檔案
int i = 10;
int j = 20;
int k = i + j;
String s = "abc";
System.out.println(s);
- 使用PC暫存器儲存位元組碼指令地址有什麼用呢?為什麼使用PC暫存器記錄當前執行緒的執行地址呢?
- 因為CPU需要不停的切換各個執行緒,這時候切換回來以後,就得知道接著從哪開始繼續執行。
- JVM的位元組碼直譯器就需要通過改變PC暫存器的值來明確下一條應該執行什麼樣的位元組碼指令。
- PC暫存器為什麼被設定為執行緒私有的?
我們都知道所謂的多執行緒在一個特定的時間段內只會執行其中某一個執行緒的方法,CPU會不停地做任務切換,這樣必然導致經常中斷或恢復,如何保證分毫無差呢?為了能夠準確地記錄各個執行緒正在執行的當前位元組碼指令地址,最好的辦法自然是為每一個執行緒都分配一個PC暫存器,這樣一來各個執行緒之間便可以進行獨立計算,從而不會出現相互干擾的情況。
由於CPU時間片輪限制,眾多執行緒在併發執行過程中,任何一個確定的時刻,一個處理器或者多核處理器中的一個核心,只會執行某個執行緒中的一條指令。
CPU時間片即CPU分配給各個程式的時間,每個執行緒被分配一個時間段,稱作它的時間片。
在巨集觀上:我們可以同時開啟多個應用程式,每個程式並行不悖,同時執行。
但在微觀上:由於只有一個CPU,一次只能處理程式要求的一部分,如何處理公平,一種方法就是引入時間片,每個程式輪流執行。
虛擬機器棧
概述
由於跨平臺性的設計,Java的指令都是根據棧來設計的。不同平臺CPU架構不同,所以不能設計為基於暫存器的。 優點是跨平臺,指令集小,編譯器容易實現,缺點是效能下降,實現同樣的功能需要更多的指令。
Java虛擬機器棧(Java Virtual Machine Stack),每個執行緒在建立時都會建立一個虛擬機器棧,其內部儲存一個個的棧幀(Stack Frame),對應著一次次的Java方法呼叫。它是執行緒私有的。生命週期和執行緒一致,也就是說執行緒結束了,該虛擬機器棧也銷燬了。
棧是一種快速有效的分配儲存方式,訪問速度僅次於程式計數器。JVM直接對Java棧的操作只有兩個:
- 每個方法執行,伴隨著進棧(入棧、壓棧)
- 執行結束後的出棧工作
對於棧來說不存在垃圾回收問題(棧存在溢位的情況)
作用
主管Java程式的執行,它儲存方法的區域性變數(8種基本資料型別和引用型別的地址)、部分結果,並參與方法的呼叫和返回。
區域性變數,它是相比於成員變數來說的(或屬性)
基本資料型別變數 VS 引用型別變數(類、陣列、介面)
記憶體中的堆和棧
首先棧是執行時的單位,而堆是儲存的單位
- 棧解決程式的執行問題,即程式如何執行,或者說如何處理資料。
- 堆解決的是資料儲存的問題,即資料怎麼放,放哪裡
異常
Java 虛擬機器規範允許Java棧的大小是動態的或者是固定不變的。
- 如果採用固定大小的Java虛擬機器棧,那每一個執行緒的Java虛擬機器棧容量可以線上程建立的時候獨立選定。如果執行緒請求分配的棧容量超過Java虛擬機器棧允許的最大容量,Java虛擬機器將會丟擲一個StackoverflowError 異常。
- 如果Java虛擬機器棧可以動態擴充套件,並且在嘗試擴充套件的時候無法申請到足夠的記憶體,或者在建立新的執行緒時沒有足夠的記憶體去建立對應的虛擬機器棧,那Java虛擬機器將會丟擲一個 outofMemoryError 異常。
設定棧大小
-Xss size 即:-XX:ThreadStackSize
-Xss1m
-Xss1k
一般預設為512k-1024k,取決於作業系統(jdk5之前,預設棧大小是256k;jdk5之後,預設棧大小是1024k)
棧的大小直接決定了函式呼叫的最大可達深度
棧的儲存單位
每個執行緒都有自己的棧,棧中的資料都是以棧幀(Stack Frame)的格式存在。
在這個執行緒上正在執行的每個方法都各自對應一個棧幀(Stack Frame)。
棧幀是一個記憶體區塊,是一個數據集,維繫著方法執行過程中的各種資料資訊。
棧執行原理
- JVM直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧,遵循“先進後出”/“後進先出”原則。
- 在一條活動執行緒中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱為當前棧幀(Current Frame),與當前棧幀相對應的方法就是當前方法(Current Method),定義這個方法的類就是當前類(Current Class)。
- 執行引擎執行的所有位元組碼指令只針對當前棧幀進行操作。
- 如果在該方法中呼叫了其他方法,對應的新的棧幀會被創建出來,放在棧的頂端,成為新的當前幀。
- 不同執行緒中所包含的棧幀是不允許存在相互引用的,即不可能在一個棧幀之中引用另外一個執行緒的棧幀。
- 如果當前方法呼叫了其他方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接著,虛擬機器會丟棄當前棧幀,使得前一個棧幀重新成為當前棧幀。
- Java方法有兩種返回函式的方式,一種是正常的函式返回,使用return指令;另外一種是丟擲異常。不管使用哪種方式,都會導致棧幀被彈出。
棧幀
每個棧幀中儲存著:
-
區域性變量表(Local Variables)
-
運算元棧(operand Stack)(或表示式棧)
-
動態連結(DynamicLinking)(或指向執行時常量池的方法引用)
-
方法返回地址(Return Address)(或方法正常退出或者異常退出的定義)
-
一些附加資訊
並行每個執行緒下的棧都是私有的,因此每個執行緒都有自己各自的棧,並且每個棧裡面都有很多棧幀,棧幀的大小主要由區域性變量表 和 運算元棧決定的
區域性變量表
-
區域性變量表:Local Variables,被稱之為區域性變數陣列或本地變量表
-
定義為一個數字陣列,主要用於儲存方法引數和定義在方法體內的區域性變數,這些資料型別包括各類基本資料型別、物件引用(reference),以及returnAddress型別。
-
由於區域性變量表是建立線上程的棧上,是執行緒的私有資料,因此不存在資料安全問題
-
區域性變量表所需的容量大小是在編譯期確定下來的,並儲存在方法的Code屬性的maximum local variables資料項中。在方法執行期間是不會改變區域性變量表的大小的。
對於這段示例程式碼
public class LocalVarialbe { public static void main(String[] args) { LocalVarialbe localVarialbe = new LocalVarialbe(); int num = 3; localVarialbe.test1(); } public void test1() { Date date = new Date(); String name1 = "zoran"; String info = null; System.out.println(date); } }
通過javap檢視結果發現,
-
方法巢狀呼叫的次數由棧的大小決定。一般來說,棧越大,方法巢狀呼叫次數越多。對一個函式而言,它的引數和區域性變數越多,使得區域性變量表膨脹,它的棧幀就越大,以滿足方法呼叫所需傳遞的資訊增大的需求。進而函式呼叫就會佔用更多的棧空間,導致其巢狀呼叫次數就會減少。
-
區域性變量表中的變數只在當前方法呼叫中有效。在方法執行時,虛擬機器通過使用區域性變量表完成引數值到引數變數列表的傳遞過程。當方法呼叫結束後,隨著方法棧幀的銷燬,區域性變量表也會隨之銷燬。
JClasslib各個欄位含義解析
關於Slot的理解
-
引數值的存放總是在區域性變數陣列的index=0開始,到陣列長度-1的索引結束。
-
區域性變量表,最基本的儲存單元是Slot(變數槽)
-
區域性變量表中存放編譯期可知的各種基本資料型別(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的區域性變數值時,只需要使用前一個索引即可。
-
如果當前幀是由構造方法或者例項方法建立的,那麼該物件引用this將會存放在index為0的slot處,其餘的引數按照引數表順序繼續排列。這也解釋了為什麼static方法中不能呼叫this,因為static方法的區域性變量表中不存在this變數
Slot的重複利用
棧幀中的區域性變量表中的槽位是可以重用的,如果一個區域性變數過了其作用域,那麼在其作用域之後申明的新的區域性變就很有可能會複用過期區域性變數的槽位,從而達到節省資源的目的。
public void test1() {
int a = 0;
{
int b = 0;
b = a + 1;
}
// c會使用之前已經銷燬的b變數佔據的slot的位置
int c = a + 1;
}
在棧幀中,與效能調優關係最為密切的部分就是前面提到的區域性變量表。在方法執行時,虛擬機器使用區域性變量表完成方法的傳遞。
區域性變量表中的變數也是重要的垃圾回收根節點,只要被區域性變量表中直接或間接引用的物件都不會被回收。
運算元棧
運算元棧:Operand Stack
- 每一個獨立的棧幀除了包含區域性變量表以外,還包含一個後進先出(Last - In - First -Out)的 運算元棧,也可以稱之為 表示式棧(Expression Stack)
- 運算元棧,在方法執行過程中,根據位元組碼指令,往棧中寫入資料或提取資料,即入棧(push)和 出棧(pop)
- 某些位元組碼指令將值壓入運算元棧,其餘的位元組碼指令將運算元取出棧。使用它們後再把結果壓入棧
- 比如:執行復制、交換、求和等操作
- 運算元棧,主要用於儲存計算過程的中間結果,同時作為計算過程中變數臨時的儲存空間。
- 運算元棧就是JVM執行引擎的一個工作區,當一個方法剛開始執行的時候,一個新的棧幀也會隨之被創建出來,這個方法的運算元棧是空的。.
這個時候陣列是有長度的,因為陣列一旦建立,那麼就是不可變的
-
每一個運算元棧都會擁有一個明確的棧深度用於儲存數值,其所需的最大深度在編譯期就定義好了,儲存在方法的Code屬性中,為max_stack的值。
-
棧中的任何一個元素都是可以任意的Java資料型別
- 32bit的型別佔用一個棧單位深度
- 64bit的型別佔用兩個棧單位深度
-
運算元棧並非採用訪問索引的方式來進行資料訪問的,而是隻能通過標準的入棧和出棧操作來完成一次資料訪問
-
如果被呼叫的方法帶有返回值的話,其返回值將會被壓入當前棧幀的運算元棧中,並更新PC暫存器中下一條需要執行的位元組碼指令。
-
運算元棧中元素的資料型別必須與位元組碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行驗證,同時在類載入過程中的類檢驗階段的資料流分析階段要再次驗證。|
-
另外,我們說Java虛擬機器的解釋引擎是基於棧的執行引擎,其中的棧指的就是運算元棧。
程式碼追蹤
public void testAddOperation() {
byte i = 15;
int j = 8;
int k = i + j;
}
0 bipush 15 //byte,short,char,boolean都以int型來儲存
2 istore_1
3 bipush 8
5 istore_2
6 iload_1
7 iload_2
8 iadd
9 istore_3
10 return
拓展:在運算元棧中,型別根據實際值進行相應的指令操作,儲存在區域性變量表中,則byte、short、char 在儲存前被轉換為int,boolean也被轉換為int,0表示false,非0表示true。
++i和i++等以上都會在位元組碼篇進行介紹
棧頂快取
棧頂快取技術:Top Of Stack Cashing
前面提過,基於棧式架構的虛擬機器所使用的零地址指令更加緊湊,但完成一項操作的時候必然需要使用更多的入棧和出棧指令,這同時也就意味著將需要更多的指令分派(instruction dispatch)次數和記憶體讀/寫次數。
由於運算元是儲存在記憶體中的,因此頻繁地執行記憶體讀/寫操作必然會影響執行速度。為了解決這個問題,HotSpot JVM的設計者們提出了棧頂快取(Tos,Top-of-Stack Cashing)技術,將棧頂元素全部快取在物理CPU的暫存器中,以此降低對記憶體的讀/寫次數,提升執行引擎的執行效率。
複習—暫存器:指令更少,執行速度快
動態連結
動態連結、方法返回地址、附加資訊 : 有些地方統稱為幀資料區
每一個棧幀內部都包含一個指向執行時常量池中該棧幀所屬方法的引用包含這個引用的目的就是為了支援當前方法的程式碼能夠實現動態連結(Dynamic Linking)。比如:invokedynamic指令
在Java原始檔被編譯到位元組碼檔案中時,所有的變數和方法引用都作為符號引用(symbolic Reference)儲存在class檔案的常量池裡。比如:描述一個方法呼叫了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示的,那麼動態連結的作用就是為了將這些符號引用轉換為呼叫方法的直接引用。
示例
public void methodA(){
System.out.println("1234");
}
public void methodB() {
System.out.println("methodB");
methodA();
num++;
}
methodB方法對應的位元組碼檔案,每條指令後面#跟著數字即是符號引用
public void methodB();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String methodB
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: invokevirtual #7 // Method methodA:()V
12: aload_0
13: dup
14: getfield #2 // Field num:I
17: iconst_1
18: iadd
19: putfield #2 // Field num:I
22: return
對應常量池
Constant pool:
#1 = Methodref #9.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #8.#24 // com/zoran/LocalVarialbe.num:I
#3 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#4 = String #27 // 1234
#5 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = String #20 // methodB
#7 = Methodref #8.#30 // com/zoran/LocalVarialbe.methodA:()V
#8 = Class #31 // com/zoran/LocalVarialbe
#9 = Class #32 // java/lang/Object
#10 = Utf8 num
#11 = Utf8 I
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/zoran/LocalVarialbe;
#19 = Utf8 methodA
#20 = Utf8 methodB
#21 = Utf8 SourceFile
#22 = Utf8 LocalVarialbe.java
#23 = NameAndType #12:#13 // "<init>":()V
#24 = NameAndType #10:#11 // num:I
#25 = Class #33 // java/lang/System
#26 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#27 = Utf8 1234
#28 = Class #36 // java/io/PrintStream
#29 = NameAndType #37:#38 // println:(Ljava/lang/String;)V
#30 = NameAndType #19:#13 // methodA:()V
#31 = Utf8 com/zoran/LocalVarialbe
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (Ljava/lang/String;)V
常量池的作用:在不同的方法,都可能呼叫常量或者方法,只需要儲存一份即可,節省了空間,常量池提供一些符號和常量,便於指令的識別
方法呼叫:解析與分配
在JVM中,將符號引用轉換為呼叫方法的直接引用與方法的繫結機制相關
連結
靜態連結
當一個位元組碼檔案被裝載進JVM內部時,如果被呼叫的目標方法在編譯期可知,且執行期保持不變時,這種情況下降呼叫方法的符號引用轉換為直接引用的過程稱之為靜態連結(invokestatic | invokespecial)
動態連結
如果被呼叫的方法在編譯期無法被確定下來,也就是說,只能夠在程式執行期將呼叫的方法的符號轉換為直接引用,由於這種引用轉換過程具備動態性,因此也被稱之為動態連結,多型就體現了動態連結。(invokevirtual | invokeinterface)
繫結機制
對應的方法的繫結機制為:早期繫結(Early Binding)和晚期繫結(Late Binding)。繫結是一個欄位、方法或者類在符號引用被替換為直接引用的過程,這僅僅發生一次。
早期繫結
早期繫結就是指被呼叫的目標方法如果在編譯期可知,且執行期保持不變時,即可將這個方法與所屬的型別進行繫結,這樣一來,由於明確了被呼叫的目標方法究竟是哪一個,因此也就可以使用靜態連結的方式將符號引用轉換為直接引用。
晚期繫結
如果被呼叫的方法在編譯期無法被確定下來,只能夠在程式執行期根據實際的型別繫結相關的方法,這種繫結方式也就被稱之為晚期繫結。多型就體現了晚期繫結。
早晚期繫結的發展歷史
隨著高階語言的橫空出世,類似於Java一樣的基於面向物件的程式語言如今越來越多,儘管這類程式語言在語法風格上存在一定的差別,但是它們彼此之間始終保持著一個共性,那就是都支援封裝、繼承和多型等面向物件特性,既然這一類的程式語言具備多型特性,那麼自然也就具備早期繫結和晚期繫結兩種繫結方式。
Java中任何一個普通的方法其實都具備虛擬函式的特徵,它們相當於C++語言中的虛擬函式(C++中則需要使用關鍵字virtual來顯式定義)。如果在Java程式中不希望某個方法擁有虛擬函式的特徵時,則可以使用關鍵字final來標記這個方法。
虛方法和非虛方法
- 如果方法在編譯期就確定了具體的呼叫版本,這個版本在執行時是不可變的。這樣的方法稱為非虛方法。
- 靜態方法、私有方法、final方法、例項構造器、父類方法都是非虛方法。
- 其他方法稱為虛方法。
子類物件的多型的使用前提
- 類的繼承關係
- 方法的重寫
虛擬機器中提供了以下幾條方法呼叫指令:
普通呼叫指令
-
invokestatic:呼叫靜態方法,解析階段確定唯一方法版本
-
invokespecial:呼叫方法、私有及父類方法,解析階段確定唯一方法版本
-
invokevirtual:呼叫所有虛方法(final修飾的除外)
-
invokeinterface:呼叫介面方法
/** * 解析呼叫中非虛方法、虛方法的測試 */ class Father { public Father(){ System.out.println("Father預設構造器"); } public static void showStatic(String s){ System.out.println("Father show static"+s); } public final void showFinal(){ System.out.println("Father show final"); } public void showCommon(){ System.out.println("Father show common"); } } public class Son extends Father{ public Son(){ super(); } public Son(int age){ this(); } public static void main(String[] args) { Son son = new Son(); son.show(); } //不是重寫的父類方法,因為靜態方法不能被重寫 public static void showStatic(String s){ System.out.println("Son show static"+s); } private void showPrivate(String s){ System.out.println("Son show private"+s); } public void show(){ //invokestatic showStatic(" 大頭兒子"); //invokestatic super.showStatic(" 大頭兒子"); //invokespecial showPrivate(" hello!"); //invokespecial super.showCommon(); //invokevirtual 因為此方法宣告有final 不能被子類重寫,所以也認為該方法是非虛方法 showFinal(); //虛方法如下 //invokevirtual showCommon();//沒有顯式加super,被認為是虛方法,因為子類可能重寫showCommon info(); MethodInterface in = null; //invokeinterface 不確定介面實現類是哪一個 需要重寫 in.methodA(); } public void info(){ } } interface MethodInterface { void methodA(); }
動態呼叫指令
-
invokedynamic:動態解析出需要呼叫的方法,然後執行
前四條指令固化在虛擬機器內部,方法的呼叫執行不可人為干預,而invokedynamic指令則支援由使用者確定方法版本。其中invokestatic指令和invokespecial指令呼叫的方法稱為非虛方法,其餘的(final修飾的除外)稱為虛方法。
invokedynamic指令
JVM位元組碼指令集一直比較穩定,一直到Java7中才增加了一個invokedynamic指令,這是Java為了實現動態型別語言支援而做的一種改進。
但是在Java7中並沒有提供直接生成invokedynamic指令的方法,需要藉助ASM這種底層位元組碼工具來產生invokedynamic指令。直到Java8的Lambda表示式的出現,invokedynamic指令的生成,在Java中才有了直接的生成方式。
Java7中增加的動態語言型別支援的本質是對Java虛擬機器規範的修改,而不是對Java語言規則的修改,這一塊相對來講比較複雜,增加了虛擬機器中的方法呼叫,最直接的受益者就是執行在Java平臺的動態語言的編譯器。
動態型別語言和靜態型別語言
動態型別語言和靜態型別語言兩者的區別就在於對型別的檢查是在編譯期還是在執行期,滿足前者就是靜態型別語言,反之是動態型別語言。
說的再直白一點就是,靜態型別語言是判斷變數自身的型別資訊;動態型別語言是判斷變數值的型別資訊,變數沒有型別資訊,變數值才有型別資訊,這是動態語言的一個重要特徵。
方法重寫的本質
Java 語言中方法重寫的本質:
- 找到運算元棧頂的第一個元素所執行的物件的實際型別,記作C。
- 如果在型別C中找到與常量中的描述符合簡單名稱都相符的方法,則進行訪問許可權校驗,如果通過則返回這個方法的直接引用,查詢過程結束;如果不通過,則返回java.1ang.I1legalAccessError 異常。
- 否則,按照繼承關係從下往上依次對C的各個父類進行第2步的搜尋和驗證過程。
- 如果始終沒有找到合適的方法,則丟擲java.lang.AbstractMethodsrror異常。
IllegalAccessError介紹
程式試圖訪問或修改一個屬性或呼叫一個方法,這個屬性或方法,你沒有許可權訪問。一般的,這個會引起編譯器異常。這個錯誤如果發生在執行時,就說明一個類發生了不相容的改變。
方法的呼叫:虛方法表
在面向物件的程式設計中,會很頻繁的使用到動態分派,如果在每次動態分派的過程中都要重新在類的方法元資料中搜索合適的目標的話就可能影響到執行效率。因此,為了提高效能,JVM採用在類的方法區建立一個虛方法表 (virtual method table)(非虛方法不會出現在表中)來實現。使用索引表來代替查詢。
每個類中都有一個虛方法表,表中存放著各個方法的實際入口。
虛方法表會在類載入的連結階段被建立並開始初始化,類的變數初始值準備完成之後,JVM會把該類的方法表也初始化完畢。
方法返回地址
存放呼叫該方法的pc暫存器的值。一個方法的結束,有兩種方式:
- 正常執行完成
- 出現未處理的異常,非正常退出
無論通過哪種方式退出,在方法退出後都返回到該方法被呼叫的位置。方法正常退出時,呼叫者的pc計數器的值作為返回地址,即呼叫該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會儲存這部分資訊。
當一個方法開始執行後,只有兩種方式可以退出這個方法:
執行引擎遇到任意一個方法返回的位元組碼指令(return),會有返回值傳遞給上層的方法呼叫者,簡稱正常完成出口;
- 一個方法在正常呼叫完成之後,究竟需要使用哪一個返回指令,還需要根據方法返回值的實際資料型別而定。
- 在位元組碼指令中,返回指令包含ireturn(當返回值是boolean,byte,char,short和int型別時使用),lreturn(Long型別),freturn(Float型別),dreturn(Double型別),areturn(引用型別)。另外還有一個return指令宣告為void的方法,例項初始化方法,類和介面的初始化方法使用。
在方法執行過程中遇到異常(Exception),並且這個異常沒有在方法內進行處理,也就是隻要在本方法的異常表中沒有搜尋到匹配的異常處理器,就會導致方法退出,簡稱異常完成出口。
方法執行過程中,丟擲異常時的異常處理,儲存在一個異常處理表,方便在發生異常的時候找到處理異常的程式碼
本質上,方法的退出就是當前棧幀出棧的過程。此時,需要恢復上層方法的區域性變量表、運算元棧、將返回值壓入呼叫者棧幀的運算元棧、設定PC暫存器值等,讓呼叫者方法繼續執行下去。
正常完成出口和異常完成出口的區別在於:通過異常完成出口退出的不會給他的上層呼叫者產生任何的返回值
附加資訊
棧幀中還允許攜帶與Java虛擬機器實現相關的一些附加資訊。例如:對程式除錯提供支援的資訊。
棧的相關面試題
- 舉例棧溢位的情況?(StackOverflowError)
- 通過 -Xss設定棧的大小
- 調整棧大小,就能保證不出現溢位麼?
- 不能保證不溢位
- 分配的棧記憶體越大越好麼?
- 不是,一定時間內降低了OOM概率,但是會擠佔其它的執行緒空間,因為整個空間是有限的。
- 垃圾回收是否涉及到虛擬機器棧?
- 不會
- 方法中定義的區域性變數是否執行緒安全?
- 具體問題具體分析
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);
}
// StringBuilder是執行緒安全的,但是String也可能執行緒不安全的
public static String method05() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder.toString();
}
}
總結一句話就是:如果物件是在內部產生,並在內部消亡,沒有返回到外部,那麼它就是執行緒安全的,反之則是執行緒不安全的。
執行時資料區,是否存在Error和GC?
執行時資料區 | 是否存在Error | 是否存在GC |
---|---|---|
程式計數器 | 否 | 否 |
虛擬機器棧 | 是 | 否 |
本地方法棧 | 是 | 否 |
方法區 | 是(OOM) | 是 |
堆 | 是 | 是 |