通過(guò)JVM深入理解Java異常機(jī)制

JVM內(nèi)部結(jié)構(gòu)

要深入理解JVM異常處理機(jī)制,需要從JVM內(nèi)部結(jié)構(gòu)開(kāi)始。
下圖描述的主要是Java程序在執(zhí)行時(shí),由JVM管理的運(yùn)行時(shí)數(shù)據(jù)區(qū);包括方法區(qū)、Java堆、Java虛擬機(jī)棧、PC寄存器、本地方法棧,還有常量池。它們又被分為兩大類(lèi)——線程共享和線程私有數(shù)據(jù)區(qū)。


  • 線程共享數(shù)據(jù)區(qū)包括:Java堆、方法區(qū)/永久代/元空間、常量池。它們會(huì)隨著虛擬機(jī)啟動(dòng)而創(chuàng)建,隨著虛擬機(jī)退出而銷(xiāo)毀。
  • 線程私有數(shù)據(jù)區(qū)包括:PC寄存器、JVM棧、native本地方法棧。它們是與線程一一對(duì)應(yīng)的,這些與線程對(duì)應(yīng)的數(shù)據(jù)區(qū)域會(huì)隨著線程開(kāi)始和結(jié)束而創(chuàng)建和銷(xiāo)毀。

而今天要聊的Java異常處理機(jī)制,跟線程私有的 JVM棧和PC寄存器有關(guān)。
JVM 用棧來(lái)跟蹤一系列的方法調(diào)用過(guò)程。該堆棧保存了每個(gè)調(diào)用方法的信息。當(dāng)一個(gè)新的方法被調(diào)用時(shí),JVM把描述該方法的信息封裝為棧幀置入棧頂,位于棧頂?shù)姆椒檎趫?zhí)行的方法。
每個(gè)方法被執(zhí)行的時(shí)候都會(huì)同時(shí)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法被調(diào)用直至執(zhí)行完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在JVM棧中從入棧到出棧的過(guò)程。

如果在執(zhí)行的方法過(guò)程中拋出異常,JVM必須找到能捕獲處理該異常的catch塊

  • JVM首先觀察當(dāng)前方法是否存在能夠處理該異常的catch塊,如果存在,就執(zhí)行該catch代碼塊
  • 如果不存在,JVM會(huì)從從棧中彈出該方法的棧幀,繼續(xù)到前一個(gè)方法中查找合適的catch塊
  • 當(dāng)JVM追溯到調(diào)用棧的最底部的方法時(shí),如果仍然沒(méi)有找到處理該異常的代碼塊,將調(diào)用異常對(duì)象的printStackTrace()方法,打印來(lái)自方法調(diào)用棧的異常信息隨后終止整個(gè)應(yīng)用程序。

Java異常表(Exception table)

提到JVM的異常處理機(jī)制,不得不提及Exception Table;以下稱(chēng)為異常表。
Java 異常表(Exception table)是 Java 編譯器為每個(gè)方法生成的一張映射表,它用于記錄方法中每個(gè)代碼塊(try-catch/try-finally 等)及其對(duì)應(yīng)的異常信息。在 Java 方法執(zhí)行時(shí),如果發(fā)生異常,Java 虛擬機(jī)會(huì)根據(jù)異常表的信息查找并確定正確的異常處理程序。
每個(gè)通過(guò)try-catch捕獲異常的方法,都有一個(gè)異常表與之關(guān)聯(lián);該表隨方法的字節(jié)碼序列一起在字節(jié)碼文件中存放。對(duì)于 try 塊捕獲的每個(gè)異常,異常表都有一個(gè)條目。
如果在方法執(zhí)行期間拋出異常,Java 虛擬機(jī)會(huì)在異常表中搜索匹配的條目。如果拋出異常代碼行號(hào)(當(dāng)前PC程序計(jì)數(shù)器)在條目指定的范圍內(nèi),并且拋出的異常類(lèi)是條目指定的異常類(lèi)(或者是指定異常類(lèi)的子類(lèi)),則異常表?xiàng)l目匹配。

Java 虛擬機(jī)按照條目在表中出現(xiàn)的順序搜索異常表。

  • 當(dāng)找到第一個(gè)匹配項(xiàng)時(shí),Java 虛擬機(jī)將PC寄存器(程序計(jì)數(shù)器)設(shè)置為新的 pc 偏移位置并在那里繼續(xù)執(zhí)行。
  • 如果未找到匹配項(xiàng),Java 虛擬機(jī)將彈出當(dāng)前堆棧幀并重新拋出相同的異常。當(dāng) Java 虛擬機(jī)彈出當(dāng)前棧幀時(shí),它有效地中止了當(dāng)前方法的執(zhí)行并返回到調(diào)用該方法的上一級(jí)方法。但是它并沒(méi)有在上一級(jí)方法中繼續(xù)正常執(zhí)行,而是在那個(gè)方法的調(diào)用處拋出了同樣的異常,這導(dǎo)致Java虛擬機(jī)經(jīng)歷了同樣的過(guò)程,搜索那個(gè)方法的異常表。

Java字節(jié)碼是通過(guò)javac編譯器 基于Java源代碼文件生成的,就是按照字節(jié)碼規(guī)范重新將源代碼換成JVM可理解的表達(dá)方式而已;
因此Java源代碼中,使用try-catch塊捕獲異常的方法,生成字節(jié)碼時(shí),會(huì)通過(guò)異常表(Exception table)來(lái)描述源代碼中的try-catch;具體來(lái)說(shuō),異常表中的每個(gè)條目,必須包含 try 塊的起始位置和結(jié)束位置、catch 塊中捕獲的異常類(lèi)型以及捕獲異常后跳轉(zhuǎn)執(zhí)行的位置。

字節(jié)碼文件中,方法Code里的Exception table,包含如下信息:

  • from 可能發(fā)生異常的起始點(diǎn)
  • to 可能發(fā)生異常的結(jié)束點(diǎn)
  • target 上述from和to之前發(fā)生異常后的異常處理器的位置
  • type 異常處理器(能)處理的異常類(lèi)型

其中,from、to和target都是PC寄存器(程序計(jì)數(shù)器)所指示的字節(jié)碼行號(hào);程序的跳轉(zhuǎn)執(zhí)行,就是修改PC寄存器,改變下一行要執(zhí)行的代碼位置來(lái)實(shí)現(xiàn)的。

Java異常表實(shí)戰(zhàn)

下面我們創(chuàng)建ExceptionTable類(lèi),編譯后再通過(guò)javap反匯編字節(jié)碼文件;

public class ExceptionTable {
    public static void main(String[] args) throws Exception {
        try {
            throw new Exception();
        } catch (Exception e) {
            System.out.println("Caught!");
        } finally {
            System.out.println("Finally!");
        }
    }
}

javap -v ExceptionTable.class

// 省略Constant pool
{
  public exception.ExceptionTable();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lexception/ExceptionTable;

  public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/lang/Exception
         3: dup
         4: invokespecial #3                  // Method java/lang/Exception."<init>":()V
         7: athrow
         8: astore_1
         9: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        12: ldc           #5                  // String Caught!
        14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        17: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        20: ldc           #7                  // String Finally!
        22: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        25: goto          39
        28: astore_2
        29: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        32: ldc           #7                  // String Finally!
        34: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        37: aload_2
        38: athrow
        39: return
      Exception table:
         from    to  target type
             0     8     8   Class java/lang/Exception
             0    17    28   any
      LineNumberTable:
        line 6: 0
        line 7: 8
        line 8: 9
        line 10: 17
        line 11: 25
        line 10: 28
        line 12: 39
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            9       8     1     e   Ljava/lang/Exception;
            0      40     0  args   [Ljava/lang/String;
      StackMapTable: number_of_entries = 3
        frame_type = 72 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 83 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 10 /* same */
    Exceptions:
      throws java.lang.Exception
}
SourceFile: "ExceptionTable.java"

main方法的Code指令后面,我們能看到Exception table:
異常表第一行:它是我們的try-catch語(yǔ)句,如果在字節(jié)碼的0-8行發(fā)生異常,將跳轉(zhuǎn)到第8行進(jìn)行處理
異常表第二行:它是我們的finally語(yǔ)句,無(wú)論0-17行發(fā)生什么,最終都將由第28行進(jìn)行處理。

異常表中的 any

  • 如果命中了 any 之后,會(huì)將異常繼續(xù)向上拋出去,交由該方法的調(diào)用方法處理。

被冗余/重復(fù)的finally
仔細(xì)觀察ExceptionTable#main方法的Code指令,會(huì)發(fā)現(xiàn)finally塊的內(nèi)容是重復(fù)的。
在 JDK1.4.2之前,javac 編譯器使用 jsrret 指令來(lái)實(shí)現(xiàn) finally 語(yǔ)句,但是JDK1.4.2之后自動(dòng)在每段可能的分支路徑后將 finally 語(yǔ)句塊內(nèi)容冗余生成一遍來(lái)實(shí)現(xiàn)。JDK1.7及之后版本,則完全禁止在字節(jié)碼文件中使用 jsrret 指令。
冗余finally塊的指令,就是把finally 代碼塊對(duì)應(yīng)的指令復(fù)制一份,分別放到了 try/catch 指令的后面,就能達(dá)到 finally 一定會(huì)被執(zhí)行的效果。

jsrret是早期Java字節(jié)碼中的兩個(gè)指令,用于實(shí)現(xiàn)Java方法的子程序調(diào)用和返回。

  • jsr指令(Jump Subroutine)用于實(shí)現(xiàn)Java方法的子程序調(diào)用。它的作用是將當(dāng)前方法的返回地址壓入操作數(shù)棧中,并跳轉(zhuǎn)到指定的子程序執(zhí)行。在子程序執(zhí)行完畢后,使用ret指令返回到原來(lái)的方法,并將返回值壓入操作數(shù)棧中。在 JDK1.4.2之前,JVM處理異常時(shí),調(diào)用異常處理程序就是通過(guò)jsr。
  • ret指令(Return from Subroutine)用于實(shí)現(xiàn)Java方法的返回。它的作用是將操作數(shù)棧頂?shù)闹底鳛榉祷刂捣祷兀⑻D(zhuǎn)到之前使用jsr指令保存的返回地址處繼續(xù)執(zhí)行。

jsrret指令在Java SE 6及以前的版本中是合法的指令,但在Java SE 7中已經(jīng)被廢棄。在Java SE 7及以后的版本中,應(yīng)該使用invokedynamic指令來(lái)實(shí)現(xiàn)動(dòng)態(tài)語(yǔ)言支持,而不是使用jsr和ret指令。

總結(jié)

1.JVM異常處理流程

  • 當(dāng)Java程序中發(fā)生異常時(shí),JVM會(huì)在方法的異常表中查找相應(yīng)的異常處理代碼。如果找到了匹配的異常處理代碼,JVM會(huì)執(zhí)行該代碼來(lái)處理異常;如果沒(méi)有找到匹配的代碼,JVM會(huì)將異常向上拋出,彈出當(dāng)前方法的棧幀,返回到調(diào)用該方法的上一級(jí)方法,查找上一級(jí)方法的異常表;
  • 依次沿調(diào)用棧查找,如果所有的棧幀被彈出,仍然沒(méi)有處理,則將異常拋給當(dāng)前的Thread,Thread會(huì)終止;如果當(dāng)前Thread為最后一個(gè)非守護(hù)線程,且未處理異常,則會(huì)導(dǎo)致JVM終止運(yùn)行。

當(dāng) JVM 中的所有非守護(hù)線程都已經(jīng)結(jié)束時(shí),JVM 就會(huì)自動(dòng)退出,而無(wú)論該線程是否拋出了未捕獲的異常。但是,如果這個(gè)非守護(hù)線程拋出了未被處理的異常,JVM 會(huì)在退出之前將堆棧信息打印到控制臺(tái)。此外,非守護(hù)線程如果沒(méi)有設(shè)置 setUncaughtExceptionHandler,也會(huì)將未捕獲的異常傳遞給默認(rèn)的未捕獲異常處理程序來(lái)進(jìn)行處理,該處理程序簡(jiǎn)單地打印了異常的堆棧軌跡到控制臺(tái)上。

編譯器生成字節(jié)碼指令時(shí),通過(guò)冗余finally塊指令內(nèi)容,達(dá)到 finally 一定會(huì)被執(zhí)行的效果。
2.Java異常表

  • 編譯器生成的異常表,是按Java源代碼中catch塊聲明的順序依次列出每個(gè)異常條目;
  • 每個(gè)異常條目包含from、to、target和type;finally塊對(duì)應(yīng)的條目一定在最后一行,from~to的范圍是整個(gè)try塊,且type是any 代表一定會(huì)執(zhí)行。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容