1. gzyueqian
      13352868059
      首頁 > 新聞中心 > > 正文

      提高代碼質(zhì)量及字節(jié)碼如何防止內(nèi)存錯(cuò)誤

      更新時(shí)間: 2008-05-07 15:57:29來源: 粵嵌教育瀏覽量:922

        【賽迪網(wǎng)技術(shù)社區(qū)整理】

        大多Java程序員知道他們的程序通常不會(huì)被編譯為本機(jī)代碼而是被編譯為由java虛擬機(jī)(JVM)執(zhí)行的字節(jié)碼格式。然而,很少有java程序員曾經(jīng)看過字節(jié)碼因?yàn)樗麄兊墓ぞ卟还膭?lì)他們?nèi)タ础4蠖郕ava 調(diào)試工具不允許單步執(zhí)行字節(jié)碼,它們要么顯示源代碼行,要么什么也不顯示。

        幸運(yùn)的是JDK提供了javap,一個(gè)命令行工具,它使得查看字節(jié)碼很容易。讓我們看一個(gè)范例:

        public class ByteCodeDemo {

        public static void main(String[] args) {

        System.out.println("Hello world");

        }

        }

        在編譯這個(gè)類后,你可以用十六進(jìn)制編輯器打開.class文件然后參照虛擬機(jī)規(guī)范翻譯字節(jié)碼。幸運(yùn)的是有更簡單的方法。JDK包含一個(gè)命令行的反匯編器:javap,它可以轉(zhuǎn)換字節(jié)碼為一種可讀的助記符形式,可以像下面這樣通過傳遞'-c'參數(shù)給javap得到字節(jié)碼列表:

        javap -c ByteCodeDemo

        你應(yīng)該會(huì)看到輸出類似這樣:

        public class ByteCodeDemo extends java.lang.Object {

        public ByteCodeDemo();

        public static void main(java.lang.String[]);

        }

        Method ByteCodeDemo()

        0 aload_0

        1 invokespecial #1

        4 return

        Method void main(java.lang.String[])

        0 getstatic #2

        3 ldc #3

        5 invokevirtual #4

        8 return

        僅僅從這個(gè)短小的列表你可以學(xué)到很多字節(jié)碼的知識。從main方法的個(gè)指令開始:

        0 getstatic #2

        開始的整數(shù)是方法中的指令的偏移值,因此個(gè)指令以0開始。緊隨偏移量是指令的助記符(mnemonic)。在這個(gè)范例中,'getstatic' 指令將一個(gè)靜態(tài)成員壓入一個(gè)稱為操作數(shù)堆棧的數(shù)據(jù)結(jié)構(gòu),后續(xù)的指令可以引用這個(gè)數(shù)據(jù)結(jié)構(gòu)中的成員。getstatic 指令后是要壓入的成員。在這個(gè)例子中,要壓入的成員是"#2 " 。如果你直接檢查字節(jié)碼,你會(huì)看到成員信息沒有直接嵌入指令而是像所有由java類使用的常量那樣存儲(chǔ)在一個(gè)共享池中。將成員信息存儲(chǔ)在一個(gè)常量池中可以減小字節(jié)碼指令的大小,因?yàn)橹噶钪恍枰鎯?chǔ)常量池中的一個(gè)索引而不是整個(gè)常量。在這個(gè)例子中,成員信息位于常量池中的#2處。常量池中的項(xiàng)目的順序是和編譯器相關(guān)的,因此在你的環(huán)境中看到的可能不是'#2' 。

        分析完個(gè)指令后很容易猜到其它指令的意思。'ldc' (load constant) 指令將常量"Hello, World."壓入操作數(shù)棧。'invokevirtual'指令調(diào)用println方法,它從操作數(shù)棧彈出它的兩個(gè)參數(shù)。不要忘記一個(gè)像println這樣的實(shí)例方法有兩個(gè)參數(shù):上面的字符串,加上隱含的'this'引用。

        字節(jié)碼如何預(yù)防內(nèi)存錯(cuò)誤

        Java語言經(jīng)常被吹捧為開發(fā)互聯(lián)網(wǎng)軟件的"安全的"語言。表面上和c++如此相似的代碼如何體現(xiàn)安全呢?它引入的一個(gè)重要的安全概念是防止內(nèi)存相關(guān)的錯(cuò)誤。計(jì)算機(jī)罪犯利用內(nèi)存錯(cuò)誤在其它情況下安全的程序中插入自己的惡意的代碼。Java字節(jié)碼是個(gè)可以預(yù)防這種攻擊的,像下面的范例展示的:

        public float add(float f, int n) {

        return f + n;

        }

        如果你將這個(gè)方法加入上面的范例中,重新編譯它,然后運(yùn)行javap,你將看到的字節(jié)碼類似這個(gè):

        Method float add(float, int)

        0 fload_1

        1 iload_2

        2 i2f

        3 fadd

        4 freturn

        在方法的開始,虛擬機(jī)將方法的參數(shù)放入一個(gè)稱為局部變量表的數(shù)據(jù)結(jié)構(gòu)中。將像名字暗示的那樣,局部變量表也包含了你聲明的任何局部變量。在這個(gè)例子中,方法以三個(gè)局部變量表的項(xiàng)開始,這些都是add方法的參數(shù),位置0保存this引用,而位置1和2分別保存float和int參數(shù)。

        為了實(shí)際的操作這些變量,它們必須被加載(壓入)到操作數(shù)棧。個(gè)指令fload_1將位置1處的float壓入操作數(shù)棧,第二個(gè)指令iload_2將位置2處的int壓入操作數(shù)棧。這些指令的一個(gè)引起注意的事情是指令中的'i'和'f'前綴,這說明Java字節(jié)碼指令是強(qiáng)類型的。如果參數(shù)的類型和字節(jié)碼的類型不匹配,VM將該字節(jié)碼作為不安全的而加以拒絕。更好的是,字節(jié)碼被設(shè)計(jì)為只需在類被加載時(shí)執(zhí)行一次這樣的類型安全檢查。

        這個(gè)類型安全是如何加強(qiáng)安全的?如果一個(gè)攻擊者能夠欺騙虛擬機(jī)將一個(gè)int作為一個(gè)float或者相反,它就可以很容易的以一個(gè)預(yù)期的的方法破壞計(jì)算。如果這些計(jì)算涉及銀行結(jié)余,那么隱含的安全性是很明顯的。更危險(xiǎn)的是欺騙VM將一個(gè)int作為一個(gè)Object引用。在大多情況下,這將導(dǎo)致VM崩潰,但是攻擊者只需要找到一個(gè)漏洞。不要忘記攻擊者不會(huì)手工搜索這個(gè)漏洞--寫出一個(gè)程序產(chǎn)生數(shù)以億計(jì)的錯(cuò)誤字節(jié)碼的排列是相當(dāng)容易的,這些排列試圖找到危害VM的幸運(yùn)的那個(gè)。
        字節(jié)碼的另一個(gè)內(nèi)存安全防護(hù)是數(shù)組操作。'aastore' 和 'aaload' 字節(jié)碼操作Java數(shù)組并且它們總是檢查數(shù)組邊界。如果調(diào)用程序越過了數(shù)組尾,這些字節(jié)碼將拋出一個(gè)ArrayIndexOutOfBoundsException。也許所有重要的檢查都使用分支指令,例如,以if開始的字節(jié)碼。在字節(jié)碼中,分支指令只能轉(zhuǎn)移到同一方法中的其它指令。在方法外可以傳遞的控制是使它返回:拋出一個(gè)異常或者執(zhí)行一個(gè)'invoke'指令。這不僅關(guān)閉了很多攻擊,同時(shí)也防止由于搖蕩引用(dangling reference)或者堆棧沖突而引發(fā)的令人厭惡的錯(cuò)誤。如果你曾經(jīng)使用系統(tǒng)調(diào)試器打開你的程序并定位到代碼中的一個(gè)隨機(jī)的位置,那么你會(huì)很熟悉這些錯(cuò)誤。

        所有這些檢查中需要記住的重要的一點(diǎn)是它們是由虛擬機(jī)在字節(jié)碼級進(jìn)行的而不是僅僅由編譯器在源代碼級進(jìn)行的。一個(gè)例如c++這樣的語言的編譯器可能在編譯時(shí)預(yù)防上面討論的某些內(nèi)存錯(cuò)誤,但是這些保護(hù)只是在源代碼級應(yīng)用。操作系統(tǒng)將很樂意加載執(zhí)行任何機(jī)器碼,無論這些代碼是由精細(xì)的c++編譯器產(chǎn)生的還是心懷惡意的攻擊者產(chǎn)生的。簡單的講,C++僅僅是在源代碼級上面向?qū)ο蠖鳭ava的面向?qū)ο蟮奶匦詳U(kuò)展到編譯過的代碼級。

        分析字節(jié)碼提升代碼質(zhì)量

        Java字節(jié)碼的內(nèi)存和安全保護(hù)無論我們是否注意都是存在地,那么我們?yōu)槭裁催€費(fèi)心查看字節(jié)碼呢?在很多情況下,知道編譯器如何將你的代碼轉(zhuǎn)換為字節(jié)碼可以幫助你寫出更高效的代碼,而且在某些情況下可以防止不易發(fā)覺的錯(cuò)誤。考慮下面的例子:

        //返回 str1+str2 的串連

        String concat(String str1, String str2) {

        return str1 + str2;

        }

        //將 str2 附加到 str1

        void concat(StringBuffer str1, String str2) {

        str1.append(str2);

        }

        猜猜每個(gè)方法需要多少個(gè)方法調(diào)用。現(xiàn)在編譯這些方法并且運(yùn)行javap,你會(huì)得到類似下面的輸出:

        Method java.lang.String concat1(java.lang.String, java.lang.String)

        0 new #5

        3 dup

        4 invokespecial #6
       
        7 aload_1

        8 invokevirtual #7

        11 aload_2

        12 invokevirtual #7

        15 invokevirtual #8

        18 areturn

        Method void concat2(java.lang.StringBuffer, java.lang.String)

        0 aload_1

        1 aload_2

        2 invokevirtual #7

        5 pop

        6 return

        concat1方法執(zhí)行了5個(gè)方法調(diào)用s: new, invokespecial和三個(gè)invokevirtuals,這比concat2方法執(zhí)行了更多的工作,后者只執(zhí)行了一個(gè)invokevirtual調(diào)用。大多Java程序員已經(jīng)得到過警告,因?yàn)镾tring是不可變的,而使用StringBuffer進(jìn)行字符串連接效率更高。使用javap分析這個(gè)使得這點(diǎn)變得很生動(dòng)。如果你不能肯定兩個(gè)語言構(gòu)造在性能上是否相等,你應(yīng)該使用javap分析字節(jié)碼。然而,對just-in-time (JIT)編譯器要小心,因?yàn)镴IT編譯器將字節(jié)碼重新編譯為本機(jī)代碼而能執(zhí)行一些javap不能揭示的附加優(yōu)化。除非你有你的虛擬機(jī)的源代碼,否則你應(yīng)該補(bǔ)充你的字節(jié)碼的基準(zhǔn)性能分析。

        的一個(gè)范例展示了檢查字節(jié)碼如何幫助防止程序中的錯(cuò)誤。像下面那樣創(chuàng)建兩個(gè)類,確保它們在獨(dú)立的文件中。

        public class ChangeALot {

        public static final boolean debug=false;

        public static boolean log=false;

        }

        public class EternallyConstant {

        public static void main(String [] args) {

        System.out.println("EternallyConstant beginning execution");

        if (ChangeALot.debug)

        System.out.println("Debug mode is on");

        if (ChangeALot.log)

        System.out.println("Logging mode is on");

        }

        }

        如果你運(yùn)行EternallyConstant,你會(huì)得到信息:

        EternallyConstant beginning execution.

        現(xiàn)在試著編輯ChangeALot,修改debug和log變量的值為true(兩個(gè)都為true)。只重新編譯ChangeALot。再次運(yùn)行EternallyConstant,你將看到下面的輸出:

        EternallyConstant beginning execution

       Logging mode is on

         debug變量怎么了?即使你將debug設(shè)置為true,信息"Debug mode is on"并沒有出現(xiàn)。答案在字節(jié)碼中。對 EternallyConstant運(yùn)行javap你會(huì)看到:

        Method void main(java.lang.String[])

        0 getstatic #2

        3 ldc #3

        5 invokevirtual #4

        8 getstatic #5

        11 ifeq 22

        14 getstatic #2

        17 ldc #6

        19 invokevirtual #4

        22 return

        驚奇吧!在log成員上有一個(gè)'ifeq'檢查,而代碼根本沒有檢查debug成員。因?yàn)閐ebug成員被標(biāo)記為final類型,編譯器知道debug成員在運(yùn)行時(shí)永遠(yuǎn)不會(huì)改變,因此它通過移除'if'聲明進(jìn)行優(yōu)化。這確實(shí)是一個(gè)非常有用的優(yōu)化,因?yàn)樗试S你在程序中嵌入調(diào)試代碼而在將它設(shè)置為false時(shí)不用付出運(yùn)行時(shí)的代價(jià)。不幸的是這個(gè)優(yōu)化能夠?qū)е轮饕木幾g時(shí)混亂。如果你改變一個(gè)final成員,你必須記住重新編譯任何可能引用該成員的類。這是因?yàn)檫@個(gè)'reference'可能已經(jīng)經(jīng)過優(yōu)化了。Java開發(fā)環(huán)境不能總是發(fā)現(xiàn)這個(gè)微妙的相關(guān)性,一些能導(dǎo)致非常奇怪的錯(cuò)誤。因此,古老的C++格言對于java環(huán)境仍然有效:"When in doubt, rebuild all."(有疑問,重新編譯所有的代碼)。

        知道一些字節(jié)碼的知識對于使用java編程的程序員都是有價(jià)值的。javap工具使得查看字節(jié)碼很容易。有時(shí)候,使用javap檢查你的代碼以期提高性能和捕獲特殊的不易察覺的錯(cuò)誤時(shí)是沒有用的。

      免費(fèi)預(yù)約試聽課

      亚洲另类欧美综合久久图片区_亚洲中文字幕日产无码2020_欧美日本一区二区三区桃色视频_亚洲AⅤ天堂一区二区三区

      
      

      1. 在线日本有码中文字幕 | 亚洲欧美中文字幕乱码在线 | 亚洲h成年动漫在线观看不卡 | 亚洲码欧美码一区二区 | 中文字幕不卡高清免费v | 亚洲国产精品久久久久秋霞1 |