1. 程式人生 > >深入理解java類版本衝突的問題

深入理解java類版本衝突的問題

一:要解決的問題 

我們在嚐鮮 JDK1.5 的時候,相信不少人遇到過 Unsupported major.minor version 49.0 錯誤,當時定會茫然不知所措。因為剛開始那會兒,網上與此相關的中文資料還不多,現在好了,網上一找就知道是如何解決,大多會告訴你要使用 JDK 1.4 重新編譯。那麼至於為什麼,那個 major.minor 究竟為何物呢?這就是本篇來講的內容,以使未錯而先知。

我覺得我是比較幸運的,因為在遇到那個錯誤之前已研讀過《深入 Java 虛擬機器》第二版,英文原書名為《Inside the Java Virtual Machine》( Second Edition),看時已知曉 major.minor 藏匿於何處,但沒有切身體會,待到與 Unsupported major.minor version 49.0 真正會面試,正好是給我驗證了一個事實。

首先我們要對 Unsupported major.minor version 49.0 建立的直接感覺是:JDK1.5 編譯出來的類不能在 JVM 1.4 下執行,必須編譯成 JVM 1.4 下能執行的類。(當然,也許你用的還是 JVM 1.3 或 JVM 1.2,那麼就要編譯成目標 JVM 能認可的類)。這也解決問題的方向。

二:major.minor 棲身於何處



何謂 major.minor,且又居身於何處呢?先感性認識並找到 major.minor 來。

寫一個 Java Hello World! 程式碼,然後用 JDK 1.5 的編譯器編譯成,HelloWorld.java

Java程式碼 
  1. package com.unmi;  
  2. public class HelloWorld  
  3. {  
  4.     public static void main(String[] args)  
  5.     {  
  6.         System.out.println("Hello, World!");  
  7.     }  
  8. }  


用 JDK 1.5 的 javac -d .  HelloWorld.java 編譯出來的位元組碼 HelloWorld.class 用 UltraEdit 開啟來的內容如圖所示:

 



從上圖中我們看出來了什麼是 major.minor version 了,它相當於一個軟體的主次版本號,只是在這裡是標識的一個 Java Class 的主版本號和次版本號,同時我們看到 minor_version 為 0x0000,major_version 為 0x0031,轉換為十制數分別為0 和 49,即 major.minor 就是 49.0 了。

三:何謂 major.minor 以及何用

Class 檔案的第 5-8 位元組為 minor_version 和 major_version。Java class 檔案格式可能會加入新特性。class 檔案格式一旦發生變化,版本號也會隨之變化。對於 JVM 來說,版本號確定了特定的 class 檔案格式,通常只有給定主版本號和一系列次版本號後,JVM 才能夠讀取 class 檔案。如果 class 檔案的版本號超出了 JVM 所能處理的有效範圍,JVM 將不會處理該 class 檔案。

在 Sun 的 JDK 1.0.2 釋出版中,JVM 實現支援從 45.0 到 45.3 的 class 檔案格式。在所有 JDK 1.1 釋出版中的 JVM 都能夠支援版本從 45.0 到 45.65535 的 class 檔案格式。在 Sun 的 1.2 版本的 SDK 中,JVM 能夠支援從版本 45.0 到46.0 的 class 檔案格式。

1.0 或 1.2 版本的編譯器能夠產生版本號為 45.3 的 class 檔案。在 Sun 的 1.2 版本 SDK 中,Javac 編譯器預設產生版本號為 45.3  的 class 檔案。但如果在 javac 命令列中指定了 -target 1.2 標誌,1.2 版本的編譯器將產生版本號為 46.0 的 class 檔案。1.0 或 1.1 版本的 JVM 上不能執行使用-target 1.2 標誌所產生的 class 檔案。

JVM 實現的 第二版中修改了對 class 檔案主版本號和次版本號的解釋。對於第二版而言,class 檔案的主版本號與 Java 平臺主釋出版的版本號保持一致(例如:在 Java 2 平臺釋出版上,主版本號從 45 升至 46),次版本號與特定主平臺釋出版的各個釋出版相關。因此,儘管不同的 class 檔案格式可以由不同的版本號表示,但版本號不一樣並不代表 class 檔案格式不同。版本號不同的原因可能只是因為 class 檔案由不同釋出版本的 java 平臺產生,可能 class 檔案的格式並沒有改變。

上面三段節選自《深入 Java 虛擬機器》,囉嗦一堆,JDK 1.2 開啟了 Java 2 的時代,但那個年代仍然離我們很遠,我們當中很多少直接跳在 JDK 1.4 上的,我也差不多,只是專案要求不得不在一段時間裡委屈在 JDK 1.3 上。不過大致我們可以得到的資訊就是每個版本的 JDK 編譯器編譯出的 class 檔案中都帶有一個版本號,不同的 JVM 能接受一個範圍 class 版本號,超出範圍則要出錯。不過一般都是能向後相容的,知道 Sun 在做 Solaris 的一句口號嗎?保持對先前版本的 100% 二進位制相容性,這也是對客戶的投資保護。

四:其他確定 class 的 major.minor version 辦法


1)Eclipse 中檢視
      Eclipse 3.3 加入的新特徵,當某個類沒有關聯到原始碼,開啟它會顯示比較詳細的類資訊,當然還未到原始碼級別了,看下圖是開啟 2.0 spring.jar 中 ClasspathXmlApplicationContext.class 顯示的資訊



 


2)命令 javap -verbose
       對於編譯出的 class 檔案用 javap -verbose 能顯示出類的 major.minor 版本,見下圖:



 


3)  MANIFEST 檔案
      把 class 打成的 JAR 包中都會有檔案 META-INF\MANIFEST,這個檔案一般會有編譯器的資訊,下面列幾個包的 META-INF\MANIFEST 檔案內容大家看看
      ·Velocity-1.5.jar 的 META-INFO\MANIFEST 部份內容
                  Manifest-Version: 1.0
                  Ant-Version: Apache Ant 1.7.0
                  Created-By: Apache Ant
                  Package: org.apache.velocity
                  Build-Jdk: 1.4.2_08
                  Extension-Name: velocity
            我們看到是用 ant 打包,構建用的JDK是 1.4.2_08,用 1.4 編譯的類在 1.4 JVM 中當然能執行。如果那人用 1.5 的 JDK 來編譯,然後用 JDK 1.4+ANT 來打包就太無聊了。
      ·2.0 spring.jar 的 META-INFO\MANIFEST 部份內容
                  Manifest-Version: 1.0
                  Ant-Version: Apache Ant 1.6.5
                  Created-By: 1.5.0_08-b03 (Sun Microsystems Inc.)
                  Implementation-Title: Spring Framework
           這下要注意啦,它是用的 JDK 1.5 來編譯的,那麼它是否帶了 -target 1.4 或 -target 1.3 來編譯的呢?確實是的,可以檢視類的二進位制檔案,這是最保險的。所在 spring-2.0.jar 也可以在 1.4 JVM 中載入執行。
      ·自已一個專案中用 ant 打的 jar 包的 META-INFO\MANIFEST
                  Manifest-Version: 1.0
                  Ant-Version: Apache Ant 1.7.0
                  Created-By: 1.4.2-b28 (Sun Microsystems Inc.)
            用的是 JDK 1.4 構建打包的。

第一第二種辦法能明確知道 major.minor version,而第三種方法應該也沒問題,但是碰到變態構建就難說了,比如誰把那個 META-INFO\MANIFEST 打包後換了也未可知。直接檢視類的二進位制檔案的方法可以萬分保證,準確無誤,就是工具篡改我也認了。

五:編譯器比較及症節之所在

現在不妨從 JDK 1.1 到 JDK 1.7 編譯器編譯出的 class 的預設 minor.major version 吧。(又走到 Sun 的網站上翻騰出我從來都沒用過的古董來)

JDK 編譯器版本 target 引數 十六進位制 minor.major 十進位制 minor.major
jdk1.1.8 不能帶 target 引數 00 03   00 2D 45.3
jdk1.2.2 不帶(預設為 -target 1.1) 00 03   00 2D 45.3
jdk1.2.2 -target 1.2 00 00   00 2E 46.0
jdk1.3.1_19 不帶(預設為 -target 1.1) 00 03   00 2D 45.3
jdk1.3.1_19 -target 1.3 00 00   00 2F 47.0
j2sdk1.4.2_10 不帶(預設為 -target 1.2) 00 00   00 2E 46.0
j2sdk1.4.2_10 -target 1.4 00 00   00 30 48.0
jdk1.5.0_11 不帶(預設為 -target 1.5) 00 00   00 31 49.0
jdk1.5.0_11 -target 1.4 -source 1.4 00 00   00 30 48.0
jdk1.6.0_01 不帶(預設為 -target 1.6) 00 00   00 32 50.0
jdk1.6.0_01 -target 1.5 00 00   00 31 49.0
jdk1.6.0_01 -target 1.4 -source 1.4 00 00   00 30 48.0
jdk1.7.0 不帶(預設為 -target 1.6) 00 00   00 32 50.0
jdk1.7.0 -target 1.7 00 00   00 33 51.0
jdk1.7.0 -target 1.4 -source 1.4 00 00   00 30 48.0
Apache Harmony 5.0M3 不帶(預設為 -target 1.2) 00 00   00 2E 46.0
Apache Harmony 5.0M3 -target 1.4 00 00   00 30 48.0


上面比較是 Windows 平臺下的 JDK 編譯器的情況,我們可以此作些總結:

1) -target 1.1 時 有次版本號,target 為 1.2 及以後都只用主版本號了,次版本號為 0
2) 從 1.1 到 1.4 語言差異比較小,所以 1.2 到 1.4 預設的 target 都不是自身相對應版本
3) 1.5 語法變動很大,所以直接預設 target 就是 1.5。也因為如此用 1.5 的 JDK 要生成目標為 1.4 的程式碼,光有 -target 1.4 不夠,必須同時帶上 -source 1.4,指定原始碼的相容性,1.6/1.7 JDk 生成目標為 1.4 的程式碼也如此。
4) 1.6 編譯器顯得較為激進,預設引數就為 -target 1.6。因為 1.6 和 1.5 的語法無差異,所以用 -target 1.5 時無需跟著 -source 1.5。
5) 注意 1.7 編譯的預設 target 為 1.6
6) 其他第三方的 JDK 生成的 Class 檔案格式版本號同對應 Sun 版本 JDK
7) 最後一點最重要的,某個版本的 JVM 能接受 class 檔案的最大主版本號不能超過對應 JDK 帶相應 target 引數編譯出來的 class 檔案的版本號

上面那句話有點長,一口氣讀過去不是很好理解,舉個例子:1.4 的 JVM 能接受最大的 class 檔案的主版本號不能超過用 1.4 JDK 帶引數 -target 1.4 時編譯出的 class 檔案的主版本號,也就是 48。

因為 1.5 JDK 編譯時預設 target 為 1.5,出來的位元組碼 major.minor version 是 49.0,所以 1.4 的 JVM 是無法接受的,只有丟擲錯誤。

那麼又為什麼從 1.1 到 1.2、從 1.2 到 1.3 或者從 1.3 到 1.4 的 JDK 升級不會發生 Unsupported major.minor version 的錯誤呢,那是因為 1.2/1.3/1.4 都保持了很好的二進位制相容性,看看 1.2/1.3/1.4 的預設 target 分別為 1.1/1.1/1.2 就知道了,也就是預設情況下1.4 JDK 編譯出的 class 檔案在 JVM 1.2 下都能載入執行,何況於 JVM 1.3 呢?(當然要去除使用了新版本擴充的 API 的因素)

六:找到問題解決的方法

那麼現在如果碰到這種問題該知道如何解決了吧,還會像我所見到有些兄弟那樣,去找個 1.4 的 JDK 下載安裝,然後用其重新編譯所有的程式碼嗎?其實大可不必如此費神,我們一定還記得 javac 還有個 -target 引數,對啦,可以繼續使用 1.5 JDK,編譯時帶上引數 -target 1.4 -source 1.4 就 OK 啦,不過你一定要對哪些 API 是 1.5 JDK 加入進來的瞭如指掌,不能你的 class 檔案拿到 JVM 1.4 下就會 method not found。目標 JVM 是 1.3 的話,編譯選項就用 -target 1.3 -source 1.3 了。

相應的如果使用 ant ,它的 javac 任務也可對應的選擇 target 和 source

<javac target="1.4" source="1.4" ............................/>

如果是在開發中,可以肯定的是現在真正算得上是 JAVA IDE 對於工程也都有編譯選項設定目的碼的。例如 Eclipse 的專案屬性中的 Java Compiler 設定,如圖



 



自已設定編譯選項,你會看到選擇不同的 compiler compliance level 是,Generated class files compatibility 和 Source compatibility 也在變,你也可以手動調整那兩項,手動設定後你就不用很在乎用的什麼版本的編譯器了,只要求他生成我們希望的位元組碼就行了,再引申一下就是即使原始碼是用 VB 寫的,只要能編譯成 JVM 能執行的位元組碼都不打緊。在其他的 IDE 也能找到相應的設定對話方塊的。

其他時候,你一定要知道當前的 JVM 是什麼版本,能接受的位元組碼主版本號是多少(可對照前面那個表)。獲息當前 JVM 版本有兩種途徑:

第一:如果你是直接用 java 命令在控制檯執行程式,可以用 java -version 檢視當前的 JVM 版本,然後確定能接受的 class 檔案版本

第二:如果是在容器中執行,而不能明確知道會使用哪個 JVM,那麼可以在容器中執行的程式中加入程式碼 System.getProperty("java.runtime.version"); 或 System.getProperty("java.class.version"),獲得 JVM 版本和能接受的 class 的版本號。

最後一絕招,如果你不想針對低版本的 JVM 用 target 引數重新編譯所有程式碼;如果你仍然想繼續在程式碼中用新的 API 的話;更有甚者,你還用了 JDK 1.5 的新特性,譬如泛型、自動拆裝箱、列舉等的話,那你用 -target 1.4 -source 1.4 就沒法編譯通過,不得不重新整理程式碼。那麼告訴你最後一招,不需要再從原始碼著手,直接轉換你所正常編譯出的位元組碼,繼續享用那些新的特性,新的 API,那就是:請參考之前的一篇日誌:Retrotranslator讓你用JDK1.5的特性寫出的程式碼能在JVM1.4中執行,我就是這麼用的,做好測試就不會有問題的。

七:再議一個實際發生的相關問題

這是一個因為拷貝 Tomcat 而產生的 Unsupported major.minor version 49.0 錯誤。情景是:我本地安裝的是 JDK 1.5,然後在網上找了一個 EXE 的 Tomcat 安裝檔案安裝了並且可用。後來同事要一個 Tomcat,不想下載或安裝,於是根據我以往的經驗是把我的 Tomcat 整個目錄拷給他應該就行了,結果是拿到他那裡瀏覽 jsp 檔案都出現 Unsupported major.minor version 49.0 錯誤,可以確定的是他安裝的是 1.4 的 JDK,但我還是有些納悶,先前對這個問題還頗有信心的我傻眼了。慣性思維是編譯好的 class 檔案拿到低版本的 JVM 會出現如是異常,可現並沒有用已 JDK 1.5 編譯好的類要執行啊。

後來仔細看異常資訊,終於發現了 %TOMCAT_HOME%\common\lib\tools.jar 這一眉目,因為 jsp 檔案需要依賴它來編譯,打來這個 tools.jar 中的一個 class 檔案來看看,49.0,很快我就明白原來這個檔案是在我的機器上安裝 Tomcat 時由 Tomcat 安裝程式從 %JDK1.5%\lib 目錄拷到 Tomcat 的 lib 目錄去的,造成在同事機器上編譯 JSP 時是 1.4 的 JVM 配搭著 49.0 的 tools.jar,那能不出錯,於是找來 1.4  JDK 的 tools.jar 替換了 Tomcat 的就 OK 啦。

八:小結

其實理解 major.minor 就像是我們可以這麼想像,同樣是微軟體的程式,32 位的應用程式不能拿到 16 位系統中執行那樣。

如果我們釋出前瞭解到目標 JVM 版本,知道怎麼從 java class 檔案中看出 major.minor 版本來,就不用等到伺服器報出異常才著手去解決,也就能預知到可能發生的問題。

其他時候遇到這個問題應具體解決,總之問題的根由是低版本的  JVM 無法載入高版本的 class 檔案造成的,找到高版本的 class 檔案處理一下就行了。