天天看點

二進制相容原理 - C/C++ &Java

       從某種意義上來講,現代軟體已經不是資料結構與算法的簡單聚合,更多的是構件開發以及基于體系結構的構件組裝.而這些構件,通常都是由不同廠商、作者開發的共享元件,是以元件管理變得越來越重要。在這方面,一個極其重要的問題是類的不同版本的二進制相容性,即一個類改變時,新版的類是否可以直接替換原來的類,卻不至于損壞其他由不同廠商/作者開發的依賴于該類的元件?

       在c++中,對域(類變量或執行個體變量)的通路被編譯成相對于對象起始位置的偏移量,在編譯時就确定,如果類加入了新的域并重新編譯,偏移量随之改變,原先編譯的使用老版本類的代碼就不能正常執行( 也許有人會認為這是c++要比java的快的一個原因,根據數值性偏移量尋找方法肯定要比字元串比對快。這種說法有一定道理,但隻說明了類剛剛裝入時的情況,此後java的jit編譯器處理的也是數值性偏移量,而不再靠字元串比對的辦法尋找方法,因為類裝入記憶體之後不可能再改變,是以這時的jit編譯器根本無須顧慮到二進制相容問題。是以,至少在方法調用這一點上,java沒有理由一定比c++慢),不僅如此,虛函數的調用也存在同樣的問題。這些我們都稱之為二進制不相容,與之對應的是源碼不相容,如修改成員變量名字等.

       c++環境通常采用重新編譯所有引用了被修改類的代碼來解決問題。在java中,少量開發環境也采用了同樣的政策,但這種政策存在諸多限制。例如,假設有人開發了一個程式p,p引用了一個外部的庫l1,但p的作者沒有l1的源代碼;l1要用到另一個庫l2。現在l2改變了,但l1無法重新編譯,是以p的開發和更改也受到了限制。為此,java引入了二進制相容的概念—如果對l2的更改是二進制相容的,那麼更改後的l2、原來的l1和現在的p能夠順利連接配接,不會出現任何錯誤。

      首先來看一個簡單的例子。authorization和customer類分别來自兩個不同的作者,authorization提供身份驗證和授權服務,customer類要調用authorization類。    

        現在author1釋出了authorization類的2.0版,customer類的作者author2希望在不更改原有customer類的情況下使用新版的authorization類。2.0版的authorization要比原來的複雜不少:

       作者author1承諾2.0版的authorization類與1.0版的類二進制相容,或者說,2.0版的authorization類仍舊滿足1.0版的authorization類與customer類的約定。顯然,author2編譯customer類時,無論使用authorization類的哪一個版本都不會出錯—實際上,如果僅僅是因為authorization類更新,customer類根本無需重新編譯,同一個customer.class可以調用任意一個authorization.class。

       這一特性并非java獨有。unix系統很早就有了共享對象庫(.so檔案)的概念,windows系統也有動态連結庫(.dll檔案)的概念,隻要替換一下檔案就可以将一個庫改換為另一個庫。就象java的二進制相容特性一樣,名稱的連結是在運作時完成,而不是在代碼的編譯、連結階段完成。但是,java的二進制相容性還有其獨特的優勢:

⑴ java将二進制相容性的粒度從整個庫(可能包含數十、數百個類)細化到了單個的類。

⑵ 在c/c++之類的語言中,建立共享庫通常是一種有意識的行為,一個應用軟體一般不會提供很多共享庫,哪些代碼可以共享、哪些代碼不可共享都是預先規劃的結果。但在java中,二進制相容變成了一種與生俱來的天然特性。

⑶ 共享對象隻針對函數名稱,但java二進制相容性考慮到了重載、函數簽名、傳回值類型。

⑷ java提供了更完善的錯誤控制機制,版本不相容會觸發異常,但可以友善地捕獲和處理。相比之下,在c/c++中,共享庫版本不相容往往引起嚴重問題。

       二進制相容的概念在某些方面與對象串行化的概念相似,兩者的目标也有一定的重疊。串行化一個java對象時,類的名稱、域的名稱被寫入到一個二進制輸出流,串行化到磁盤的對象可以用類的不同版本來讀取,前提是該類要求的名稱、域都存在,且類型一緻。二進制相容和串行化都考慮到了類的版本不斷更新的問題,允許為類加入方法和域,而且純粹的加入不會影響程式的語義;類似地,單純的結構修改,例如重新排列域或方法,也不會引起任何問題。

       了解二進制相容的關鍵是要了解延遲綁定(late binding)。在java語言裡,延遲綁定是指直到運作時才檢查類、域、方法的名稱,而不象c/c++的編譯器那樣在編譯期間就清除了類、域、方法的名稱,代之以偏移量數值—這是java二進制相容得以發揮作用的關鍵。由于采用了延遲綁定技術,方法、域、類的名稱直到運作時才解析,意味着隻要域、方法等的名稱(以及類型)一樣,類的主體可以任意替換—當然,這是一種簡化的說法,還有其他一些規則制約java類的二進制相容性,例如通路屬性(private、public等)以及是否為abstract(如果一個方法是抽象的,那麼它肯定是不可直接調用的)等,但延遲綁定機制無疑是二進制相容的核心所在。

隻有掌握了二進制相容的規則,才能在改寫類的時候保證其他類不受到影響。下面再來看一個例子,kakamail和messimail是兩個email程式:

       如果我們重新實作message,不再讓它實作classifiable接口,messimail仍能正常運作,但kakamail會抛出異常"java.lang.incompatibleclasschangeerror"。這是因為messimail不要求emailmessage是一個classifiable,但kakamail卻要求emailmessage是一個classifiable,編譯kakamail得到的二進制.class檔案引用了classifiable這個接口名稱。

       從二進制相容的角度來看,一個方法由四部分構成,分别是:方法的名稱,傳回值類型,參數,方法是否為static。改變其中任何一個,對jvm而言,它已經變成了另一個方法。如果該類沒有提供一個名稱、參數、傳回值類型完全比對的方法,它就使用從超類繼承的方法。由于java的二進制相容性規則,這種繼承實際上在運作期間确定,而不是在編譯期間确定。也正是因為繼承,在代碼重構過程中,會招緻各種錯誤.比反說删除父類的某個在子類覆寫的域,然後調用了強制類型轉換後的子類同名字段,往往會出現"java.lang.nosuchfielderror".

      最新的jls7一文中,有一章節是專門介紹java語言的二進制相容性原理的,感興趣的同學可以下載下傳翻閱,以便加深了解~

ps: 案例拾遺

運作期異常: exception in thread "main" java.lang.abstractmethoderror: org.apache.batik.dom.genericelement.settextcontent(ljava/lang/string;)v

        why?abstractmethoderror這個錯誤挺經典的,一般發生在compile time,那出現在運作期,就可能意味着發生了不相容類更改,為什麼這麼說,我們看一個例子,直接上代碼:

        這麼寫當然沒有任何問題了~好,那node類出于更新等目的,改為抽象類,settextcontent改為抽象方法,使用java 指令行方式執行java svgnode,随你怎麼編譯新版node,javac也行,後面就昭然若揭了~

        總結一下: 該問題在引用外部包的時候常有發生,尤其當類的繼承層次比較複雜時,一般不容肉眼識别,但萬變不離其宗~其根本原因可能是父類出現了不相容修改~另外,要確定編譯器和jvm類加載路徑完全一緻,争取在編譯期就發現問題~

參考文獻: