大多Java程序員知道他們的程序通常不會被編譯為本機代碼而是被編譯為由java虛擬機(JVM)執行的字節碼格式然而很少有java程序員曾經看過字節碼因為他們的工具不鼓勵他們去看大多Java 調試工具不允許單步執行字節碼它們要麼顯示源代碼行要麼什麼也不顯示
幸運的是JDK提供了javap一個命令行工具它使得查看字節碼很容易讓我們看一個范例
public class ByteCodeDemo {
public static void main(String[] args) {
Systemoutprintln(Hello world)
}
}
在編譯這個類後你可以用十六進制編輯器打開class文件然後參照虛擬機規范翻譯字節碼幸運的是有更簡單的方法JDK包含一個命令行的反匯編器javap它可以轉換字節碼為一種可讀的助記符形式可以像下面這樣通過傳遞c參數給javap得到字節碼列表
javap c ByteCodeDemo
你應該會看到輸出類似這樣
public class ByteCodeDemo extends javalangObject {
public ByteCodeDemo()
public static void main(javalangString[])
}
Method ByteCodeDemo()
aload_
invokespecial #
return
Method void main(javalangString[])
getstatic #
ldc #
invokevirtual #
return
僅僅從這個短小的列表你可以學到很多字節碼的知識從main方法的第一個指令開始
getstatic #
開始的整數是方法中的指令的偏移值因此第一個指令以開始緊隨偏移量是指令的助記符(mnemonic)在這個范例中getstatic 指令將一個靜態成員壓入一個稱為操作數堆棧的數據結構後續的指令可以引用這個數據結構中的成員getstatic 指令後是要壓入的成員在這個例子中要壓入的成員是# 如果你直接檢查字節碼你會看到成員信息沒有直接嵌入指令而是像所有由java類使用的常量那樣存儲在一個共享池中將成員信息存儲在一個常量池中可以減小字節碼指令的大小因為指令只需要存儲常量池中的一個索引而不是整個常量在這個例子中成員信息位於常量池中的#處常量池中的項目的順序是和編譯器相關的因此在你的環境中看到的可能不是#
分析完第一個指令後很容易猜到其它指令的意思ldc (load constant) 指令將常量Hello World壓入操作數棧invokevirtual指令調用println方法它從操作數棧彈出它的兩個參數不要忘記一個像println這樣的實例方法有兩個參數上面的字符串加上隱含的this引用
字節碼如何預防內存錯誤
Java語言經常被吹捧為開發互聯網軟件的安全的語言表面上和c++如此相似的代碼如何體現安全呢?它引入的一個重要的安全概念是防止內存相關的錯誤計算機罪犯利用內存錯誤在其它情況下安全的程序中插入自己的惡意的代碼Java字節碼是第一個可以預防這種攻擊的像下面的范例展示的
public float add(float f int n) {
return f + n;
}
如果你將這個方法加入上面的范例中重新編譯它然後運行javap你將看到的字節碼類似這個
Method float add(float int)
fload_
iload_
if
fadd
freturn
在方法的開始虛擬機將方法的參數放入一個稱為局部變量表的數據結構中將像名字暗示的那樣局部變量表也包含了你聲明的任何局部變量在這個例子中方法以三個局部變量表的項開始這些都是add方法的參數位置保存this引用而位置和分別保存float和int參數
為了實際的操作這些變量它們必須被加載(壓入)到操作數棧第一個指令fload_將位置處的float壓入操作數棧第二個指令iload_將位置處的int壓入操作數棧這些指令的一個引起注意的事情是指令中的i和f前綴這說明Java字節碼指令是強類型的如果參數的類型和字節碼的類型不匹配VM將該字節碼作為不安全的而加以拒絕更好的是字節碼被設計為只需在類被加載時執行一次這樣的類型安全檢查
這個類型安全是如何加強安全的?如果一個攻擊者能夠欺騙虛擬機將一個int作為一個float或者相反它就可以很容易的以一個預期的的方法破壞計算如果這些計算涉及銀行結余那麼隱含的安全性是很明顯的更危險的是欺騙VM將一個int作為一個Object引用在大多情況下這將導致VM崩潰但是攻擊者只需要找到一個漏洞不要忘記攻擊者不會手工搜索這個漏洞寫出一個程序產生數以億計的錯誤字節碼的排列是相當容易的這些排列試圖找到危害VM的幸運的那個
字節碼的另一個內存安全防護是數組操作aastore 和 aaload 字節碼操作Java數組並且它們總是檢查數組邊界如果調用程序越過了數組尾這些字節碼將拋出一個ArrayIndexOutOfBoundsException也許所有最重要的檢查都使用分支指令例如以if開始的字節碼在字節碼中分支指令只能轉移到同一方法中的其它指令在方法外可以傳遞的唯一控制是使它返回拋出一個異常或者執行一個invoke指令這不僅關閉了很多攻擊同時也防止由於搖蕩引用(dangling reference)或者堆棧沖突而引發的令人厭惡的錯誤如果你曾經使用系統調試器打開你的程序並定位到代碼中的一個隨機的位置那麼你會很熟悉這些錯誤
所有這些檢查中需要記住的重要的一點是它們是由虛擬機在字節碼級進行的而不是僅僅由編譯器在源代碼級進行的一個例如c++這樣的語言的編譯器可能在編譯時預防上面討論的某些內存錯誤但是這些保護只是在源代碼級應用操作系統將很樂意加載執行任何機器碼無論這些代碼是由精細的c++編譯器產生的還是心懷惡意的攻擊者產生的簡單的講C++僅僅是在源代碼級上面向對象而Java的面向對象的特性擴展到編譯過的代碼級
分析字節碼提升代碼質量
Java字節碼的內存和安全保護無論我們是否注意都是存在地那麼我們為什麼還費心查看字節碼呢?在很多情況下知道編譯器如何將你的代碼轉換為字節碼可以幫助你寫出更高效的代碼而且在某些情況下可以防止不易發覺的錯誤考慮下面的例子
//返回 str+str 的串連
String concat(String str String str) {
return str + str;
}
//將 str 附加到 str
void concat(StringBuffer str String str) {
strappend(str)
}
猜猜每個方法需要多少個方法調用
現在編譯這些方法並且運行javap
你會得到類似下面的輸出
Method java
lang
String concat
(java
lang
String
java
lang
String)
new #
dup
invokespecial #
aload_
invokevirtual #
aload_
invokevirtual #
invokevirtual #
areturn
Method void concat
(java
lang
StringBuffer
java
lang
String)
aload_
aload_
invokevirtual #
pop
return
concat
方法執行了
個方法調用s: new
invokespecial和三個invokevirtuals
這比concat
方法執行了更多的工作
後者只執行了一個invokevirtual調用
大多Java程序員已經得到過警告
因為String是不可變的
而使用StringBuffer進行字符串連接效率更高
使用javap分析這個使得這點變得很生動
如果你不能肯定兩個語言構造在性能上是否相等
你應該使用javap分析字節碼
然而
對just
in
time (JIT)編譯器要小心
因為JIT編譯器將字節碼重新編譯為本機代碼而能執行一些javap不能揭示的附加優化
除非你有你的虛擬機的源代碼
否則你應該補充你的字節碼的基准性能分析
最後的一個范例展示了檢查字節碼如何幫助防止程序中的錯誤
像下面那樣創建兩個類
確保它們在獨立的文件中
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
)
}
}
如果你運行EternallyConstant
你會得到信息
EternallyConstant beginning execution
現在試著編輯ChangeALot
修改debug和log變量的值為true(兩個都為true)
只重新編譯ChangeALot
再次運行EternallyConstant
你將看到下面的輸出
EternallyConstant beginning execution
Logging mode is on
debug變量怎麼了?即使你將debug設置為true
信息
Debug mode is on
並沒有出現
答案在字節碼中
對 EternallyConstant運行javap你會看到
Method void main(java
lang
String[])
getstatic #
ldc #
invokevirtual #
getstatic #
ifeq
getstatic #
ldc #
invokevirtual #
return
驚奇吧!在log成員上有一個
ifeq
檢查
而代碼根本沒有檢查debug成員
因為debug成員被標記為final類型
編譯器知道debug成員在運行時永遠不會改變
因此它通過移除
if
聲明進行優化
這確實是一個非常有用的優化
因為它允許你在程序中嵌入調試代碼而在將它設置為false時不用付出運行時的代價
不幸的是這個優化能夠導致主要的編譯時混亂
如果你改變一個final成員
你必須記住重新編譯任何可能引用該成員的類
這是因為這個
reference
可能已經經過優化了
Java開發環境不能總是發現這個微妙的相關性
一些能導致非常奇怪的錯誤
因此
古老的C++格言對於java環境仍然有效
When in doubt
rebuild all
(有疑問
重新編譯所有的代碼)
知道一些字節碼的知識對於使用java編程的程序員都是有價值的
javap工具使得查看字節碼很容易
有時候
使用javap檢查你的代碼以期提高性能和捕獲特殊的不易察覺的錯誤時是沒有用的
From:http://tw.wingwit.com/Article/program/Java/hx/201311/26744.html