Java中異常Exception的實現的一些分析
文章地址:https://blog.csdn.net/hengyunabc/article/details/14108617
前言:
最近發現一個很有用的Eclipse插件:https://andrei.gmxhome.de/bytecode/,可以在Eclipse直接查看,調試Java的字節碼。
順帶研究了下Java裏異常的實現機製,還有JDK7裏的mutil catch的實現原理。
athrow指令:
在JVM裏實現異常的指令是athrow,指令的參考在這裏:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
大段的英文就不粘貼過來了:)。
個人理解:JVM是基於所謂的棧幀的(stack frame)的,一個函數調用鏈就是一個個棧幀組成,當在一個棧裏用athrow拋出異常時,JVM會搜索當前函數的異常處理表(參考下麵的Class文件分析),如果有找到對應的異常處理的handler,則由這個handler來處理。如果沒有,則清理當前棧,再回到上一層棧幀中處理。如果一層層棧幀回退,最終都沒有找到Exception Handler,則線程終止。
下麵貼點實際代碼:
一個簡單的函數:
public void testFunc(int i) throws NamingException, XPathException, SQLException {
if (i == 3) {
throw new XPathException("");
}
if (i == 4) {
throw new SQLException();
}
if (i == 5) {
throw new NamingException();
}
}
前麵提到的ByteCode插件給出的分析:
// access flags 0x1
public testFunc(I)V throws javax/naming/NamingException javax/xml/xpath/XPathException java/sql/SQLException
L0
LINENUMBER 31 L0
ILOAD 1
ICONST_3
IF_ICMPNE L1
L2
LINENUMBER 32 L2
NEW javax/xml/xpath/XPathException
DUP
LDC ""
INVOKESPECIAL javax/xml/xpath/XPathException.<init>(Ljava/lang/String;)V
ATHROW
L1
LINENUMBER 34 L1
FRAME SAME
ILOAD 1
ICONST_4
IF_ICMPNE L3
L4
LINENUMBER 35 L4
NEW java/sql/SQLException
DUP
INVOKESPECIAL java/sql/SQLException.<init>()V
ATHROW
L3
LINENUMBER 37 L3
FRAME SAME
ILOAD 1
ICONST_5
IF_ICMPNE L5
L6
LINENUMBER 38 L6
NEW javax/naming/NamingException
DUP
INVOKESPECIAL javax/naming/NamingException.<init>()V
ATHROW
L5
LINENUMBER 40 L5
FRAME SAME
RETURN
L7
LOCALVARIABLE this LTest; L0 L7 0
LOCALVARIABLE i I L0 L7 1
MAXSTACK = 3
MAXLOCALS = 2
如果對匯編有一定了解的話,可以很容易看到,在Java裏,拋出一個異常真的是非常簡單的:
先New一個異常對象,再把這個對象的引用放到棧頂,再用athrow指令拋出這個異常。
catch塊的實現:
那下麵來看下,從指令層麵,是如何處理這個異常的:
首先,處理這個異常的函數:
public void test() {
try {
testFunc(100);
} catch (NamingException e) {
// TODO Auto-generated catch block
System.out.println("11111");
} catch (XPathException e) {
// TODO Auto-generated catch block
System.out.println("22222");
} catch (SQLException e) {
// TODO Auto-generated catch block
System.out.println("33333");
}
}
ByteCode插件給出的分析:
// access flags 0x1
public test()V
TRYCATCHBLOCK L0 L1 L2 javax/naming/NamingException
TRYCATCHBLOCK L0 L1 L3 javax/xml/xpath/XPathException
TRYCATCHBLOCK L0 L1 L4 java/sql/SQLException
L0
LINENUMBER 44 L0
ALOAD 0
BIPUSH 100
INVOKEVIRTUAL Test.testFunc(I)V
L1
LINENUMBER 45 L1
GOTO L5
L2
FRAME SAME1 javax/naming/NamingException
ASTORE 1
L6
LINENUMBER 47 L6
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "11111"
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V
L7
GOTO L5
L3
LINENUMBER 48 L3
FRAME SAME1 javax/xml/xpath/XPathException
ASTORE 1
L8
LINENUMBER 50 L8
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "22222"
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V
L9
GOTO L5
L4
LINENUMBER 51 L4
FRAME SAME1 java/sql/SQLException
ASTORE 1
L10
LINENUMBER 53 L10
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "33333"
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V
L5
LINENUMBER 55 L5
FRAME SAME
RETURN
L11
LOCALVARIABLE this LTest; L0 L11 0
LOCALVARIABLE e Ljavax/naming/NamingException; L6 L7 1
LOCALVARIABLE e Ljavax/xml/xpath/XPathException; L8 L9 1
LOCALVARIABLE e Ljava/sql/SQLException; L10 L5 1
MAXSTACK = 2
MAXLOCALS = 2
可以看到最開始部分有三條TRYCATCHBLOCK,再分析下這些TRYCATCHBLOCK後麵跟著三個標簽,最後還有一個異常的名字,再仔細分析下,可以發現三個標簽分別對應try塊開始的地方,try塊結束的地方,catch塊開始的地方。這個實際上就是所謂的Execption Table。
class文件格式分析:
另外,在Class文件的格式裏,我們也可以看到Method的Execption Table。可以看出一個條目有四個元素組成:
start_pc, end_pc, handler_pc, catch_type。顯然這些異常表裏的數據是和代碼位置有關的,和我們上麵看到的一致。
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
所以,我們可以看到,test()函數調用了testFunc()函數,那麼,當testFunc()函數裏拋出異常時,JVM先回退到test()函數的棧幀,再從Execption Table裏查找是否有合適的Execption Hanler,查找首先當前的pc(program counter)要在start_pc, end_pc之間,而且異常的名字要匹配(當然這個應該會被優化成常量的比較,即一個long的比較,不會真的去比較字符串)。如果找到,則跳到對應的handler_pc處繼續執行。
finally塊的實現:
下麵再來看下Finally塊到底是怎麼實現的:
在代碼裏增加finally塊:
public void test2() {
try {
testFunc(100);
} catch (NamingException e) {
// TODO Auto-generated catch block
System.out.println("11111");
} catch (XPathException e) {
// TODO Auto-generated catch block
System.out.println("22222");
} catch (SQLException e) {
// TODO Auto-generated catch block
System.out.println("33333");
}finally {
System.out.println("xxxxxxxxxxxx");
}
}
ByteCode插件的分析:
// access flags 0x1
public test2()V
TRYCATCHBLOCK L0 L1 L2 javax/naming/NamingException
TRYCATCHBLOCK L0 L1 L3 javax/xml/xpath/XPathException
TRYCATCHBLOCK L0 L1 L4 java/sql/SQLException
TRYCATCHBLOCK L0 L5 L6
TRYCATCHBLOCK L3 L7 L6
TRYCATCHBLOCK L4 L8 L6
L0
LINENUMBER 60 L0
ALOAD 0
BIPUSH 100
INVOKEVIRTUAL Test.testFunc(I)V
L1
LINENUMBER 61 L1
GOTO L9
L2
FRAME SAME1 javax/naming/NamingException
ASTORE 1
L10
LINENUMBER 63 L10
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "11111"
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V
L5
LINENUMBER 71 L5
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "xxxxxxxxxxxx"
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V
GOTO L11
L3
LINENUMBER 64 L3
FRAME SAME1 javax/xml/xpath/XPathException
ASTORE 1
L12
LINENUMBER 66 L12
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "22222"
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V
L7
LINENUMBER 71 L7
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "xxxxxxxxxxxx"
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V
GOTO L11
L4
LINENUMBER 67 L4
FRAME SAME1 java/sql/SQLException
ASTORE 1
L13
LINENUMBER 69 L13
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "33333"
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V
L8
LINENUMBER 71 L8
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "xxxxxxxxxxxx"
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V
GOTO L11
L6
LINENUMBER 70 L6
FRAME SAME1 java/lang/Throwable
ASTORE 2
L14
LINENUMBER 71 L14
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "xxxxxxxxxxxx"
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V
L15
LINENUMBER 72 L15
ALOAD 2
ATHROW
L9
LINENUMBER 71 L9
FRAME SAME
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "xxxxxxxxxxxx"
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V
L11
LINENUMBER 73 L11
FRAME SAME
RETURN
L16
LOCALVARIABLE this LTest; L0 L16 0
LOCALVARIABLE e Ljavax/naming/NamingException; L10 L5 1
LOCALVARIABLE e Ljavax/xml/xpath/XPathException; L12 L7 1
LOCALVARIABLE e Ljava/sql/SQLException; L13 L8 1
MAXSTACK = 2
MAXLOCALS = 3
我們可以很神奇的發現,finally塊的代碼在每一個catch後麵都有一份。也就是說finally的實現有點像內聯優化,把代碼複製了很多份。
JDK7中mutil catch的實現:
我們再來看下JDK7新增的mutil catch語法的實現:
public void test3() {
try {
testFunc(100);
} catch (NamingException | XPathException | SQLException e) {
e.printStackTrace();
}
}
ByteCode插件的分析:
public test3()V
TRYCATCHBLOCK L0 L1 L2 javax/naming/NamingException
TRYCATCHBLOCK L0 L1 L2 javax/xml/xpath/XPathException
TRYCATCHBLOCK L0 L1 L2 java/sql/SQLException
L0
LINENUMBER 77 L0
ALOAD 0
BIPUSH 100
INVOKEVIRTUAL Test.testFunc(I)V
L1
LINENUMBER 78 L1
GOTO L3
L2
FRAME SAME1 java/lang/Exception
ASTORE 1
L4
LINENUMBER 79 L4
ALOAD 1
INVOKEVIRTUAL java/lang/Exception.printStackTrace()V
L3
LINENUMBER 81 L3
FRAME SAME
RETURN
L5
LOCALVARIABLE this LTest; L0 L5 0
LOCALVARIABLE e Ljava/lang/Exception; L4 L3 1
MAXSTACK = 2
MAXLOCALS = 2
我們可以發現,每一個TRYCATCHBLOCK的配置都是一樣的,隻是異常的名字不一樣。所以實際上mutil catch的實現和普通的實現沒有太大的區別,當然從JVM的實現角度來看,mutil catch有可能可以優化Exception Handler的查找過程(純猜測的,如果是線性查找,則效率是一樣的)。不過有好處是可以減少class文件的體積,這個也比較有用,因為目前Java的class文件的大小是有限製的。參考這裏;https://stackoverflow.com/questions/5497495/maximum-size-of-java-class-exception-table
總結:
Java中的異常的實現不是什麼太神秘的東東,和人們的直覺的實現差不多。任何編程語言的異常機製都會有一定的開銷,但是異常如果沒有觸發,實際上是沒有開銷的。
異常在觸發時,要new一個異常對象,再一層層地棧幀回退,每層都要查找異常處理表,開銷還是比較大的。
所在異常隻應該用在合適的地方,如果異常像Switch那樣用,那就悲劇了。
參考:
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
https://andrei.gmxhome.de/bytecode/ 非常有用的分析Java 匯編代碼的Eclipse插件
最後更新:2017-04-03 14:54:00