java异常字节码详细分析与原理解析
概述
异常是什么?
异常:程序运行时不在预期范围内的事情都可以称作为
异常
;异常阻止程序按照期望正常运行。
异常如果捕获与处理?
捕获异常可以使用
if
对预期值进行判断后输出异常码,在C++
C#
JAVA
等语言中可以使用try...catch
语法进行捕获与处理
C语言处理程序异常
在C语言中捕获与处理异常比较麻烦,C语言本身没有提供异常的处理机制;下面举例一种通俗异懂的方式:
int main(){
int status=0;
//预期值为0
if(status==1){
//输出或者处理异常信息
}else{
//处理正常的逻辑
}
}
上面是一段C语言精典的异常处理方式if
判断预期值;不符合预期进输出或者处理异常信息;这样的方式在大多数编程语言中都是能用的。
但这样的方式处理会有一个很大的缺点,试想如果业务逻辑复杂的情况下,需要处理的预期判断也就会增多;加上正常的业务逻辑判断;最后程序会成为一个if炸弹,在不知什么时候就爆炸了;这样的形式对于调用者也是不友好的,调用者需要看每个方法的使用文档才能知道被调用的程序需要什么样的预期值;返回什么样的状态码是异常的,每个异常码又代表什么意思。
对于程序的调试也是一大挑战;在异常处理只输出错误提示与状态码时并不知道程序当前的调用栈信息,如果有重复的错误码;这个工作很难做下去。
在OOP的时代,是否可以使用OOP三大精髓封装
继承
多态
来解决上面的这些问题呢?
答案是可以的,现在大部分OOP语言都这样做了,有很好的异常处理机制;在Java中就可以使用try...catch...finally
的语法来处理异常,抛出异常可以使用thow
,指示方法会有那些异常可以使用thows
;使用java.lang.Throwable
封装异常状态码、异常描述、堆栈信息;
Java异常捕获与处理
try...catch...finally
语法示例:
public void demoTryCatch(){
InputStream in;
try{
in= new FileInputStream(new File("./test.txt"));
//使用除数为0的错误使程序抛出异常信息
int d=1/0;
in.close();
}catch (Throwable e){
//在catch中捕获除数为0的异常并输出异常堆栈信息
e.printStackTrace();
}finally{
//任何时候都会执行的块
System.out.println("finally");
if(in != null){
//关闭I/O
in.close();
}
}
}
示例中1/0
的算式在Java中会自动的抛出ArithmeticException
异常;在catch
中捕获Throwable
类型的异常并在代码块中进行处理;Java中所有的异常都继承处Throwable
类型,上述示例中使用Throwable
类型进行捕获时捕获到Java中的所有异常;Java在异常处理块中还提供了finally
关键字用于程序在发生异常时做一些必要的补救,finally
关键字的代码块任何时候都会执行,包括在try
catch
代码块中使用return
返回函数结果的情况。
例如在操作I/O时,在try
中打开了I/O后计算1/0
的值时出现了异常,这时会执行catch
块中的代码;如果没有finally
块的话I/O就不会被关闭;这就会导致内存泄漏等风险最后导致系统内存被占用完毕后导致程序非正常退出;这样的情况不是我们想要结果,这时就可以使用finally
代码块来处理这样的情况,在finally
块中对I/O进行关闭的操作。
Throw 与Throws 关键字
在上面的示例中的异常触发点自己做判断后抛出自定义的异常;那需要怎样将将这个异常抛出呢?做法如下示例:
public void demoTryCatch() throws Exception{
InputStream in;
try{
in= new FileInputStream(new File("./test.txt"));
int i=0;
//使用除数为0的错误使程序抛出异常信息
if(i==0){
throw new new Exception("无效的因子");
}
int d=1/i;
in.close();
}catch (Throwable e){
//在catch中捕获除数为0的异常并输出异常堆栈信息
e.printStackTrace();
}finally{
//任何时候都会执行的块
System.out.println("finally");
if(in != null){
//关闭I/O
in.close();
}
}
}
做除法运算前先使用if
判断被除数是不是为0;如果为0将抛出Exception
异常,并在异常中写入异常描述信息无效的因子;Exception
异常类型在java中是检查类异常;所以需要提示调用者捕获可能出现的异常Exception
;这个提示就可以在方法后面使用throws
进行标示,多个异常时使用,
分隔;非检查异常也可以写在throws
关键字后面;对于检查异常在编译时会输出错误提示使编译不通过。
Java通过throw
抛出异常;使用throws
标示方法可能出现的异常;对于检查异常在没有进行捕获处理时在编译阶段给出错误的提示。通过这样的形式来避免部分人为因素导致程序崩溃。
Java 检查异常 运行时异常
Java基本的异常类类图:
Throwable
Java中所有的异常类都继承自它,角色在异常的领域中和Object
一样;需要捕获所有异常时可以使用catch(Throwable e)
进行捕获
Error
它表示不希望被程序捕获或者是程序无法处理的错误。它的子类常用有OutOfMemoryError
StackOverFlowError
Exception
它表示程序可能捕捉的异常或者程序可以处理的异常,Exception
为检查异常;方法使用throws
标示Exception
的异常后,调用者需要明确处理方法;
RuntimeException
运行时异常,它是Exception
的子类;是一个特殊的检查异常,调用者可以忽略不处理这个异常,交给上面的调用者或者全局进行捕获处理;继承自RuntimeException
的异常可以不用throws
关键字在方法上进行标示也能通过编译,但通常不建议这样做。
Java异常捕获原理分析
上面讲了Java异常捕获的各种使用方法、异常的类型及作用,但Java是怎样处理异常的呢?发生异常又是怎样保证finally
块的代码一定会被调用呢?异常信息在Java中又存储在那呢?
下面通过Java编译后生成的字节码来分析Java异常的处理过程(字节码看不懂没有关系,搞清楚思想就可以)
public static void main(String[] args) {
try {
System.out.println("enter try block");
} catch (Exception e) {
System.out.println("enter catch block");
}finally{
System.out.println("enter finally block");
}
}
通过在编译器中进行编码后使用jclasslib 工具查看上面代码的字节码,当然也可以使用javap -c <类名>进行查看,主方法的字节码如下图:
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String enter try block
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #5 // String enter finally block
13: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: goto 50
19: astore_1
20: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
23: ldc #7 // String enter catch block
25: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
28: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
31: ldc #5 // String enter finally block
33: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: goto 50
39: astore_2
40: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
43: ldc #5 // String enter finally block
45: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
48: aload_2
49: athrow
50: return
Exception table:
from to target type
0 8 19 Class java/lang/Exception
0 8 39 any
19 28 39 any
}
关键指令解析:
- ldc: 从常量池加载对应的常量到操作数栈
- goto: 跳转到指定的行
- athrow:抛出栈顶的异常
- dup:将本地变量压入栈顶
- astore_n:将栈顶的引用类型变量放入本地变量表
- aload_n:将本地变量压入栈顶
*下面使用’@N’代表指向上面字节码中的行
根据程序的逻辑在@3的命令ldc
代表进入try
代码块,@11指令代表进入finally
代码块;@36行goto
指令将跳转到@50行,再执行50行的return
指令跳出函数;@23行进入catch
异常处理块,后面的@31 @43分别为finally
的代码块;所有代码块中执行的指令都是同一块。
通过上面类文件的字节码文件简单的阅读可以发现Java在try
catch
块后面都会插入finally
块的代码来保证finally块能被执行到;学习到这你可能会想:如果在catch
块或者finally
块中再次发生异常;Java又如果处理呢?
public static void main(String[] args) throws NullPointerException {
try {
System.out.println("enter try block");
return;
} catch (Exception e) {
System.out.println("enter catch block");
throw new NullPointerException();
} finally {
System.out.println("enter finally block");
throw new NullPointerException();
}
}
字节码:
public static void main(java.lang.String[]) throws java.lang.NullPointerException;
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String enter try block
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #5 // String enter finally block
13: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: new #6 // class java/lang/NullPointerException
19: dup
20: invokespecial #7 // Method java/lang/NullPointerException."<init>":()V
23: athrow
24: astore_1
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
28: ldc #9 // String enter catch block
30: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: new #6 // class java/lang/NullPointerException
36: dup
37: invokespecial #7 // Method java/lang/NullPointerException."<init>":()V
40: athrow
41: astore_2
42: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
45: ldc #5 // String enter finally block
47: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
50: new #6 // class java/lang/NullPointerException
53: dup
54: invokespecial #7 // Method java/lang/NullPointerException."<init>":()V
57: athrow
Exception table:
from to target type
0 8 24 Class java/lang/Exception
0 8 41 any
24 42 41 any
}
与上面的代码的代码对比在catch
块多了抛出异常,在finally
块也添加了抛出异常。在字节码中@11~@23行间多出了几行:@16行使用关键字new
创建异常NullPointException,@19行使用指令dup
将新创建的变量入栈;@20调用NullPointException
的init
方法初始化实例;@23行中使用athrow
指令抛出异常;再看看@28~@40行也同样多了这些指令。
Exception table
,看了上面两字节码的代码;在最后都有一个Exception table
;根据命名不难理解,它就是传说中的异常表
作用是标示出代码中的那些行出现异常后跳转到那行执行,下面解析下Exception table
中各列的表示什么意思
- from: 监听异常的起点
- to: 监听异常的终点,不包含标示的本行
- target:发生异常后跳转执行的行
- type:监听的异常类型,
any
表示任何异常
Exception table
的每一行表示一个监听块,如上的第一行表示从字节码的第一行开始监听到第八行,监听的异常类型为Class java.lang.Exception
;出现异常时跳转至字节码24行开始执行。
下面来一一回顾并解答上面提的问题:
- Java是怎样处理异常的?
- 从字节码可以看出,Java使用
Exception table
在类文件中标示监听的异常块,通过Exception table
中的type
列进行异常类型的匹配,匹配时跳转到target
列指定的行继续执行。
- 从字节码可以看出,Java使用
- 发生异常是怎样保证
finally
块的代码一定会被调用?- Java通过在
try
catch
代码块的后面加入finally
块的代码,在最后return
指令前加入finally
块,再配合Exception table
中target
的方式来保证finally
块的调用
- Java通过在
- 异常信息在Java中又存储在那?
- 在Java中异常也被包装成一种
类
;异常发生后会将异常类的实例压入栈顶;再通过Exception table
匹配跳转的行执行;在catch
块执行前通过astore
指令将异常信息写入本地变量(赋值给e
这个变量)。这样分析下来异常是引用变量,异常的实例存储在堆中;在栈上存储引用(可以用C中的指针进行理解)。
- 在Java中异常也被包装成一种
throws 与 catch的顺序
分析完上面的问题后再来看看throws
,异常是如果向上传递以及多个catch
块执行顺序的分析
public static void main(String[] args) throws NullPointerException {
test();
}
public static void test() throws NullPointerException {
try {
System.out.println("enter try block");
throw new NullPointerException();
} catch (NullPointerException ne) {
System.out.println("enter catch block, NullPointerException");
} catch (Exception e) {
System.out.println("enter catch block");
throw new Exception();
} finally {
System.out.println("enter finally block");
}
}
public static void main(java.lang.String[]) throws java.lang.NullPointerException; Code: 0: invokestatic #2 // Method test:()V 3: return public static void test() throws java.lang.NullPointerException; Code: 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #4 // String enter try block 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: new #6 // class java/lang/NullPointerException 11: dup 12: invokespecial #7 // Method java/lang/NullPointerException."<init>":()V 15: athrow 16: astore_0 17: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 20: ldc #8 // String enter catch block, NullPointerException 22: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 25: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 28: ldc #9 // String enter finally block 30: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 33: goto 64 36: astore_0 37: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 40: ldc #11 // String enter catch block 42: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 45: new #6 // class java/lang/NullPointerException 48: dup 49: invokespecial #7 // Method java/lang/NullPointerException."<init>":()V 52: athrow 53: astore_1 54: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 57: ldc #9 // String enter finally block 59: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 62: aload_1 63: athrow 64: return Exception table: from to target type 0 16 16 Class java/lang/NullPointerException 0 16 36 Class java/lang/Exception 0 25 53 any 36 54 53 any
示例代码中添加了test
方法,方法上增加了throws NullPointerException
,增加了两个catch
块;用这份新的字节码和上面的字节码对比在test
方法后面增加了throws
;在Exception table
中增加了一行用于捕获NullPointerException
异常。
在try
中增加了NullPointerException
抛出异常的代码;在字节码中对应@8~@15行;在Exception table
中也的前两项也包含了try
这段代码块的异常检查。
catch
的检查顺序也是按Exception table
中的顺序进行匹配;如果出现的异常不是NullPointerException
Exception
这两个类型时将会被第三行的any
进行捕获后跳转至@53行执行:
- @53行
astore_1
将栈中的变量放入局部变量中,腾出栈中的位置 - @54~@59行执行
finally
中的输出语句 - @62行
aload_1
将局部变量中的值压入栈顶中 - @63行
athrow
将栈顶的异常变量抛出并回到调用点
从@53~@63这一段字节码可以看出finally
中并不会影响return的值;但如果改的是对象中的某个属性值这就不一样了。
最后再看看main
函数,main
函数中没有使用try...catch
块进行异常的捕获,而是通过throws
关键交由调用者处理;通过运行上面的代码可得知在发生异常后整个JVM进程将退款。
总结
经过上面对异常处理字节码的分析可以得出如下JVM异常处理流程:
- 在方法的调用中通过
Exception table
表进行捕获,并找到处理异常的catch
块代码 - 如果没有匹配到
catch
块将向上抛出异常交由调用者进行处理 - 在调用者中使用第一、第二步的方法进行处理
- 通过第三步处理完成后异常还是没有进行处理时将抛出出调用者的线程,线程将异常终止
- 如果抛出的线程为主线程则会导致主线程异常终止;JVM将会异常终止
推荐阅读:
Java String StringPool StringBuilder StringBuffer详解,面试不再难
Java 8种基本值类型
知识点:Java HashMap 原理与源码分析(下)