【賽迪網(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í)是沒有用的。
提高代碼質(zhì)量及字節(jié)碼如何防止內(nèi)存錯(cuò)誤
更新時(shí)間: 2008-05-07 15:57:29來源: 粵嵌教育瀏覽量:922