1. 程式人生 > 實用技巧 >第五章、虛擬機器棧

第五章、虛擬機器棧

一、虛擬機器棧概述

1.1、虛擬機器棧出現的背景

由於跨平臺性的設計,java的指令都是根據棧來設計的。不同平臺CPU架構不同,所以不能設計為基於暫存器的,暫存器和CPU的耦合性高。

JVM為何選擇基於棧的結構?

JVM為何要基於棧來設計有幾個理由。一個是JVM要設計成與平臺無關的,而平臺無關性就是要保證在沒有或者有很少的暫存器的機器上也要同樣能正確地執行Java程式碼。例如,在80X86的機器上暫存器就是沒有規律的,很難針對某一款機器設計通用的基於暫存器的指令,所以基於暫存器的架構很難做到通用。在手機作業系統方面,Google的Android平臺上的Dalvik VM就是基於特定晶片(ARM)設計的基於暫存器的架構,這樣在特定晶片上實現基於暫存器的架構可能更多考慮效能,但是也犧牲了跨平臺的移植性,當然在當前的手機上這個需求還不是最迫切的。

還有一個理由是為了指令的緊湊性,因為Java的位元組碼可能在網路上傳輸,所以class檔案的大小也是設計JVM位元組碼指令的一個重要因素,如在class檔案中位元組碼除了處理兩個表跳轉的指令外,其他都是位元組對齊的,操作碼可以只佔一個位元組大小,這都是為了儘量讓編譯後的class檔案更加緊湊。

根據棧設計的優點是跨平臺,指令集小,編譯器容易實現,缺點是效能下降,實現同樣的功能需要更多的指令。

1.2、記憶體中的堆與棧

棧是執行時的單位,而堆是儲存的單位。

  1. 棧解決程式的執行問題,即程式如何執行,或者說如何處理資料。堆解決的是資料儲存的問題,即資料怎麼放、放在哪兒。

  2. 一般來講,物件主要都是放在堆空間的,是執行時資料區比較大的一塊

  3. 棧空間存放基本資料型別的區域性變數,以及引用資料型別的物件的引用

1.3、虛擬機器棧的基本內容

(1)、虛擬機器棧(Java Virtual Machine Stack)是什麼?

  • java虛擬機器棧(Java Virtual Machine Stack),早期也叫Java棧。 每個執行緒在建立時都會建立一個虛擬機器棧,其內部儲存一個個的棧幀(Stack Frame),一個棧楨對應這一次次java方法呼叫。它是執行緒私有的。

  • 棧是一種快速有效的分配儲存方式,訪問速度僅次於PC暫存器(程式計數器)

(2)、虛擬機器棧(Java Virtual Machine Stack)`生命周?

生命週期和執行緒是一致的,隨著執行緒的建立而建立,消亡而消亡

(3)、作用

主管java程式的執行,它儲存方法的區域性變數8種基本資料型別物件的引用地址部分結果,並參與方法的呼叫和返回。

  • 區域性變數:相較於成員變數(成員變數或稱屬性)

  • 基本資料變數:8種基本資料型別

  • 引用型別變數:類,陣列,介面

(4)、棧的特點(優點)

  • 棧是一種快速有效的分配儲存方式,訪問速度僅次於程式計數器。

  • JVM直接對java棧的操作只有兩個

    • 每個方法執行,伴隨著進棧(入棧,壓棧)

    • 執行結束後的出棧工作

  • 對於棧來說不存在垃圾回收。

(5)、棧中可能出現的異常

Java虛擬機器規範允許Java棧的大小是動態的或者是固定不變的。

  • 如果採用固定大小的java虛擬機器,那每一個執行緒的虛擬機器棧容量可以線上程建立時獨立選定,如果執行緒請求分配的棧容量超過Java虛擬機器棧的允許的最大容量,Java虛擬機器就會丟擲一個堆疊溢位(StackOverFlowError)異常。

public class T2 {
    public static void main(String[] args) {
      main(args);
    }
}
  • 如果Java虛擬機器棧可以動態擴充套件,並且在嘗試擴充套件的時候無法申請到足夠的記憶體,或者在建立新的執行緒時沒有足夠的記憶體去建立對應的虛擬機器棧,那麼Java虛擬機器就會丟擲記憶體溢位(OutOfMemeoryError)異常。

(6)、設定棧的大小

我們可以使用引數-Xss選項來設定執行緒的最大棧空間,棧的大小直接決定函式呼叫的最大可達深度。

public class T2 {

    /**
     * 預設情況先 count:11402
     * 設定棧的大小為256k後 count:2460
     *
     */
    private  static int count =0;
    public static void main(String[] args) {
        System.out.println(count);
        count++;
        main(args);
    }
}

可以看出設定棧大小生效。

二、棧的儲存單位

棧中儲存什麼?

  • 每個執行緒都有自己的棧,棧中的資料都是以棧楨(Stack Fream)的格式存在的。

  • 在這個執行緒上正在執行每個方法都各自對應一個棧楨(Stack Fream)。

  • 棧楨是一個記憶體區塊,是一個數據集,維繫著方法執行過程中的各種資料資訊。

2.1、棧執行原理

  • JVM對棧的操作只有兩種,就是對棧楨的壓棧出棧,遵循“先進後出/後進先出”原則。

  • 在一條執行緒中,一個時間點上,只會有一個活動的棧楨,即只有當前正在執行的方法的棧楨(棧頂棧楨)是有效的,這個棧楨被稱為當前棧楨(Current Frame),與當前棧楨對應的方法就是當前方法(Current Method),定義這個方法的類就是當前類(Current Class)。

  • 執行引擎執行的所有位元組碼指令只針對當前棧楨進行操作

  • 如果在該方法中呼叫其他方法,對應的新的棧楨會被創建出來,方法棧的頂端,成為新的當前棧楨。

2.2、棧楨的內部結構

每個棧幀中儲存著

  1. 區域性變量表(Local Variables)

  2. 運算元棧(Operand Stack)(或表示式棧)

  3. 動態連結(Dynamic Linking)(或執行執行時常量池的方法引用)

  4. 方法返回地址(Return Adress)(或方法正常退出或者異常退出的定義)

  5. 一些附加資訊

三、區域性變量表(Local Variables)

  1. 區域性變量表也被稱之為區域性變數陣列本地變量表

  2. 定義為一個數字陣列,主要用於儲存方法引數定義在方法體內的區域性變數這些資料型別包括各類基本資料型別、物件引用(reference),以及returnAddress型別。

  3. 由於區域性變量表是建立線上程的棧上,是執行緒私有的資料,因此不存在資料安全問題

  4. 區域性變量表所需的容量大小是在編譯期確定下來的,並儲存在方法的Code屬性的maximum local variables資料項中。在方法執行期間是不會改變區域性變量表的大小的。

  5. 方法巢狀呼叫的次數由棧的大小決定。一般來說,棧越大,方法巢狀呼叫次數越多。對一個方法而言,他的引數和區域性變數越多,使得區域性變量表膨脹,它的棧幀就越大,以滿足方法呼叫所需傳遞的資訊增大的需求。進而函式呼叫就會佔用更多的棧空間。

  6. 區域性變量表中的變數只在當前方法呼叫中有效。在方法執行時,虛擬機器通過使用區域性變量表完成引數值到引數變數列表的傳遞過程。當方法呼叫結束後,隨著方法棧幀的銷燬,區域性變量表也會隨之銷燬

3.1、檢視幀的區域性變量表

利用javap命令對位元組碼檔案進行解析檢視main()方法對應棧幀的區域性變量表,可以在IDEA 上安裝jclasslib byte viewcoder外掛檢視方法內部位元組碼資訊剖析,以main()方法為例

如圖:簡單解釋位元組碼指令的意義。

  

  

  

length:區域性邊的作用域。

3.2、變數槽(slot)的理解與演示

  1. 引數值的存放總是從區域性變數陣列的index0開始,到陣列長度-1的索引結束

  2. 區域性變量表,最基本的儲存單元是Slot(變數槽)

  3. 區域性變量表中存放編譯期可知的各種基本資料型別(8種),引用型別(reference),returnAddress型別的變數。

  4. 在區域性變量表裡,32位以內的型別只佔用一個slot(包括returnAddress型別),64位的型別(long和double)佔用兩個slot

  5. byte、short、char、float在儲存前被轉換為int,boolean也被轉換為int,0表示false,非0表示true,long和double則佔據兩個slot

  1. JVM會為區域性變量表中的每一個slot都分配一個訪問索引,通過這個索引即可成功訪問到區域性變量表中指定的區域性變數值。

  2. 當一個例項方法被呼叫的時候,它的方法引數和方法體內部定義的區域性變數將會按照宣告順序被複制到區域性變量表中的每一個slot上

  3. 如果需要訪問區域性變量表中一個64bit的區域性變數值時,只需要使用前一個索引即可。(比如:訪問long或者double型別變數)

  4. 如果當前幀是由構造方法或者例項方法建立的(意思是當前幀所對應的方法是構造器方法或者是普通的例項方法),那麼該物件引用this將會存放在index為0的slot處,其餘的引數按照引數表順序排列。

    • 靜態方法中不能引用this,是因為靜態方法所對應的棧幀當中的區域性變量表中不存在this。
public class T3 {

     private int count = 1;
    //靜態方法不能使用this
     public static void testStatic(){
         //編譯錯誤,因為this變數不存在與當前方法的區域性變量表中!!!
         System.out.println(this.count);
     }
}

3.3、slot的重複利用

棧幀中的區域性變量表中的槽位是可以重複利用的,如果一個區域性變數過了其作用域,那麼在其作用域之後申明的新的區域性變數就很有可能會複用過期區域性變數的槽位,從而達到節省資源的目的。

private void test2() {
        int a = 0;
        {
            int b = 0;
            b = a+1;
        }
        //變數c使用之前以及經銷燬的變數b佔據的slot位置
        int c = a+1;
    }

上述程式碼對應的棧幀中區域性變量表中一共有多少個slot,或者說區域性變量表的長度是幾?

答案是3:

變數b的作用域是

{
     int b = 0;
     b = a+1;
}

this佔0號、a單獨佔1個槽號、c重複使用了b的槽號

3.4、靜態變數與區域性變數的對比

變數的分類:

  • 按照資料型別分:

    • 基本資料型別;

    • 引用資料型別;

  • 按照在類中宣告的位置分:

    • 成員變數:在使用前,都經歷過預設初始化賦值

      • static修飾:類變數,類載入連結的準備preparation階段給類變數預設賦0值—>初始化階段initialization給類變數顯式賦值即靜態程式碼塊賦值;

      • 不被static修飾:例項變數,隨著物件的建立,會在堆空間分配例項變數空間,並進行預設賦值

    • 區域性變數:在使用前,必須要進行顯式賦值的!否則,編譯不通過

3.5、補充說明

  • 在棧幀中,與效能調優關係最為密切的部分就是區域性變量表。在方法執行時,虛擬機器使用區域性變量表完成方法的傳遞

  • 區域性變量表中的變數也是重要的垃圾回收根節點,只要被區域性變量表中直接或間接引用的物件都不會被回收

四、運算元棧(Operand Stack)

  • 棧 :可以使用陣列或者連結串列來實現,陣列和連結串列是真實存在的資料結構。

  • 每一個獨立的棧幀中除了包含區域性變量表以外,還包含一個後進先出的運算元棧,也可以稱為表示式棧

  • 運算元棧,在方法執行過程中,根據位元組碼指令,往棧中寫入資料或提取資料,即入棧(push)或出棧(pop)

某些位元組碼指令將值壓入運算元棧,其餘的位元組碼指令將運算元取出棧,使用他們後再把結果壓入棧。(如位元組碼指令bipush操作)比如:執行復制、交換、求和等操作。

  • bipush:將一個常量載入到運算元棧。

  • istore_<n>:將一個數值從運算元棧儲存到區域性變量表。

  • iload_<n>:將一個區域性變數載入到操作棧。

4.1、運算元棧特點

  • 運算元棧,主 要用於儲存計算過程的中間結果,同時作為計算過程中變數臨時的儲存空間

  • 運算元棧就是jvm執行引擎的一個工作區,當一個方法開始執行的時候,一個新的棧幀也會隨之被創建出來,這個方法的運算元棧是空的

  • 每一個運算元棧都會擁有一個明確的棧深度用於儲存數值,其所需的最大深度在編譯器就定義好了,儲存在方法的code屬性中,為max_stack的值

  • 棧中的任何一個元素都是可以任意的java資料型別 32bit的型別佔用一個棧單位深度 64bit的型別佔用兩個棧深度單位

  • 運算元棧並非採用訪問索引的方式來進行資料訪問的,而是隻能通過標準的入棧push和出棧pop操作來完成一次資料訪問

  • 如果被呼叫的方法帶有返回值的話,其返回值將會被壓入當前棧幀的運算元棧中,並更新PC暫存器中下一條需要執行的位元組碼指令。

  • 運算元棧中的元素的資料型別必須與位元組碼指令的序列嚴格匹配,這由編譯器在編譯期間進行驗證,同時在類載入過程中的類驗證階段的資料流分析階段要再次驗證。

  • 另外,我們說Java虛擬機器的解釋引擎是基於棧的執行引擎,其中的棧指的就是運算元棧。

4.2、運算元棧程式碼追蹤

結合上圖結合下面的圖來看一下一個方法(棧幀)的執行過程

9入棧;

儲存2,2進入區域性變量表 注意:區域性變量表的0號位被構造器佔用,這裡的15從區域性變量表1號開始

③ 壓入12;

④ 12出棧,儲存12進入區域性變量表;

從區域性變量表中把索引為1和2的是資料取出來,放到運算元棧;

iadd相加操作

iadd操作結果21出棧

將23儲存在區域性變量表索引為3的位置上istore_3

4.3、棧頂快取技術(Top-of-Stack Cashing)

  • 基於棧式架構的虛擬機器所使用的零地址指令(即不考慮地址,單純入棧出棧)更加緊湊,但完成一項操作的時候必然需要使用更多的入棧和出棧指令,這同時也就意味著將需要更多的指令分派(instruction dispatch)次數和記憶體讀/寫次數

  • 由於運算元是儲存在記憶體中的,因此頻繁地執行記憶體讀/寫操作必然會影響執行速度。為了解決這個問題,HotSpot JVM的設計者們提出了棧頂快取技術,將棧頂元素全部快取在物理CPU的暫存器中,以此降低對記憶體的讀/寫次數,提升執行引擎的執行效率

五、動態連結(Dynamic Linking)

執行時常量池位於方法區(注意: JDK1.7 及之後版本的 JVM 已經將執行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放執行時常量池。)

  • 為什麼需要常量池呢?

  常量池的作用,就是為了提供一些符號和常量,便於指令的識別。

  • 每一個棧幀內部都包含一個指向執行時常量池Constant pool或該棧幀所屬方法的引用。包含這個引用的目的就是為了支援當前方法的程式碼能夠實現動態連結。比如invokedynamic指令

  • 在Java原始檔被編譯成位元組碼檔案中時,所有的變數和方法引用都作為符號引用(symbolic Refenrence)儲存在class位元組碼檔案(javap反編譯檢視)的常量池裡。比如:描述一個方法呼叫了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示的,那麼動態連結的作用就是為了將這些符號引用(#)最終轉換為呼叫方法的直接引用。

六、方法的呼叫:解析與分派

在JVM中,將符號引用轉換為呼叫方法的直接引用與方法的繫結機制相關。

  • 靜態連結 當一個位元組碼檔案被裝載進JVM內部時,如果被呼叫的目標方法在編譯期可知,且執行期保持不變時。這種情況下將呼叫方法的符號引用轉換為直接引用的過程稱之為靜態連結。

  • 動態連結 如果被呼叫的方法在編譯期無法被確定下來,也就是說,只能夠在程式執行期將呼叫方法的符號引用轉換為直接引用,由於這種引用轉換過程具備動態性,因此也就被稱之為動態連結。

對應的方法的繫結機制為:早期繫結(Early Binding)和晚期繫結(Late Bingding)。繫結是一個欄位、方法或者類在符號引用被替換為直接引用的過程,這僅僅發生一次。

  • 早期繫結 早期繫結就是指被呼叫的目標方法如果在編譯期可知,且執行期保持不變時,即可將這個方法與所屬的型別進行繫結,這樣一來,由於明確了被呼叫的目標方法究竟是哪一個,因此也就可以使用靜態連結的方式將符號引用轉換為直接引用。

  • 晚期繫結 如果被呼叫的方法在編譯期無法被確定下來,只能夠在程式執行期根據實際的型別繫結相關的方法,這種繫結方式也就被稱之為晚期繫結。

隨著高階語言的橫空出世,類似於java一樣的基於面向物件的程式語言如今越來越多,儘管這類程式語言在語法風格上存在一定的差別,但是它們彼此之間始終保持著一個共性,那就是都支援封裝,整合和多型等面向物件特性,既然這一類的程式語言具備多型特性,那麼自然也就具備早期繫結和晚期繫結兩種繫結方式。

Java中任何一個普通的方法其實都具備虛擬函式的特徵,它們相當於C++語言中的虛擬函式(C++中則需要使用關鍵字virtual來顯式定義)。如果在Java程式中不希望某個方法擁有虛擬函式的特徵時,則可以使用關鍵字final來標記這個方法。

6.1、虛方法和非虛方法

子類物件的多型性使用前提:

①類的繼承關係(父類的宣告)

②方法的重寫(子類的實現)

實際開發編寫程式碼中用的介面,實際執行是匯入的的三方jar包已經實現的功能

非虛方法

  • 如果方法在編譯器就確定了具體的呼叫版本,這個版本在執行時是不可變的。這樣的方法稱為非虛方法

  • 靜態方法、私有方法、final方法、例項構造器(例項已經確定,this()表示本類的構造器)、父類方法(super呼叫)都是非虛方法

其他所有體現多型特性的方法稱為虛方法

6.2、虛擬機器中提供了以下幾條方法呼叫指令

  1. 普通呼叫指令:

    1. invokestatic:呼叫靜態方法,解析階段確定唯一方法版本;

    2. invokespecial:呼叫<init>方法、私有及父類方法,解析階段確定唯一方法版本;

    3. invokevirtual呼叫所有虛方法;

    4. invokeinterface:呼叫介面方法;

  2. 動態呼叫指令(Java7新增)

    1. invokedynamic:動態解析出需要呼叫的方法,然後執行 .

    2. 前四條指令固化在虛擬機器內部,方法的呼叫執行不可人為干預,而invokedynamic指令則支援由使用者確定方法版本。

其中invokestatic指令和invokespecial指令呼叫的方法稱為非虛方法 其中invokevirtual(final修飾的除外,JVM會把final方法呼叫也歸為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();
}

6.3、關於invokedynamic指令

  • JVM位元組碼指令集一直比較穩定,一直到java7才增加了一個invokedynamic指令,這是Java為了實現【動態型別語言】支援而做的一種改進

  • 但是java7中並沒有提供直接生成invokedynamic指令的方法,需要藉助ASM這種底層位元組碼工具來產生invokedynamic指令.直到Java8的Lambda表示式的出現,invokedynamic指令的生成,在java中才有了直接生成方式

  • Java7中增加的動態語言型別支援的本質是對java虛擬機器規範的修改,而不是對java語言規則的修改,這一塊相對來講比較複雜,增加了虛擬機器中的方法呼叫,最直接的受益者就是執行在java平臺的動態語言的編譯器

6.4、動態型別語言和靜態型別語言

  • 動態型別語言和靜態型別語言兩者的卻別就在於對型別的檢查是在編譯期還是在執行期,滿足前者就是靜態型別語言,反之則是動態型別語言。

  • 直白來說 靜態語言是判斷變數自身的型別資訊;動態型別語言是判斷變數值的型別資訊,變數沒有型別資訊,變數值才有型別資訊,這是動態語言的一個重要特徵

  • Java是靜態型別語言(儘管lambda表示式為其增加了動態特性),js,python是動態型別語言.

Java:String info = "jdy";//靜態語言

JS:var name = "jdy“;var name = 10;//動態語言

Pythom: info = 130;//更加徹底的動態語言

6.5、方法重寫的本質

  • 找到運算元棧的第一個元素所執行的物件的實際型別,記作 C。

  • 如果在型別C中找到與常量池中的描述符、簡單名稱都相符的方法,則進行訪問許可權校驗,如果通過則返回這個方法的直接引用,查詢過程結束;如果不通過,則返回java.lang.IllegalAccessError異常。

  • 否則,按照繼承關係從下往上依次對c的各個父類進行第二步的搜尋和驗證過程。

  • 如果始終沒有找到合適的方法,則丟擲java.lang.AbstractMethodError異常。 IllegalAccessError介紹 程式檢視訪問或修改一個屬性或呼叫一個方法,這個屬性或方法,你沒有許可權訪問。一般的,這個會引起編譯器異常。這個錯誤如果發生在執行時,就說明一個類發生了不相容的改變。

6.6、虛方法表

  • 在面向物件程式設計中,會很頻繁期使用到動態分派,如果在每次動態分派的過程中都要重新在累的方法元資料中搜索合適的目標的話就可能影響到執行效率。因此,為了提高效能,jvm採用在類的方法區建立一個虛方法表(virtual method table)(非虛方法不會出現在表中)來實現。使用索引表來代替查詢。

  • 每個類中都有一個虛方法表,表中存放著各個方法的實際入口。

  • 那麼虛方法表什麼時候被建立? 虛方法表會在類載入的連結階段被建立 並開始初始化,類的變數初始值準備完成之後,jvm會把該類的虛方法表也初始化完畢。

七、方法返回地址(Return Address)

  • 存放呼叫該方法的PC暫存器的值(下一條要執行的指令的地址值)。

  • 一個方法的結束,有兩種方式:

    • 正常執行完成

    • 出現未處理的異常,非正常退出

  • 無論通過哪種方式退出,在方法退出後都返回到該方法被呼叫的位置。方法正常退出時,呼叫者(方法的呼叫者可能也是一個方法)的pc計數器的值作為返回地址,即呼叫該方法的指令的下一條指令的地址。而通過異常退出時,返回地址是要通過異常表來確定,棧幀中一般不會儲存這部分資訊。

  • 本質上,方法的退出就是當前棧幀出棧的過程。此時,需要恢復上層方法的區域性變量表、運算元棧、將返回值入呼叫者棧幀的運算元棧、設定PC暫存器值等,讓呼叫者方法繼續執行下去。

  • 正常完成出口和異常完成出口的區別在於:通過異常完成出口退出的不會給他的上層呼叫者產生任何的返回值。

方法退出的方式

當一個方法開始執行後,只有兩種方式可以退出這個方法

  1. 執行引擎遇到任意一個方法返回的位元組碼指令(return),會有返回值傳遞給上層的方法呼叫者,簡稱正常完成出口;

    • 一個方法在正常呼叫完成之後究竟需要使用哪一個返回指令還需要根據方法返回值的實際資料型別而定

    • 在位元組碼指令中,返回指令包含ireturn(當返回值是boolena、byte、char、short和int型別時使用)、lreturn、freturn、dreturn以及areturn(引用型別的)

    • 另外還有一個return指令供宣告為void的方法、例項初始化方法、類和介面的初始化方法使用

  1. 在方法執行的過程中遇到了異常(Exception),並且這個異常沒有在方法內進行處理,也就是隻要在本方法的異常表中沒有搜素到匹配的異常處理器,就會導致方法退出,簡稱異常完成出口方法執行過程中丟擲異常時的異常處理,儲存在一個異常處理表,方便在發生異常的時候找到處理異常的程式碼。