運行時資料區-虛擬機堆疊

java虛擬機在執行java程式程序中會把它所管理的記憶體劃分為若干個不同的區域,這些區域各有各的作用,根據java虛擬機規范,java虛擬機所管理的記憶體將會包括以下幾個記憶體,入上圖所示
| 運行時資料區 | 是否可能拋出錯誤 | 執行緒是否私有 | 是否存在GC | 生命周期 |
|---|---|---|---|---|
| 程式計數器 | × | √ | × | 執行緒 |
| 虛擬機堆疊 | √ | √ | × | 執行緒 |
| 本地方法堆疊 | √ | √ | × | 執行緒 |
| 堆 | √ | × | √ | 行程 |
| 方法區 | √ | × | √ | 行程 |
注:這里錯誤指OutOfMemoryError(無法申請到足夠記憶體)
程式計數器(Program Counter Registers)
程式計數器,或者叫做PC暫存器,程式計數器是一塊比較小的空間,可以看做是當前執行緒所執行的位元組碼的行號指示器,執行緒隔離,它的作用就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,是程式控制流的指示器,分支,回圈,跳轉,例外處理,執行緒恢復等基礎操作都需要依賴程式計數器來完成
如果執行緒正在執行一個java方法,這個計數器記錄的就是正在執行的虛擬機位元組碼指令的地址,如果正在執行的是一個本地方法,這個計數器的值應該為(UndeFined)
虛擬機堆疊(Java Virtual Machine Stack)

執行緒私有,宣告周期和執行緒一致,虛擬機描述的是java方法執行的執行緒記憶體模型,每個方法被執行時,java虛擬機都會同步創建一個堆疊幀(Stack Frame)用于存盤區域變數表,運算元堆疊,動態連接,方法出口等資訊,每一個方法被呼叫執行直到完畢程序,都會對應著一個堆疊幀在虛擬機堆疊中入堆疊出堆疊的程序,在堆疊頂的堆疊幀稱為當前堆疊幀,下面如果沒有特殊說明"堆疊",那么就是指的java虛擬機堆疊,而不是本地方法堆疊
堆疊中存盤什么
- 每一個執行緒都有自己的堆疊,堆疊中資料都是以堆疊幀(Stack Frame) 的格式存在
- 執行緒上正在執行的每個方法都對應一個堆疊幀
- 堆疊幀是一個記憶體區塊是一個資料集,維系著方法執行程序中的各種資料資訊
如果java虛擬機允許堆疊動態擴展大小,當堆疊擴展時無法申請到足夠記憶體將會拋出OOM(OutOfMemoryError)例外,如果不允許動態擴展,當執行緒請求的堆疊深度大于虛擬機允許深度將拋出StackOverflowError例外
演示StackOverflowError例外情況,代碼非常簡單
public class Demo {
public static void main(String[] args) {
main(args);
}
}
//例外
Exception in thread "main" java.lang.StackOverflowError
當main方法呼叫main方法,而被呼叫的main方法中又呼叫main方法,一直回圈下去,上面說了當一個方法被呼叫那么在堆疊中就會對應產生一個堆疊幀,當無限的堆疊幀添加到堆疊中就會導致堆疊溢位的情況
HotSpot虛擬機的堆疊容量是不可以動態擴展的,以前的Classic虛擬機倒是可以,在HotSpot虛擬機中無法演示OOM例外
設定堆疊的大小,默認大小為byte,如果想設定不同的單位只需要后面加上單位的簡寫
k=KB m=MB g=G 例如設定1024KB -Xss1024k,設定3MB -Xss3m
堆疊運行原理
- JVM直接對Java堆疊的操作只有兩個,就是對堆疊幀的壓堆疊和出堆疊,遵循"先進后出" / "后進先出"原則,
- 在一潭訓動執行緒中,一個時間點上,只會有一個活動的堆疊幀,即只有當前正在執行的方法的堆疊幀(堆疊頂堆疊幀)是有效的,這個堆疊幀被稱為當前堆疊幀(Current Frame) ,與當前堆疊幀相對應的方法就是當前方法(currentMethod) ,定義這個方法的類就是當前類(current class)
- 執行引擎運行的所有位元組碼指令只針對當前堆疊幀進行操作
- 如果在該方法中呼叫了其他方法,對應的新的堆疊幀會被創建出來,放在堆疊的頂端,成為新的當前幀,
- 不同的執行緒中所包含的堆疊幀是不允許存在相互參考的,即不可能在一個堆疊幀中參考另外一個執行緒的堆疊幀
- 如果當前方法呼叫了其他方法,方法回傳之際,當前堆疊幀會傳遞會此方法的執行結果給前一個堆疊幀,接著虛擬機會丟棄當前堆疊幀,使前一個堆疊幀重新成為當前堆疊幀
- Java方法有兩種回傳函式方式,一種正常的函式回傳,使用return指令,另一種拋出例外,不管使用哪種方式,都會導致堆疊幀被彈出
回傳方法的兩種方式拋出例外指的是沒有捕獲的例外,而不是指進行try的例外
例如方法A呼叫方法B,如果在B方法中出現例外沒有進行try,那么B方法就屬于拋出例外結束,如果在方法A中將B的例外進行了捕獲,并成功走完程式,那么A就屬于return指令結束,如果A和B都沒有進行try,那么就都屬于拋出例外結束
對于上面的方法回傳return或拋出例外,都知道方法回傳引數為void的可以不寫return,就和上面的"return指令或拋出例外結束"沖突,但是如果查看位元組碼的話,就會發現即使不寫return,在位元組碼最后也會有一條return指令
區域變數表(Local Variables Table)
區域變數表是一組變數值的存盤空間,定義為一個數字陣列,主要存盤方法引數和方法內部定義的區域變數,這些資料型別包括基本資料型別,物件參考(reference)和returnAddress型別,由于區域變數表是建立在執行緒的堆疊上的,屬于執行緒的私有資料,所有不存在資料的安全問題(多執行緒并發情況),而區域變數表的大小在java編譯為Class檔案時,就在方法的Code屬性的max_locals資料項中確定了該方法所需分配的區域變數表的最大容量
- 方法嵌套呼叫次數由堆疊的大小決定,一般來說堆疊越大,方法嵌套呼叫次數越多,對一個函式而言,它的引數和區域變數越多,使得區域變數表越大,它的堆疊幀就越大.以滿足方法呼叫所需傳遞的資訊增大的需求,進行函式呼叫就會占用更多的堆疊空間,導致其嵌套次數就會減少
- 區域變數表的變數只在當前方法呼叫中有效,在方法執行時,虛擬機通過使用區域變數表完成引數值到引數串列的傳遞程序,方法呼叫接收后,隨著方法堆疊幀的銷毀,區域變數表也會隨之銷毀
- 如果一個變數未賦值,那么它是不會出現在該堆疊幀的區域變數表中的,因為是無用功
Slot
-
在區域變數表中,最基本的存盤單元是Slot(變數槽)
-
引數值的存放總是在區域變數陣列的index0開始,到陣列長度-1的索引結束
-
區域變數表中存放編譯期間可知的各種基本資料型別(8種),參考資料型別(reference),returnAddress 型別的變數
-
在區域變數表里,32位以內的型別只占用一個slot (包括returnAddress型別),64位的型別(long和double)占用兩個slot
- byte,short,char 在存盤前被轉換為int boolean 也被轉換為int 0代表false 非0 表示true
- long 和double 則占用兩個slot

- JVM會為區域變數變中每一個Slot都分配一個訪問索引,通過這個索引即可成功訪問到區域變數表中指定的區域變數值
- 當一個實體方法被呼叫時,它的方法引數和方法體內部定義的區域變數會按照順序復制到區域變數表中的每一個Slot上
- 如果需要訪問區域變數表中的一個64bit的區域變數值時,只需要使用前一個索引即可(例如訪問long或double型別)
- 如果當前幀是由構造方法或者實體方法(非靜態方法)創建的,那么該物件參考this將會放在index為0的solot處,其余引數參照表順序繼續排列
Slot重復利用
為了盡可能節省堆疊幀耗用的記憶體空間,區域變數表中的槽是可以重復利用的,方法中定義了變數,其作用域不一定會覆寫整個方法,在作用域結束后對應的變數槽就可以交給其他變數使用
例如下面這個代碼
public static void main(String[] args) {
int a = 2;
{
int b = 3;
}
int c = 10;
}
按理來說槽的最大深度應該是4,一個形參args,三個int型別變數,我們通過idea插件jclasslib來查看一下

發現最大槽數為3,也就是說,int b=3;這段代碼直到執行完,走出代碼塊{}后,它的作用域就消失了,而它的位置剩出一個槽,于是int c=10;這段代碼中的c放到了b空出的位置,我們去掉代碼塊在來看一下

發現去掉代碼塊{}后槽數變為4了,因為直到方法結束,沒有能重復利用的槽位
運算元堆疊(Operand Stack)
-
每一個獨立的堆疊幀中除了包含區域變數表以外,還包含一個后進先出(Last-In-First-Out)的運算元堆疊,也可以稱之為運算式堆疊(Expression stack)
-
運算元堆疊,在方法執行程序中,根據位元組碼指令,往堆疊中寫入資料或提取資料,即入堆疊(push) /出堆疊(pop),
- 某些位元組碼指令將值壓入運算元堆疊,其余的位元組碼指令將運算元取出堆疊,使用它們后再把結果壓入堆疊
- 比如:執行復制、交換、求和等操作
-
如果被呼叫的方法帶有回傳值的話,其回傳值將會被壓入當前堆疊幀的運算元堆疊中,并更新PC暫存器中下一條需要執行的位元組碼指令,
-
運算元堆疊中元素的資料型別必須與位元組碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行驗證,同時在類加載程序中的類檢驗階段的資料流分析階段要再次驗證,
-
另外,我們說Java虛擬機的解釋引擎是基于堆疊的執行引擎,其中的堆疊指的就是運算元堆疊,
-
運算元堆疊,主要用于保存計算程序的中間結果,同時作為計算程序中變數臨時的存盤空間,
-
運算元堆疊就是JVM執行引擎的一個作業區,當一個方法剛開始執行的時候,個新的堆疊幀也會隨之被創建出來,這個方法的運算元堆疊是空的,
-
每一個運算元堆疊都會擁有一個明確的堆疊深度用于存盤數值,其所需的最大深度在編譯期就定義好了,保存在方法的code屬性中,為max stack的值,
-
堆疊中的任何一個元素都是可以任意的Java資料型別,
- 32bit的型別占用一個堆疊單位深度
- 64bit的型別占用兩個堆疊單位深度
-
運算元堆疊并非采用訪問索引的方式來進行資料訪問的,而是只能通過標準的入堆疊(push)和出堆疊(pop)操作來完成一次資料訪問,
-
如果被呼叫的方法帶有回傳值的話,其回傳值將會被壓入當前堆疊幀的運算元堆疊中,并更新PC暫存器中下一條需要執行的位元組碼指令,
-
運算元堆疊中元素的資料型別必須與位元組碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行驗證,同時在類加載程序中的類檢驗階段的資料流分析階段要再次驗證,
-
另外,我們說Java虛擬機的解釋引擎是基于堆疊的執行引擎,其中的堆疊指的就是運算元堆疊,
-
向運算元堆疊添加int型別時,根據數值的大小通過不同會根據不同的添加方式壓入運算元堆疊
- bipush,sipush
java代碼
public void testAddOperation() {
byte i = 15;
int j = 8;
int k = i + j;
}
- 將15 放到運算元堆疊中

- 將運算元堆疊中的15放入區域變數表下標1的位置,因為不是靜態方法,下標0的位置存放了this,所以從1開始存放

- 將8放入運算元堆疊

- 將運算元堆疊中的8放入區域變數表中的下標2位置

5.取出區域變數表中下標1的資料放入運算元堆疊中,當前15就是堆疊頂

6.將區域變數表中的下標2的資料8放入運算元堆疊,當前8就是堆疊頂

- 將運算元堆疊中的兩個資料出堆疊,通過執行引擎進行和的操作,然后在將結果放入運算元堆疊

- 將運算元堆疊中得23放入區域變數表中的3下標位置,然后return,堆疊幀結束

動態連接
每一個堆疊幀內部都包含一個指向運行時常量池中該堆疊幀所屬方法的參考,包含這個參考的目的就是為了支持當前方法的代碼能夠實作動態鏈接(Dynamic Linking) ,比如: invokedynamic指令
在Java源檔案被編譯到位元組碼檔案中時,所有的變數和方法參考都作為符號參考(symbolic Reference)保存在class檔案的常量池里,在類加載初始化時,一部分符號參考就被轉換為直接參考,這些轉化被稱為靜態決議,而另一部分將在運行期間轉化為直接參考,這些就叫做動態連接
比如:描述一個方法呼叫了另外的其他方法時,就是通過常量池中指向方法的符號參考來表示的,那么動態鏈接的作用就是為了將這些符號參考轉換為呼叫方法的直接參考,

方法呼叫
方法呼叫并不等同于方法中的代碼被執行,方法呼叫的唯一任務就是確定被呼叫方法的版本(即呼叫哪一個方法)
決議呼叫
所有方法呼叫的目標方法在Class檔案里面都是一個常量池的符號應參考,在類加載的決議階段,會將其中一部分的符號參考轉化為直接參考,這種決議能夠成立的前提是:方法在程式真正運行之前就有一個可以確定的呼叫版本,并且這個方法的呼叫版本在運行期是不可改變的,換句話說,呼叫目標在程式代碼中寫好,編譯器進行編譯那一刻就已經確定下來了,這類方法的呼叫稱為決議(Resolution)
在java中符合"編譯器可知,運行期不變"這個方法的要求主要有靜態方法個私有方法兩大類,前者與型別直接關聯,后置在外部不可訪問,這兩種方法的特點決定它們都不可能通過繼承或別的方式重寫出其他版本,因此它們都適合在類加載階段進行決議
呼叫不同的型別的方法,位元組碼指令集中設計了不同的指令
- invokestatic 呼叫靜態方法
- invokespecial 呼叫實體構造器init()方法,私有方法,父類中的方法
- invokevirtual 呼叫所有虛方法
- invokeinterface 呼叫介面方法,運行時再確定一個實作該介面的物件
- invokedynamic 先在運行時動態決議出呼叫點限定符所參考的方法,然后再執行該方法,前面4條呼叫指令,分派邏輯都是固化在java虛擬機內部,而invokedynamic指令的分派邏輯是由用戶設定的引導方法來決定的
只要能被invokestatic,invokespecial指令呼叫的方法,都可以在決議階段中確定唯一的呼叫版本,java語言中符合這個條件的方法有4種,加上final修飾的方法(final方法使用invokevirtual指令呼叫),總共5種方法,這5種方法呼叫會在類加載的時候就可以把符號參考轉化為直接參考,這些方法統稱為非虛方法(Non-Virtual Method)
- 靜態方法
- 私有方法
- 實體構造器
- 父類方法
- final修飾的方法
小例子
public class StaticResolution {
public static void sayHello(){
System.out.println("hello world");
}
public static void main(String[] args) {
StaticResolution.sayHello();
}
}
可以使用javap -v 類名.class 進行反編譯來查看位元組碼指令
也可以使用IDEA插件jclasslib來查看
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #5 // Method sayHello:()V
3: return
可以看到使用invokestatic指令來呼叫sayHello方法,非虛方法除了使用invokestatic和invokespecial呼叫以外,被final修飾的方法將會使用invokevirtual來呼叫
分派呼叫
可以是靜態呼叫也可以是動態呼叫,依據分派的宗量數有可以分為單分派和多分派,這兩類分派的組合就構成靜態單分派,動態單分派,靜態多分派,動態多分派,分派和java虛擬機實作多載和重寫有很大的聯系
靜態分派
先來看一個小例子
public class Assign {
static abstract class Human {
}
static class Man extends Human {
}
static class WoMan extends Human {
}
public void sayHello(Human human) {
System.out.println("hello human!");
}
public void sayHello(Man man) {
System.out.println("hello man!");
}
public void sayHello(WoMan woMan) {
System.out.println("hello woMan!");
}
public static void main(String[] args) {
Assign assign = new Assign();
Human human1=new Man();
Human human2=new WoMan();
assign.sayHello(human1);
assign.sayHello(human2);
}
}
//運行結果
hello human!
hello human!
那么為什么虛擬機會選擇執行引數為Human的多載版本呢?再解決這個問題前先來了解兩個概念
Human man = new Man();
我們把上面"Human"稱為"靜態變數"(Static Type) 或者稱為"外觀型別"(Apparent Type),而后面的"Man"稱為"實際型別"(Actual Type)或者叫"運行時型別"(Runtime Type)
靜態型別和實際型別在程式中都有可能發生變化,區別是靜態型別的變化僅僅在使用時發生,變數本身的靜態型別不會發生改變,并且最終的靜態型別在編譯期間是可知的; 而實際型別變化的結果在運行期間才可以確定,編譯器在編譯程式時并不知道物件的實際型別是什么,例如下面的例子
//實際型別變化
Human human = (args == null) ? new WoMan() : new Man();
//靜態型別變化
assign.sayHello((Man) human1);
assign.sayHello((WoMan) human1);
實際型別變化:先看上面的一個Human賦值操作,這時human變數的靜態型別已經確定,就是Human型別,而它的實際型別賦值是一個三元運算子,只有在程式運行中判斷是和否來進行賦值WoMan型別或Man型別,這種已經確定靜態型別,不確定實際型別的就叫做實際型別變化
靜態型別變化:下面兩個強轉的變數是已經確定它們的靜態型別就是Human,進行強轉后也可以明確知道它們的型別,這種就叫做靜態型別變化
明確了這兩個概念再回到上面的例子,在明確方法接收者是物件assign的前提下,使用哪個多載版本就完全取決于傳入引數數量和型別,代碼中故意定義了兩個靜態型別相同,實際型別不同的變數,但是虛擬機(準確的說是編譯器)在多載時是通過引數的靜態型別而不是實際型別作為判斷依據的,由于靜態型別在編譯期間可知,所以在編譯階段,javac編譯器就根據引數的靜態型別決定了會使用哪個多載版本,因此選擇了sayHello(Human)作為呼叫目標,并把這個方法的符號參考寫到main()方法里的兩條invokevirtual指令引數中
所有依賴靜態型別來決定方法執行版本的分派動作,都成為靜態分派,其中最經典的應用表現就是方法的多載,靜態分派是發生在編譯期間,而不是由虛擬機來執行的
需要注意的是javac編譯器雖然能確定方法的多載版本,但很多情況下并不是唯一的,往往只能確定一個相對合適的版本,例如下面例子
public class OverLoad {
public void get(int num){
System.out.println("int");
}
public void get(long num){
System.out.println("long");
}
public void get(char num){
System.out.println("char");
}
public void get(int... num){
System.out.println("int...");
}
}
public static void main(String[] args) {
OverLoad load = new OverLoad();
load.get('1');
}
當我們直接執行,輸出char,也就是旋轉型別為char的多載版本,將char引數的多載方法注釋后繼續執行,又選擇了int,注釋int引數的方法后選擇了long,繼續注釋又選擇了int可變長引數
還有一點可能比較混淆,前面決議和分派這兩者之間關系并不是二選一的排他關系,它們是在不同層次上去篩選,確定目標方法的程序,例如靜態方法會在編譯器確定,在類加載進行決議,而靜態方法顯然也可以有多載版本,選擇多載版本程序就是在靜態分派完成的
動態分派
動態分派和java實作重寫有密切的關聯,來看例子
public class DynamicDispatch {
static abstract class Human{
public abstract void sayHello();
}
static class Man extends Human{
@Override
public void sayHello() {
System.out.println("man");
}
}
static class WoMan extends Human{
@Override
public void sayHello() {
System.out.println("woman");
}
}
public static void main(String[] args) {
Human man=new Man();
Human woman=new WoMan();
man.sayHello();
woman.sayHello();
man=new WoMan();
man.sayHello();
}
}
//結果
man
woman
woman
顯然這里選擇呼叫方法版本不可能是再根據靜態型別來決定的,因為靜態變數相同都是Human的兩個變數man和woman都呼叫sayHello()方法卻產生了不同的行為,甚至man在兩次呼叫中還執行了兩個不同的方法,導致這個現象原因很明顯,因為這兩個變數的實際型別不同,java虛擬機是如何根據實際型別來分派方法執行的版本呢?我們使用javap命令輸出這段代碼的位元組碼
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class DynamicDispatch$Man
3: dup
4: invokespecial #3 // Method DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #4 // class DynamicDispatch$WoMan
11: dup
12: invokespecial #5 // Method DynamicDispatch$WoMan."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method DynamicDispatch$Human.sayHello:()V
24: new #4 // class DynamicDispatch$WoMan
27: dup
28: invokespecial #5 // Method DynamicDispatch$WoMan."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6 // Method DynamicDispatch$Human.sayHello:()V
36: return
其中16-21行是關鍵的部分,aload指令分別把兩個創建的物件參考壓到堆疊頂,這兩個物件將要執行sayHello()方法的所有者,稱為接收者,17-21行是呼叫的指令,這兩條指令從位元組碼指令來看,無論是指令(invokevirtual)還是引數(都指向常量池中Human.sayHello的符號參考)都一模一樣,但是這兩條執行的執行目標方法卻不相同,具體來看一下invokevirtual指令的運行時決議程序大致分為幾步
- 找到運算元堆疊堆疊頂的第一個元素指向的物件的實際型別,記作C
- 如果在型別C中找到與常量池中描述符和簡單名稱都相符的方法,則進行訪問呢權限效驗,如果通過回傳這個物件的直接參考,否則回傳java.lang.IllegalAccessError例外
- 否則按照繼承關系從下往上依次對C的各個父類進行第二步的搜索和驗證程序
- 如果始終沒有找到,則拋出java.lang.AbstractMethodError例外
正是因為invokevirtual指令第一步就是在運行期確定接收者的實際型別,所有兩次呼叫中的invokevirtual指令并不是把常量池中的符號參考直接決議到直接參考上就結束了,還會根據方法接收者的實際型別來選擇方法版本,這個程序就是java重寫的本質,把這種在運行期間根據實際型別確定方法執行版本的分派程序稱為動態分派
既然多型的根源就在于虛方法呼叫指令的invokevirtual的指令邏輯,那么自然得出的結論就是只對方法有效,對欄位無效,欄位永遠不會參與多型,那個類的方法訪問某個名字的欄位時,該名字指的就是這個類能看到的欄位,當子類宣告了和父類的同名欄位,雖然在子類記憶體中都會出現,但是子類欄位會掩蔽父類的同名欄位,看下面例子
public class Demo {
static class Father {
public int i = 1;
public Father(){
i=2;
showI();
}
public void showI() {
System.out.println("father"+i);
}
}
static class Son extends Father {
public int i = 3;
public Son(){
i=4;
showI();
}
public void showI() {
System.out.println("son"+i);
}
}
public static void main(String[] args) {
Father p=new Son();
System.out.println(p.i);
}
}
//結果
son0
son4
2
輸出兩句都為son,因為son類創建時,隱式呼叫父類構造,而父類構造中showI()是一個虛方法,也就是使用invokevirtual指令來執行,上面寫過invokervirtual的執行程序,那么就呼叫了son類的showI()方法,而這時son類還沒有進行初始化,int i還是為0,所以第一次輸出son0,當父類構造完成后回到子類,子類進行i=4賦值操作,然后呼叫showI(),第二次顯示為son4,最后一句通過靜態型別訪問到父類中的i為2
單分派和多分派
方法的接收者與方法的引數統稱為方法的宗量
public class Demo {
static class Open {
}
static class Book {
}
public static class Father {
public void show(Open open) {
System.out.println("father open");
}
public void show(Book book) {
System.out.println("father book");
}
}
public static class Son extends Father {
public void show(Open open) {
System.out.println("Son open");
}
public void show(Book book) {
System.out.println("Son book");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.show(new Open());
son.show(new Book());
}
}
//結果
father book
Son book
在main方法中呼叫了兩次show方法,首先要關注的是編譯器的選擇程序,也就是靜態分派的程序,選擇目標的依據有兩點,一:靜態型別是Father還是Son,二方法引數是Book還是Open,這次選擇最終產生兩條invokevirtual指令,而這兩條指令指向Father.show(Open)方法和Father.show(Book)方法,因為是根據兩個宗量進行選擇,所以java語言的靜態分派屬于多分派型別
16: aload_1
17: new #6 // class Demo$Open
20: dup
21: invokespecial #7 // Method Demo$Open."<init>":()V
24: invokevirtual #8 // Method Demo$Father.show:(LDemo$Open;)V
27: aload_2
28: new #9 // class Demo$Book
31: dup
32: invokespecial #10 // Method Demo$Book."<init>":()V
35: invokevirtual #11 // Method Demo$Father.show:(LDemo$Book;)V
再來看運行階段,在執行invokevirtual指令是已經確定目標方法的簽名為show(Open),唯一影響虛擬機選擇的就是接收者的實體是son還是Father,只有一個影響宗量,所有動態分派屬于單分派型別
動態分派是執行非常繁瑣的動作,而且動態分派的方法版本選擇程序需要運行時再接收者型別的方法元資料中搜索合適的方法,因此java虛擬機實作基于性能考慮,真正運行時一般不會如此反復的搜索型別元資料,而是建立一個虛方法表(Virtual Method Table),使用虛方法表索引來替代元資料查找以提高性能
虛方法表中存存放著各個方法的實際入口,如果某個方法在子類中沒重寫,那么子類的虛方法表中的地址入口和父類相同的方法的地址入口是相同的,都指向父類的實作入口,如果子類中重寫了這個方法,子類虛方法表中的地址就會被替換為指向子類實作版本的入口地址
方法回傳地址
一個方法退出有兩種方式,一是遇到沒有進行處理的例外,二是執行引擎執行到return位元組碼指令,這時候可能有回傳值傳遞給上層呼叫者(呼叫當前方法的方法稱為呼叫者或主調方法),方法是否有回傳值以及回傳值的型別根據遇到何種回傳指令來決定的,如果是遇到沒有處理的例外進行退出的,這種退出稱為例外呼叫完成,方法使用例外呼叫完成退出是不會給它的上層呼叫者提供任何回傳值的
無論采用那種方式退出,在方法退出后,都必須回傳到最初方法被呼叫的位置,程式才能繼續執行,方法回傳時可能需要在堆疊幀中保存一些資訊,來幫助恢復它的上層主調方法的執行狀態,一般來說,方法正常退出時,主調方法的PC計數器就可以作為方法回傳地址,堆疊幀中很可能就會保存這個計數器的值,而方法例外退出時,方法的回傳地址是要通過例外處理器表來確定的,堆疊幀中就一般不會保存這部分資訊
方法的退出的程序實際上等同于把當前堆疊幀出堆疊,因此退出時可能執行的操作有:恢復上層方法的區域變數表和運算元堆疊,把回傳值(如果有的話)壓入呼叫者的運算元堆疊中,調整PC計數器的值以指向方法呼叫指令的后面一條指令等
一些附加資訊
java虛擬機規范允許虛擬機實作增加一些規范里沒有描述的資訊到堆疊幀中,例如與除錯,性能收集相關的資訊,這部分資訊完全取決于具體的虛擬機實作
本地方法
簡單地講,一個Native Method就是一個Java呼叫非Java代碼的介面,一個Native Method是這樣一個Java方法:該方法的實作由非Java語言實作,比如c,這個特征并非Java所特有,很多其它的編程語言都有這一機制,比如在C++中你可以用extern "c"告知C++編譯器去呼叫一個c的函式,
在定義一個native method時,并不提供實作體(有些像定義一個Javainterface) ,因為其實作體是由非java語言在外面實作的,
本地介面的作用是融合不同的編程語言為Java所用,它的初衷是融合c/C++程式,
-
Java虛擬機堆疊用于管理Java方法的呼叫,而本地方法堆疊用于管理本地方法的呼叫,
-
本地方法堆疊,也是執行緒私有的,
-
允許被實作成固定或者是可動態擴展的記憶體大小,(在記憶體溢位方面是相同的)
- 如果執行緒請求分配的堆疊容量超過本地方法堆疊允許的最大容量, Java虛擬機將會拋出一個stackoverflowError例外,
- 如果本地方法堆疊可以動態擴展,并且在嘗試擴展的時候無法申請到足夠的記憶體,或者在創建新的執行緒時沒有足夠的記憶體去創建對應的本地方法堆疊,那么Java虛擬機將會拋出一個outofMemoryError例外,
-
本地方法是使用c語言實作的,
-
它的具體做法是Native Method stack中登記native方法,在Execution Engine執行時加載本地方法庫
- 當某個執行緒呼叫一個本地方法時,它就進入了一個全新的并且不再受虛擬機限制的世界,它和虛擬機擁有同樣的權限,
- 本地方法可以通過本地方法介面來訪問虛擬機內部的運行時資料區,
- 它甚至可以直接使用本地處理器中的暫存器
- 直接從本地記憶體的堆中分配任意數量的記憶體
-
并不是所有的JVM都支持本地方法,因為Java虛擬機規范并沒有明確要求本地方法堆疊的使用語言、具體實作方式、資料結構等,如果JVM產品不打算支持native方法,也可以無需實作本地方法堆疊,
-
在Hotspot JVM中,直接將本地方法堆疊和虛擬機堆疊合二為一,
本文僅個人理解,如果有不對的地方歡迎評論指出或私信,謝謝?(?>?<?)?
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/254302.html
標籤:Java
上一篇:設計模式之享元模式
下一篇:Java是參考傳遞還是值傳遞?
