天天看點

在裝2個不同版本JDK時遇到了這個問題

一:要解決的問題

我們在嘗鮮 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

package com.unmi;   

public class helloworld   

{   

    public static void main(string[] args)   

    {   

        system.out.println("hello, world!");   

    }   

}  

package com.unmi;public class helloworld{ public static void main(string[] args) { system.out.println("hello, world!"); }}

用 jdk 1.5 的 javac -d .  helloworld.java 編譯出來的位元組碼 helloworld.class 用 ultraedit 打開來的内容如圖所示:

在裝2個不同版本JDK時遇到了這個問題

從上圖中我們看出來了什麼是 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個不同版本JDK時遇到了這個問題

2)指令 javap -verbose

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

在裝2個不同版本JDK時遇到了這個問題

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 部份内容

                  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

                  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)

-target 1.2

00 00   00 2e

46.0

jdk1.3.1_19

-target 1.3

00 00   00 2f

47.0

j2sdk1.4.2_10

不帶(預設為 -target 1.2)

-target 1.4

00 00   00 30

48.0

jdk1.5.0_11

不帶(預設為 -target 1.5)

00 00   00 31

49.0

-target 1.4 -source 1.4

jdk1.6.0_01

不帶(預設為 -target 1.6)

00 00   00 32

50.0

-target 1.5

jdk1.7.0

-target 1.7

00 00   00 33

51.0

apache harmony 5.0m3

上面比較是 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 設定,如圖

在裝2個不同版本JDK時遇到了這個問題

自已設定編譯選項,你會看到選擇不同的 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 的版本号。

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

這是一個因為拷貝 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 檔案處理一下就行了