上善若水
          In general the OO style is to use a lot of little objects with a lot of little methods that give us a lot of plug points for overriding and variation. To do is to be -Nietzsche, To bei is to do -Kant, Do be do be do -Sinatra
          posts - 146,comments - 147,trackbacks - 0

          問題描述

          先來看一下以下的代碼,猜猜他們會是什么樣的結果:

            1 public class FinallyIssue {
            2     public static void main(String[] args) {
            3        System.out.println("finallyReturnTest : ");
            4        System.out.println("return value : " + finallyReturnTest(1));
            5        System.out.println("return value : " + finallyReturnTest(-1));
            6       
            7        System.out.println("finallyBreakTest : ");
            8        System.out.println("return value : " + finallyBreakTest(true));
            9        System.out.println("return value : " + finallyBreakTest(false));
           10       
           11        System.out.println("valueChangeInFinallyTest : ");
           12        System.out.println("return value : " + valueChangeInFinallyTest());
           13       
           14        System.out.println("valueChangeReturnInFinallyTest : ");
           15        System.out.println("return value : " + valueChangeReturnInFinallyTest());
           16       
           17        System.out.println("refValueChangeInFinallyTest : ");
           18        System.out.println("return name : " + refValueChangeInFinallyTest().name);
           19     }
           20    
           21     private static boolean finallyReturnTest(int value) {
           22        try {
           23            if(value > 0) {
           24               return true;
           25            } else {
           26               return false;
           27            }
           28        } finally {
           29            return false;
           30        }
           31     }
           32    
           33     private static boolean finallyBreakTest(boolean value) {
           34        while(value) {
           35            try {
           36               return true;
           37            } finally {
           38               break;
           39            }
           40        }
           41        return false;
           42     }
           43    
           44     private static int valueChangeInFinallyTest() {
           45        int i = 10;
           46        int j = 1;
           47        try {
           48            i = 100;
           49            j = 2;
           50            System.out.println("try : i = " + i);
           51            System.out.println("try : j = " + j);
           52            return i;
           53        } catch(Exception e) {
           54            e.printStackTrace();
           55        } finally {
           56            i = 1000;
           57            j = 3;
           58            System.out.println("finally : i = " + i);
           59            System.out.println("finally : j = " + j);
           60        }
           61       
           62        return i;
           63     }
           64    
           65     private static int valueChangeReturnInFinallyTest() {
           66        int i = 10;
           67        int j = 1;
           68        try {
           69            i = 100;
           70            j = 2;
           71            System.out.println("try : i = " + i);
           72            System.out.println("try : j = " + j);
           73            return i;
           74        } catch(Exception e) {
           75            e.printStackTrace();
           76        } finally {
           77            i = 1000;
           78            j = 3;
           79            System.out.println("finally : i = " + i);
           80            System.out.println("finally : j = " + j);
           81            return i;
           82        }
           83     }
           84    
           85     private static Person refValueChangeInFinallyTest() {
           86        Person p = new Person();
           87        try {
           88            p.name = "person1";
           89            System.out.println("try : Person name is : " + p.name);
           90            return p;
           91        } catch(Exception e) {
           92            e.printStackTrace();
           93        } finally {
           94            p.name = "person2";
           95            System.out.println("finally : Person name is : " + p.name);
           96        }
           97       
           98        p.name = "person3";
           99        System.out.println("out : Person name is : " + p.name);
          100       
          101        return p;
          102     }
          103    
          104     static class Person {
          105        public String name;
          106     }
          107 }

          這樣一段代碼的結果會是什么呢?

          以下是運行結果:

          finallyReturnTest :

          return value : false

          return value : false

          finallyBreakTest :

          return value : false

          return value : false

          valueChangeInFinallyTest :

          try : i = 100

          try : j = 2

          finally : i = 1000

          finally : j = 3

          return value : 100

          valueChangeReturnInFinallyTest :

          try : i = 100

          try : j = 2

          finally : i = 1000

          finally : j = 3

          return value : 1000

          refValueChangeInFinallyTest :

          try : Person name is : person1

          finally : Person name is : person2

          return name : person2

           

          這個結果很出乎我的意料,我們知道finally總是會在try-catch語句塊執行完后執行,不管try語句塊中是否已經返回或者拋出了異常。

           

          但是在上面的代碼測試中,如果finally語句塊中有return、break、continue等語句,那么它們會覆蓋try語句塊中的returnbreakcontinue的語句,如以上的finallyReturnTest()、finallyBreakTest()、valueChangeReturnInFinallyTest()三個函數。

          另外,如果在finally語句塊中修改要返回的值類型變量的值,則這些修改不會保存下來,如valueChangeInFinallyTest()函數;如果要返回的值是引用類型,則修改引用類型的內部成員的值會保存下來。

          如何解釋這個結果呢?

           

          問題解釋

          結合《深入Java虛擬機(第二版)》這本書和代碼編譯后產生的二進制指令代碼,我對以上問題做了部分解釋,鑒于我的才疏學淺,有些觀點是有誤的,希望高手指正(有誤的觀點容易引起誤導,這也是所以我一直非常小心,奈何水平有限,有些時候難免出錯)。

           

          在《深入Java虛擬機(第二版)》的第18章中提到,在早期的Java中,finally的行為是通過JSR指令來實現的,并且為這個指令引入了微型子程序的概念。我的理解,所謂微型子程序就是在函數A中嵌入一個不完整的函數B的調用。比如在這本書上的一個例子:

              private static int microSubroutine(boolean bValue) {
                 
          try {
                     
          if(bValue) {
                        
          return 1;
                     }
                     
          return 0;
                 } 
          finally {
                     System.out.println(
          "finally");
                 }
              }

          會生成以下的二進制代碼:

           0 iload_0

           1 ifeq 11

           4 iconst_1

           5 istore_1

           6 jsr 24

           9 iload_1

          10 ireturn

          11 iconst_0

          12 istore_1

          13 jsr 24

          16 iload_1

          17 ireturn

          18 astore_2

          19 jsr 24

          22 aload_2

          23 athrow

           

          24 astore_3

          25 getstatic #7 <Field java.io.PrintStream out>

          28 ldc #1 <String “finally”>

          30 invokevirtual #8 <Method void println(java.lang.String)>

          33 ret 3

           

          如上,24前綴的代碼行以后的部分就是微型子程序,在每一個出口之前都會用JSR調用這個微型子例程序,在這個微型子例程序返回(ret)后,返回調用JSR指令的下一條指令,然后返回(ireturn、athrow)。

          jsr指令和ret指令的格式如下:

          jsr    branchbyte1, branchbyte2

          把返回地址壓棧,跳轉至((branchbyte1<<8) | branchbyte2)的位置繼續之行。

          ret index

          返回在index指示的局部變量中存儲的值(位置)。

           

          在上面的二進制代碼中,每次通過jsr 24跳轉到微型子程序,它先將返回地址(jsr 24指令的下一條指令的地址)保存在index3的局部變量中,執行完微型子程序后,通過ret 3返回到調用jsr 24指令的下一條指令執行,并最終執行返回。

           

          可是后來(有人說是自1.4.2后),JVM中取消了jsr指令了,所有finally內部的代碼都內聯到源代碼中了(二進制的源代碼)。所以以上的代碼在之后的編譯器中會產生如下的二進制代碼:

               0 iload_0 [bValue]

               1 ifeq 14

               4 getstatic java.lang.System.out : java.io.PrintStream [16]

               7 ldc <String "finally"> [94]

               9 invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]

              12 iconst_1

          13 ireturn

           

              14 getstatic java.lang.System.out : java.io.PrintStream [16]

              17 ldc <String "finally"> [94]

              19 invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]

              22 iconst_0

          23 ireturn

           

              24 astore_1

              25 getstatic java.lang.System.out : java.io.PrintStream [16]

              28 ldc <String "finally"> [94]

              30 invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]

              33 aload_1

              34 athrow

           

          額,貌似有點偏題了,以上的描述是為了解釋《深入Java虛擬機(第二版)》中對finally描述過時的描述。下面讓我們來真正的解決這個問題。還是從生成的Java二進制代碼入手。

           

          首先來看一下valueChangeInFinallyTest()函數的二進制代碼(注釋了打印語句,使代碼簡潔):

                   //int i = 10

               0 bipush 10

               2 istore_0 [i]

                 //int j = 1

               3 iconst_1

               4 istore_1 [j]

                 //i = 100

               5 bipush 100

               7 istore_0 [i]

                 //j = 2

               8 iconst_2

               9 istore_1 [j]

                 //保存i的值,因為它是要返回的

              10 iload_0 [i]

          11 istore 4

          //--------------------------------內聯finally語句塊(開始)----------------------

          //i = 1000

              13 sipush 1000

          16 istore_0 [i]

          //j = 3

              17 iconst_3

          18 istore_1 [j]

          //--------------------------------內聯finally語句塊(結束)----------------------

          //加載保存后的i的值,并返回。這里返回的是finally語句塊執行前的i(由istore 4語句緩存起來)的值,因而在finally語句塊中任何對i的操作并不會保留下來。這是在沒有異常發生的情況下。

              19 iload 4

          21 ireturn

           

              22 astore_2 [e]

              23 aload_2 [e]

          24 invokevirtual java.lang.Exception.printStackTrace() : void [104]

          //--------------------------------內聯finally語句塊(開始)----------------------

              27 sipush 1000

              30 istore_0 [i]

              31 iconst_3

          32 istore_1 [j]

          //--------------------------------內聯finally語句塊(結束)----------------------

          33 goto 45

           

          36 astore_3

          //--------------------------------內聯finally語句塊(開始)----------------------

              37 sipush 1000

              40 istore_0 [i]

              41 iconst_3

          42 istore_1 [j]

          //--------------------------------內聯finally語句塊(結束)----------------------

          //而在異常發生但沒有被正確處理的情況下,返回值已經沒有什么意義了。

              43 aload_3

          44 athrow

           

          //這里是在有異常發生,并且異常得到了正確處理的情況下返回的,此時在finally語句塊中對i的操作就會保存下來,并返回給調用者。

              45 iload_0 [i]

              46 ireturn

          相信以上的注釋已經能很好的的解決這個問題了(注:這里j的存在是為了證明在內聯finally語句塊的時候,它只緩存返回值i,而無須緩存其他變量的值,如j的值)。需要特別注意的一點是,如果正常返回的話,finally語句塊中修改i的值是保存不下來的,但是如果出現異常,并被正常捕獲后,在finally語句塊中修改的i的值就會保存下來了。

           

          那么對valueChangeReturnInFinallyTest()函數中的現象如何解釋呢?對這個問題解釋,首先要理解ireturn的指令。ireturn指令沒有操作數,它把當前操作棧的棧頂的int值作為默認的操作數。ireturn指令會彈出當前棧頂的int值,將其壓入調用者的操作棧中,同時忽略當前操作棧中的其他值,即函數正常返回。因而如果在不優化的情況下,在finally語句塊中的return語句會返回當前棧頂的int值(修改后的i值),然后函數返回,此時棧上的其他操作數就被忽略了,并且原本應該執行的ireturn語句也不會之行了。這種方式甚至會忽略拋出的異常,即使當前方法有異常拋出,它的調用方法還是認為它正常返回了。

          如果查看優化后的valueChangeReturnInFinallyTest()方法的二進制源碼后,會發現當前的代碼更加簡潔了。但是它還是沒有避免在finally語句塊中使用return后,會忽略沒有捕獲到的異常的問題。

                   //int i = 10

               0 bipush 10

               2 istore_0 [i]

                 //int j = 1

               3 iconst_1

               4 istore_1 [j]

                 //i = 100

               5 bipush 100

               7 istore_0 [i]

                 //j = 2

               8 iconst_2

               9 istore_1 [j]

          10 goto 22

          //catch block

              13 astore_2 [e]

              14 aload_2 [e]

              15 invokevirtual java.lang.Exception.printStackTrace() : void [104]

              18 goto 22

          21 pop

          //--------------------------------內聯finally語句塊(開始)----------------------

          //i = 100

              22 sipush 1000

          25 istore_0 [i]

          //j = 3

              26 iconst_3

          27 istore_1 [j]

          //--------------------------------內聯finally語句塊(結束)----------------------

          //返回finally語句塊中i的值

              28 iload_0 [i]

              29 ireturn

          經過以上的解釋,我想對refValueChangeInFinallyTest()函數中的現象就比較好解釋了,因為當進入finally語句塊的時候,保存的只是Person實例的一個引用,在finally語句塊中依然可以通過引用操作Person內部成員的,因而在finally語句塊中的修改才能保存下來。

           

          而經過編譯器優化后的finallyReturnTest()finallyBreakTest()函數生成的二進制代碼就成一樣的了:

               0 iload_0 [value]

               1 ifeq 8

               4 goto 8

               7 pop

               8 iconst_0

               9 ireturn

           

          后記

          原本以為這是一個小問題的,沒想到花了我一個下午的時間才把問題說清楚了,而在描述問題的過程中,我對問題的本質也看的更加清晰了。這個問題開始是源于我在論壇http://www.javaeye.com/topic/458668中看到,感覺論壇里面的人都沒很好的說清楚這個問題,剛好我看完了《深入Java虛擬機(第二版)》的書,就把這個問題完整的描述出來了。

                                                                                                                                                                                   于2010年9月24日

          注:這些文章都是前些時候寫的,之前博客很亂,也都是隨便貼一些自己寫的或轉載的,還有一些則是沒有貼出來過的?,F在打算好好整理一下,完整的記錄自己的一些學習歷程,而每次看到過去的時間,則讓我想起以前的日子,因而我對時間一直是很重視的,所以每篇都著名寫的日期,直到最先的文章出現。:)
          posted on 2011-06-24 22:37 DLevin 閱讀(2508) 評論(5)  編輯  收藏

          FeedBack:
          # re: finally知多少
          2011-06-25 22:17 | CodePro
          不錯,很深入  回復  更多評論
            
          # re: finally知多少
          2011-06-28 09:38 | hongfeng-Maple
          還是很不錯的哈  回復  更多評論
            
          # re: finally知多少
          2011-06-28 14:44 | 雷斌輝
          不錯 不錯  回復  更多評論
            
          # re: finally知多少[未登錄]
          2011-10-16 18:18 | chris
          這些天在學習字節碼相關的東東,很感謝樓主的分享,采用JDK1.6編譯了一下樓主的代碼,發現一下詭異的問題:
          //加載保存后的i的值,并返回。這里返回的是finally語句塊執行前的i(由istore 4語句緩存起來)的值,因而在finally語句塊中任何對i的操作并不會保留下來。這是在沒有異常發生的情況下。

          19 iload 4
          21 ireturn

          我的控制臺顯示的iload0, 就是說返回的是經過finally語句賦值后的i, 而并不是在try塊中的i, 在eclipse調試運行得出的也是相同的結果,不知是怎么回事  回復  更多評論
            
          # re: finally知多少[未登錄]
          2011-10-16 18:37 | chris
          抱歉,樓主是對的,我運行的是函數valueChangeReturnInFinallyTest,差別是如果finally里面有return語句的情況下,最終返回的是經過finally修改的值,在此情況下執行的字節碼為:
          iload0
          ireturn
          否則返回的為try塊中的值,執行的字節碼為
          iload4
          ireturn  回復  更多評論
            

          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          主站蜘蛛池模板: 博湖县| 开平市| 鹤峰县| 新巴尔虎左旗| 新昌县| 江源县| 南华县| 客服| 永城市| 墨脱县| 敖汉旗| 江孜县| 从化市| 马公市| 谷城县| 宜章县| 西平县| 黎川县| 政和县| 仲巴县| 宜昌市| 太湖县| 诸城市| 井陉县| 法库县| 沛县| 威信县| 大冶市| 三都| 饶平县| 白城市| 萨嘎县| 临汾市| 蒲江县| 固镇县| 胶州市| 青神县| 新营市| 如皋市| 阿拉善左旗| 弥勒县|