目錄
- 1、基本介紹
- 2、位元組碼
- 2.1、jclasslib插件
- 2.2、助記符
- 3、堆疊的存盤單位--堆疊幀
- 3.1、堆疊幀內部結構
- 3.2、區域變數表
- 1. 區域變數表理解
- 2. slot理解
- 3. slot重復利用
- 4. 靜態變數與區域變數
- 5. 補充說明
- 3.3、運算元堆疊
- 1. 運算元堆疊特點
- 2. 運算元堆疊代碼追蹤
- 3.4、動態鏈接
- 1. 理解:
- 2. 方法的呼叫
- 3. 虛方法和非虛方法
- 4. 方法呼叫指令
- 5.虛方法表
- 3.5、方法回傳地址
- 3.6、附加資訊
- 4、相關面試題
1、基本介紹
記憶體中的堆和堆疊:


? 注:區域變數是指8中資料型別以及物件的參考地址,
JVM直接對java堆疊的操作:
- 每個方法執行,伴隨著入堆疊/壓堆疊
- 執行結束后的出堆疊作業
對于堆疊不存在垃圾回收問題
java堆疊的大小是動態的或者固定不變的
- 修改堆疊大小
2、位元組碼
2.1、jclasslib插件



2.2、助記符
-
ldc:將int,float,String型別的常量值從常量池中推送至堆疊頂(LDC類)
-
bipush:表示將單位元組(-128~127)的常量值推送至堆疊頂
-
sipush:表示將短整型常量值(-32768~32767)推送至堆疊頂(2^15=32768)
-
iconst_1:表示將int型別的1推送至堆疊頂(iconst_0~iconst_5同樣適用)
-
iconst_m1:表示將int型別的-1推送至堆疊頂(詳見ICONST類)
-
anewarray:表示創建一個參考型(類、介面)的陣列,推到堆疊頂
-
newarray:表示創建一個原始型別(int,float...)的陣列,推到堆疊頂
3、堆疊的存盤單位--堆疊幀
- 每個執行緒都有自己的堆疊,資料在堆疊中的存盤單位是堆疊幀
- 每個方法對應一個堆疊幀
- 堆疊幀是一個記憶體區塊,是一個資料集
- 執行緒之間堆疊不能共享

方法結束的三種方式:
- 正常回傳
- 拋出例外(未捕獲例外)結束
不管哪種方法結束,到會導致堆疊幀被彈出
? 存在呼叫關系時,當方法回傳時,當前堆疊幀會傳遞回傳結果給前一個堆疊幀,接著虛擬機拋棄當前堆疊幀,使得前一個堆疊幀未新的當前堆疊幀,
3.1、堆疊幀內部結構

3.2、區域變數表
1. 區域變數表理解


2. slot理解

1.引數值的存放總是在區域變數陣列的index0開始,到陣列長度-1的索引結束
2.區域變數表,最基本的存盤單元是Slot(變數槽)
3.區域變數表中存放編譯期可知的各種基本資料型別(8種),參考型別(reference),returnAddress型別的變數,
4.在區域變數表里,32位以內的型別只占用一個slot(包括returnAddress型別),64位的型別(long和double)占用兩個slot,
byte、short、char、float在存盤前被轉換為int,boolean也被轉換為int,0表示false,非0表示true;
long和double則占據兩個slot,
5.JVM會為區域變數表中的每一個slot都分配一個訪問索引,通過這個索引即可成功訪問到區域變數表中指定的區域變數值
6.當一個實體方法被呼叫的時候,它的方法引數和方法體內部定義的區域變數將會按照宣告順序被復制到區域變數表中的每一個slot上
7.如果需要訪問區域變數表中一個64bit的區域變數值時,只需要使用前一個索引即可,(比如:訪問long或者double型別變數)
8.如果當前幀是由構造方法或者實體方法創建的(意思是當前幀所對應的方法是構造器方法或者是普通的實體方法),那么該物件參考this將會存放在index為0的slot處,其余的引數按照引數表順序排列,
9.靜態方法中不能參考this,是因為靜態方法所對應的堆疊幀當中的區域變數表中不存在this
3. slot重復利用
堆疊幀中的區域變數表中的槽位是可以重復利用的,如果一個區域變數過了其作用域,那么在其作用域之后申明的新的區域變數就很有可能會復用過期區域變數的槽位,從而達到節省資源的目的,

4. 靜態變數與區域變數
變數的分類:
- 按照資料型別分:
- ①基本資料型別;
- ②參考資料型別;
- 按照在類中宣告的位置分:
- ①成員變數:在使用前,都經歷過默認初始化賦值
- static修飾:類變數:類加載鏈接的準備preparation階段給類變數默認賦0值——>初始化階段initialization給類變數顯式賦值即靜態代碼塊賦值;
- 不被static修飾:實體變數:隨著物件的創建,會在堆空間分配實體變數空間,并進行默認賦值
- ②區域變數:在使用前,必須要進行顯式賦值的!否則,編譯不通過
- ①成員變數:在使用前,都經歷過默認初始化賦值
5. 補充說明
- 在堆疊幀中,與性能調優關系最為密切的部分就是區域變數表,在方法執行時,虛擬機使用區域變數表完成方法的傳遞
- 區域變數表中的變數也是重要的垃圾回收根節點,只要被區域變數表中直接或間接參考的物件都不會被回收
3.3、運算元堆疊
1.堆疊 :可以使用陣列或者鏈表來實作
2.每一個獨立的堆疊幀中除了包含區域變數表以外,還包含一個后進先出的運算元堆疊,也可以成為運算式堆疊
3.運算元堆疊,在方法執行程序中,根據位元組碼指令,往堆疊中寫入資料或提取資料,即入堆疊(push)或出堆疊(pop)
某些位元組碼指令將值壓入運算元堆疊,其余的位元組碼指令將運算元取出堆疊,使用他們后再把結果壓入堆疊,(如位元組碼指令bipush操作)
比如:執行復制、交換、求和等操作
代碼舉例

1. 運算元堆疊特點
- 運算元堆疊,主要用于保存計算程序的中間結果,同時作為計算程序中變數臨時的存盤空間,
- 運算元堆疊就是jvm執行引擎的一個作業區,當一個方法開始執行的時候,一個新的堆疊幀也會隨之被創建出來,這個方法的運算元堆疊是空的
- 每一個運算元堆疊都會擁有一個明確的堆疊深度用于存盤數值,其所需的最大深度在編譯器就定義好了,保存在方法的code屬性中,為max_stack的值,
- 堆疊中的任何一個元素都是可以任意的java資料型別
- 32bit的型別占用一個堆疊單位深度
- 64bit的型別占用兩個堆疊深度單位
- 運算元堆疊并非采用訪問索引的方式來進行資料訪問的,而是只能通過標準的入堆疊push和出堆疊pop操作來完成一次資料訪問
- 如果被呼叫的方法帶有回傳值的話,其回傳值將會被壓入當前堆疊幀的運算元堆疊中,并更新PC暫存器中下一條需要執行的位元組碼指令,
- 運算元堆疊中的元素的資料型別必須與位元組碼指令的序列嚴格匹配,這由編譯器在編譯期間進行驗證,同時在類加載程序中的類驗證階段的資料流分析階段要再次驗證,
- 另外,我們說Java虛擬機的解釋引擎是基于堆疊的執行引擎,其中的堆疊指的就是運算元堆疊,
2. 運算元堆疊代碼追蹤
結合上圖結合下面的圖來看一下一個方法(堆疊幀)的執行程序
①15入堆疊;②存盤15,15進入區域變數表
注意:區域變數表的0號位被構造器占用,這里的15從區域變數表1號開始

③壓入8;④8出堆疊,存盤8進入區域變數表;

⑤從區域變數表中把索引為1和2的是資料取出來,放到運算元堆疊;⑥iadd相加操作

⑦iadd操作結果23出堆疊⑧將23存盤在區域變數表索引為3的位置上istore_3

3.4、動態鏈接
1. 理解:
執行運行時常量池的方法參考
1、每一個堆疊幀內部都包含一個指向運行時常量池Constant pool或該堆疊幀所屬方法的參考,包含這個參考的目的就是為了支持當前方法的代碼能夠實作動態鏈接,比如invokedynamic指令
2、在Java源檔案被編譯成位元組碼檔案中時,所有的變數和方法參考都作為符號參考(symbolic Refenrence)保存在class位元組碼檔案(javap反編譯查看)的常量池里,比如:描述一個方法呼叫了另外的其他方法時,就是通過常量池中指向方法的符號參考來表示的,那么動態鏈接的作用就是為了將這些符號參考(#)最終轉換為呼叫方法的直接參考,
3、常量池的作用:提供一些符號和常量便于指令的識別,
2. 方法的呼叫
在JVM中,將符號參考轉換為呼叫方法的直接參考與方法的系結機制相關
- 靜態鏈接
當一個 位元組碼檔案被裝載進JVM內部時,如果被呼叫的目標方法在編譯期可知,且運行期保持不變時,這種情況下將呼叫方法的符號參考轉換為直接參考的程序稱之為靜態鏈接, - 動態鏈接
如果被呼叫的方法在編譯期無法被確定下來,也就是說,只能夠在程式運行期將呼叫方法的符號參考轉換為直接參考,由于這種參考轉換程序具備動態性,因此也就被稱之為動態鏈接,
對應的方法的系結機制為:早起系結(Early Binding)和晚期系結(Late Bingding),系結是一個欄位、方法或者類在符號參考被替換為直接參考的程序,這僅僅發生一次,
- 早期系結
早期系結就是指被呼叫的目標方法如果在編譯期可知,且運行期保持不變時,即可將這個方法與所屬的型別進行系結,這樣一來,由于明確了被呼叫的目標方法究竟是哪一個,因此也就可以使用靜態鏈接的方式將符號參考轉換為直接參考, - 晚期系結
如果被呼叫的方法在編譯期無法被確定下來,只能夠在程式運行期根據實際的型別系結相關的方法,這種系結方式也就被稱之為晚期系結,
隨著高級語言的橫空出世,類似于java一樣的基于面向物件的編程語言如今越來越多,盡管這類編程語言在語法風格上存在一定的差別,但是它們彼此之間始終保持著一個共性,那就是都支持封裝,集成和多型等面向物件特性,既然這一類的編程語言具備多型特性,那么自然也就具備早期系結和晚期系結兩種系結方式,
Java中任何一個普通的方法其實都具備虛函式的特征,它們相當于C++語言中的虛函式(C++中則需要使用關鍵字virtual來顯式定義),如果在Java程式中不希望某個方法擁有虛函式的特征時,則可以使用關鍵字final來標記這個方法,
3. 虛方法和非虛方法
子類物件的多型性使用前提:實際開發撰寫代碼中用的介面,實際執行是匯入的的三方jar包已經實作的功能
①類的繼承關系(父類的宣告)②方法的重寫(子類的實作)
非虛方法
-
如果方法在編譯器就確定了具體的呼叫版本,這個版本在運行時是不可變的,這樣的方法稱為非虛方法
-
靜態方法、私有方法、final方法、實體構造方法(實體已經確定,this()表示本類的構造器)、父類方法(super呼叫)都是非虛方法
-
其他所有體現多型特性的方法稱為虛方法
4. 方法呼叫指令
普通呼叫指令:
1、invokestatic:呼叫靜態方法,決議階段確定唯一方法版本;
2、invokespecial:呼叫
3、invokevirtual:呼叫所有虛方法;
4、invokeinterface:呼叫介面方法;
動態呼叫指令(Java7新增):
5、invokedynamic:動態決議出需要呼叫的方法,然后執行 .
前四條指令固化在虛擬機內部,方法的呼叫執行不可人為干預,而invokedynamic指令則支持由用戶確定方法版本,
其中invokestatic指令和invokespecial指令呼叫的方法稱為非虛方法
其中invokevirtual(final修飾的除外,JVM會把final方法呼叫也歸為invokevirtual指令,但要注意final方法呼叫不是虛方法)、invokeinterface指令呼叫的方法稱稱為虛方法,
package com.lx;
/**
* 決議呼叫中非虛方法、虛方法的測驗
*/
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() {
//invokespecial--->父類構造方法
super();
}
public Son(int age) {
this();
}
public static void main(String[] args) {
//invokespecial-->構造方法
Son son = new Son();
//invokevirtual--->其它方法
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(" 兒子");
//invokespecial--->私有方法
showPrivate(" hello!");
/*invokevirtual --->final方法
因為此方法宣告有final 不能被子類重寫,所以也認為該方法是非虛方法
JVM會把final方法呼叫也歸為invokevirtual指令*/
showFinal();
//invokespecial---->父類方法
super.showFinal();
//invokespecial--->父類方法
super.showCommon();
/*虛方法如下*/
//invokevirtual
showCommon();//沒有顯式加super,被認為是虛方法,因為無法去頂子類是否重寫了showCommon
info();
MethodInterface in = null;
//invokeinterface 不確定介面實作類是哪一個 需要重寫
in.methodA();
}
public void info() {
}
}
interface MethodInterface {
void methodA();
}
5.虛方法表
-
在面向物件編程中,會很頻繁期使用到動態分派,如果在每次動態分派的程序中都要重新在累的方法元資料中搜索合適的目標的話就可能影響到執行效率,因此,為了提高性能,jvm采用在類的方法區建立一個虛方法表(virtual method table)(非虛方法不會出現在表中)來實作,使用索引表來代替查找,
-
每個類中都有一個虛方法表,表中存放著各個方法的實際入口,
-
虛方法表什么時候被創建?
虛方法表會在類加載的鏈接階段被創建 并開始初始化,類的變數初始值準備完成之后,jvm會把該類的虛方法表也初始化完畢,

3.5、方法回傳地址
- 存放呼叫該方法的PC暫存器的值,
- 一個方法的結束,有兩種方式:
- 正常執行完成
- 出現未處理的例外,非正常退出
- 無論通過哪種方式退出,在方法退出后都回傳到該方法被呼叫的位置,方法正常退出時,呼叫者(方法的呼叫者可能也是一個方法)的pc計數器的值作為回傳地址,即呼叫該方法的指令的下一條指令的地址,而通過例外退出時,回傳地址是要通過例外表來確定,堆疊幀中一般不會保存這部分資訊,
- 本質上,方法的退出就是當前堆疊幀出堆疊的程序,此時,需要恢復上層方法的區域變數表、運算元堆疊、將回傳值入呼叫者堆疊幀的運算元堆疊、設定PC暫存器值等,讓呼叫者方法繼續執行下去,
- 正常完成出口和例外完成出口的區別在于:通過例外完成出口退出的不會給他的上層呼叫者產生任何的回傳值,
當一個方法開始執行后,只有兩種方式可以退出這個方法
1、執行引擎遇到任意一個方法回傳的位元組碼指令(return),會有回傳值傳遞給上層的方法呼叫者,簡稱正常完成出口;
- 一個方法在正常呼叫完成之后究竟需要使用哪一個回傳指令還需要根據方法回傳值的實際資料型別而定
- 在位元組碼指令中,回傳指令包含ireturn(當回傳值是boolena、byte、char、short和int型別時使用)、lreturn、freturn、dreturn以及areturn(參考型別的)
- 另外還有一個return指令供宣告為void的方法、實體初始化方法、類和介面的初始化方法使用
2、在方法執行的程序中遇到了例外(Exception),并且這個例外沒有在方法內進行處理,也就是只要在本方法的例外表中沒有搜素到匹配的例外處理器,就會導致方法退出,簡稱例外完成出口,
-
方法執行程序中拋出例外時的例外處理,存盤在一個例外處理表,方便在發生例外的時候找到處理例外的代碼,


3.6、附加資訊
堆疊幀中還允許攜帶與java虛擬機實作相關的一些附加資訊,例如,對程式除錯提供支持的資訊,(很多資料都忽略了附加資訊)
4、相關面試題

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/160764.html
標籤:Java
下一篇:JVM--堆


