程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 深入Java編程:Java的字節代碼

深入Java編程:Java的字節代碼

編輯:關於JAVA

Java程序員很少注意程序的編譯結果。事實上,Java的字節代碼向我們提供了 非常有價值的信息。特別是在調試排除Java性能問題時,編譯結果讓我們可以更 深入地理解如何提高程序執行的效率等問題。其實JDK使我們研究Java字節代碼變 得非常容易。本文闡述怎樣利用JDK中的工具查看解釋Java字節代碼,主要包含以 下方面的一些內容:

l Java類分解器——javap

l Java字節代碼是怎樣使程序 避免程序的內存錯誤

l 怎樣通過分析字節代碼來提高程序的執行效率

l 利用第三方工具反編譯Java字節代碼

一、Java類分解器 ——javap

大多數Java程序員知道他們的程序不是編譯成本機 代碼的。實際上,程序被編譯成中間字節代碼,由Java虛擬機來解釋執行。然而 ,很少程序員注意一下字節代碼,因為他們使用的工具不鼓勵他們這樣做。大多 數的Java調試工具不允許單步的字節代碼調試。這些工具要麼顯示源代碼,要麼 什麼都不顯示。

幸好JDK提供了Java類分解器javap,一個命令行工具。 javap對類名給定的文件(.class)提供的字節代碼進行反編譯,打印出這些類的 一個可讀版本。在缺省情況下,javap打印出給定類內的公共域、方法、構造函數 ,以及靜態初始值。

1.javap的具體用法

語法: javap <選項 > <類名>...

其中選項包括:

參數 含義 b 向後兼容JDK 1.1中的 javap c 反編譯代碼,打印出每個給定類中方法的 Java虛擬機指令。使用該選項後,將對包括私有及受保護方法在內的所有方法進 行反編譯 classpath <pathlist> 指明到哪裡 查找用戶的類文件。這個選項值覆蓋了缺少路徑以及由CLASSPATH環境變量定義的 路徑。此處給出的路徑是一個目錄及zip文件有序列表,其元素在Unix中以 “:”,在Windows中以“;”分隔。要想在不覆蓋缺省系統 類路徑的情況下增加一些要查找的目錄或zip文件,應使用CLASSPATH環境變量, 使用方法與編譯器的-classpath相同。 extdirs <dirs> 覆蓋安裝擴展目錄 help 顯示幫助信息 J<flag> 將<flag>直接傳遞給運行系 統 l  在原來打印信息的基礎上,增加行號和局部 變量表 public 只顯示公共類及其成員 protected 顯示受保護/公共類及其成員 package 顯示包受保護/公共類及其成員(缺省) private 顯示所有類及其成員 s 打印內部類型標記 bootclasspath <pathlist> 覆蓋由引導類加 載器加載的類文件位置 verbose 打印堆棧大小,方 法的局部變量和參數的數目。若可驗證,打印出錯原因

2.應用實例

讓我們來看一個例子來進一步說 明如何使用javap。

// Imports
import  java.lang.String;

public class ExampleOfByteCode {
  // Constructors
  public ExampleOfByteCode() { }

  // Methods
  public static void main(String[]  args) {
    System.out.println("Hello world");
  }
}

編譯好這個類以後,可以用一個十六進制編輯器打開.class文 件,再通過虛擬機說明規范來解釋字節代碼的含義,但這並不是好方法。利用 javap,可以將字節代碼轉換成人們可以閱讀的文字,只要加上-c參數:

javap -c ExampleOfByteCode

輸出結果如下:

Compiled from ExampleOfByteCode.java
public class  ExampleOfByteCode extends java.lang.Object {
    public  ExampleOfByteCode();
    public static void main (java.lang.String[]);
}

Method ExampleOfByteCode()
   0 aload_0
   1 invokespecial #6 <Method  java.lang.Object()>
   4 return

Method void  main(java.lang.String[])
   0 getstatic #7 <Field  java.io.PrintStream out>
   3 ldc #1 <String "Hello  world">
   5 invokevirtual #8 <Method void  println(java.lang.String)>
   8 return

從以上 短短的幾行輸出代碼中,可以學到關於字節代碼的許多知識。在main方法的第一 句指令是這樣的:

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

開頭的初始數字是指令在方法中的偏移,所以第一個指令的偏移 是0。緊跟偏移的是指令助記符。在本例中,getstatic指令將一個靜態字段壓入 一個數據結構,我們稱這個數據結構為操作數堆棧。後續指令可以通過此結構引 用這個字段。緊跟getstatic指令後面的是壓到哪個字段中去。這裡的字段是 “#7 <Field java.io.PrintStream out>”。如果直接察看字 節代碼,這些字段信息並沒有直接存放到指令中去。事實上,就象所有Java類使 用的常量一樣,字段信息存儲在共享池中。在共享池中存儲字段信息可以減小字 節代碼的大小。這是因為指令僅僅需要存儲的是整型索引號,而不是將整個常量 存儲到常量池中。本例中,字段信息存放在常量池的第七號位置。存放的次序是 由編譯器決定的,所以看到的是“#7”。

通過分析第一行指令 ,我們可以看出猜測其它指令的含義還是比較簡單的。“ldc”(載入 常量)指令將常量“Hello, World.”壓入操作數堆棧。 “invokevirtual ”激發println方法,此方法從操作數堆棧中彈出 兩個參數。不要忘記象println這樣的方法有兩個參數:明顯的一個是字符串參數 ,加上一個隱含的“this”引用。

二、Java字節代碼是怎樣使 程序避免程序的內存錯誤

Java程序設計語言一直被稱為internet的安全語 言。從表面上看,這些代碼象典型的C++代碼,安全從何而來?安全的重要方面是 避免程序的內存錯誤。計算機罪犯利用程序的內存錯誤可以將他們的非法代碼加 到其它安全的程序中去。Java字節代碼是站在第一線抵御這種攻擊的

1.類 型安全檢測實例

以下的例子可以說明Java具體是怎樣做的。

public float add(float f, int n) {
return f +  n;
}

如果你將這段代碼加到第一個例子中去,重新編譯, 運行javap,分析情況如下:

Method float add(float, int)
   0 fload_1
   1 iload_2
   2 i2f
    3 fadd
   4 freturn

在Java方法的開頭,虛擬機 將方法的參數放到一個被稱為舉辦變量表的數據結構中。從名字就可以看出,局 部變量表包含所有聲明的局部變量。在本例中,方法從三個局部變量表實體開始 ,這些是add方法的三個參數。位置0保存該方法返回類型,位置1和2保存浮點和 整型參數。

為了真正操縱變量,它們必須被裝載(壓)到操作數堆棧。第 一條指令fload_1將浮點參數壓到操作數堆棧的位置1。第二條指令iload_2將整型 參數壓到操作數堆棧的位置2。有趣的是這些指令的前綴是以“i”和 “f”開頭的,這表明Java字節代碼的指令按嚴格的類型劃分的。如果 參數類型與字節代碼的參數類型不符合,虛擬機將拒絕不安全的字節代碼。更妙 的是,字節代碼被設計成僅執行一次類型安全檢查——當加載類的時 候。

2.Java中的類型安全檢測

類型安全是怎樣增強系統安全性的 呢?如果攻擊者可以讓虛擬機將整型變量當成浮點變量,或更嚴重更多,很容易 預見計算的崩潰。如果計算是發生在銀行賬戶上的,牽連的安全問題是很明顯的 。更危險的是欺騙虛擬機將整型變量編程一個對象引用。在大多數情況下,虛擬 機將崩潰,但是攻擊者只要找到一個漏洞即可。不要忘記攻擊者不需要手工查找 ——更好且容易的辦法是寫一個程序產生大量變換的壞的字節代碼, 直到找到一個可以危害虛擬機的。

另一種字節代碼保護內存安全的是數組操作。“aastore”和 “aaload”字節代碼操作Java數組,而它們一直要檢查數組的邊界。 當調用者超越數組邊界時,這些字節代碼將產生數組溢出錯誤 (ArrayIndexOutOfBoundsException)。也許所有應用中最重要的檢測是分支指 令,例如,以“if.”開始的字節代碼。在字節代碼中,分支指令在同 一個方法中只能跳轉到另一條指令。向方法之外傳遞控制的唯一辦法是返回,產 生一個異常,或執行一個喚醒(invoke)指令。這不僅關閉了許多易受攻擊的大 門,也防止由伴隨引用和堆棧的崩潰導致的可惡的程序錯誤。如果你曾經用系統 調試器打開過代碼中隨機定位的程序,你對這些程序錯誤會很熟悉。

需要 著重指出的是:所有的這些檢測是由虛擬機在字節代碼級上完成的,不僅僅是編 譯器。其它編程語言的編譯器象C++的,可以防止一些我們在上面討論過的內存錯 誤,但這些保護是基於源代碼級的。操作系統將讀入執行任何機器代碼,而不管 這些代碼是由小心翼翼的C++編譯器還是由邪惡的攻擊者產生的。簡單地說,C++ 是在源程序級上是面向對象的,而Java的面向對象特性擴展到已經編譯好的字節 代碼上。

三、怎樣通過分析字節代碼來提高程序的執行效率

不管你注意它們與 否,Java字節代碼的內存和安全保護都客觀存在,那為什麼還要那麼麻煩去看字 節代碼呢?其實,就如在DOS下深入理解匯編就可以寫出更好的C++代碼一樣,了 解編譯器怎樣將你的代碼翻譯成字節代碼可幫助你寫出更有效率的代碼,有時候 甚至可以防止不知不覺的程序錯誤。

1.為什麼在進行字符串合並時要使用 StringBuffer來代替String

我們看以下代碼:

//Return  the concatenation str1+str2
    String concat(String str1,  String str2) {
        return str1 + str2;
     }

    //Append str2 to str1
    void  concat(StringBuffer str1, String str2) {
         str1.append(str2);
    }

試想一下每個方法需要執行 多少函數。編譯該程序並執行javap,輸出結果如下:

Method  java.lang.String concat(java.lang.String, java.lang.String)
    0 new #6 <Class java.lang.StringBuffer>
   3  dup
   4 aload_1
   5 invokestatic #14 <Method  java.lang.String valueOf(java.lang.Object)>
   8  invokespecial #9 <Method java.lang.StringBuffer (java.lang.String)>
  11 aload_2
  12 invokevirtual  #10 <Method java.lang.StringBuffer append(java.lang.String) >
  15 invokevirtual #13 <Method java.lang.String  toString()>
  18 areturn

Method void concat (java.lang.StringBuffer, java.lang.String)
   0 aload_1
   1 aload_2
   2 invokevirtual #10 <Method  java.lang.StringBuffer append(java.lang.String)>
   5  pop
   6 return

第一個concat方法有五個方法調用: new,invokestatic,invokespecial和兩個invokevirtual 。這比第二個cacat 方法多了好多些工作,而第二個cacat只有一個簡單的invokevirtual調用。 String類的一個特點是其實例一旦創建,是不能改變的,除非重新給它賦值。在 我們學習Java編程時,就被告知對於字符串連接來說,使用StringBuffer比使用 String更有效率。使用javap分析這點可以清楚地看到它們的區別。如果你懷疑兩 種不同語言架構在性能上是否相同時,就應該使用javap分析字節代碼。不同的 Java編譯器,其產生優化字節代碼的方式也不同,利用javap也可以清楚地看到它 們的區別。以下是JBuilder產生字節代碼的分析結果:

Method  java.lang.String concat(java.lang.String, java.lang.String)
    0 aload_1
   1 invokestatic #5 <Method  java.lang.String valueOf(java.lang.Object)>
   4  aload_2
   5 invokestatic #5 <Method java.lang.String  valueOf(java.lang.Object)>
   8 invokevirtual #6  <Method java.lang.String concat(java.lang.String)>
  11  areturn

可以看到經過JBuilder的優化,第一個concat方法有三 個方法調用:兩個invokestatic invokevirtual 。這還是沒有第二個concat方 法簡潔。

不管怎樣,熟悉即時編譯器(JIT, Just-in-time)。因為當某個 方法被第一次調用時,即時編譯器將對該虛擬方法表中所指向的字節代碼進行編 譯,編譯完後表中的指針將指向編譯生成的機器碼,這樣即時編譯器將字節代碼 重新編譯成本機代碼,它可以使你進行更多javap分析沒有揭示的代碼優化。除非 你擁有虛擬機的源代碼,你應當用性能基准來進行字節代碼分析。

2.防止 應用程序中的錯誤

以下的例子說明如何通過檢測字節代碼來幫助防止應用 程序中的錯誤。首先創建兩個公共類,它們必須存放在兩個不同的文件中。

public class ChangeALot {
    // Variable
    public static final boolean debug=false;
     public static boolean log=false;
}

public class  EternallyConstant {
    // Methods
    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。只重新編譯ChangeALot文件,再運行EternallyConstant,輸 出結果如下:

EternallyConstant beginning execution
Logging mode is on

在調試模式下怎麼了?即使設置debug 為true,“Debug mode is on”還是打印不出來。答案在字節編碼中 。運行javap分析EternallyConstant類,可看到如下結果:

Compiled from EternallyConstant.java
public class  EternallyConstant extends java.lang.Object {
    public  EternallyConstant();
    public static void main (java.lang.String[]);
}

Method EternallyConstant()
   0 aload_0
   1 invokespecial #1 <Method  java.lang.Object()>
   4 return

Method void  main(java.lang.String[])
   0 getstatic #2 <Field  java.io.PrintStream out>
   3 ldc #3 <String  "EternallyConstant beginning execution">
   5  invokevirtual #4 <Method void println(java.lang.String)>
   8 getstatic #5 <Field boolean log>
  11 ifeq  22
  14 getstatic #2 <Field java.io.PrintStream  out>
  17 ldc #6 <String "Logging mode is  on">
  19 invokevirtual #4 <Method void println (java.lang.String)>
  22 return

很奇怪吧!由於 有“ifep”檢測log字段,代碼一點都不檢測debug字段。因為debug字 段被標記為final ,編譯器知道debug字段在運行過程中不會改變。所以 “if”語句被優化,分支部分被移去了。這是一個非常有用的優化, 因為這使你可以在引用程序中嵌入調試代碼,而設置為false時不用付出代價,不 幸的是這會導致編譯混亂。如果改變了final字段,記住重新編譯其它引用該字段 的類。這就是引用有可能被優化的原因。Java開發工具不是每次都能檢測這個細 微的改變,這些可能導致臨時的非常程序錯誤。在這裡,古老的C++格言對於Java 環境來說一樣成立:“每當迷惑不解時,重新編譯所有程序“。

四、利用第三方工具反編譯Java字節代碼

以上介紹了利用javap來 分析Java字節代碼,實際上,利用第三方的工具,可以直接得到源代碼。這樣的 工具有很多,其中NMI's Java Code Viewer (NJCV)是其中使用起來比較方便的一 種。

1.NMI's Java Code Viewer簡介

NJCV針對編譯好的Java字節 編碼,即.class文件、.zip或.jar文件。.jar文件實際上就是.zip文件。利用 NJCV這類反編譯工具,可以進一步調試、監聽程序錯誤,進行安全分析等等。通 過分析一些非常優秀的Java代碼,我們可以從中學到許多開發Java程序的技巧。

NMI's Java Code Viewer 的最新版本是4.8.3,而且只能運行在以下 Windows平台:

l Windows 95/98

l Windows 2000

l Windows NT 3.51/4.0

2. NMI's Java Code Viewer應用實例

我們以前面例舉到的ExampleOfByteCode.class作為例子。打開File菜單 中的open菜單,打開Java字節代碼文件,Java class files中列出了所有與該文 件在同一個目錄的文件。選擇要反編譯的文件,然後在Process菜單中選擇 Decompile或Dissasemble,反編譯好的文件列在Souce-code files一欄。用NMI's Java Code Viewer提供的Programmer’s File Editor打開該文件,瞧,源 代碼都列出來了。

// Processed by NMI's Java Code  Viewer 4.8.3 © 1997-2000 B. Lemaire
// Website:  http://njcv.htmlplanet.com  E-mail : [email protected]
// Copy registered to Evaluation Copy
// Source File  Name:   ExampleOfByteCode.java

import  java.io.PrintStream;

public class ExampleOfByteCode {

    public ExampleOfByteCode() {
    }

    public static void main(String args[]) {
         System.out.println("Hello world");
    }

     public float add(float f, int n) {
         return f + (float)n;
    }

    String  concat(String str1, String str2) {
        return  str1 + str2;
    }

    void concat (StringBuffer str1, String str2) {
         str1.append(str2);
    }
}

NMI's Java Code Viewer也支持直接從jar/zip文件中提取類文件。反編譯好的文件缺省用.nmi擴展 名存放,用戶可以設置.java擴展名。編輯源文件時可以使用NJCV提供的編輯器, 用戶可以選擇自己喜歡的編輯器。其結果與原文件相差不大,相信大家會喜歡它 。

五、結束語

了解一些字節代碼可以幫助從事Java程序編程語言 的程序員們編程。javap工具使察看字節代碼變得非常容易,第三方的一些工具使 代碼的反編譯易如反掌。經常使用javap檢測代碼,利用第三方工具反編代碼,對 於找到特別容易忘記的程序錯誤、提高程序運行效率、提高系統的安全性和性能 來說,其價值是無法估量的。

隨著Java編程技術的發展,Java類庫不斷完 善,利用Java優越的跨平台性能開發的應用軟件也越來越多。Oracle用Java編寫 了Oracle 8i的Enterprise Manager,以及其數據庫的安裝程序;Inprise公司的 Borland JBuilder 3.5也用Java寫成;一些Internet電話也使用了Java技術,如 MediaRing、DialPad的網絡電話采用了Java的解決方案;甚至以上提到的NMI's Java Code Viewer也是用Java寫成的。Java2已使Java得運行性能基本接近C++程 序的執行速度,結合Enterprise JavaBean、Servlet以及COBRA、RMI技術,Java 的功能會越來越強大,其應用也將日益廣泛。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved