一:要解決的問題
我們在嘗鮮 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 打開來的内容如圖所示:
從上圖中我們看出來了什麼是 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 部份内容
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 設定,如圖
自已設定編譯選項,你會看到選擇不同的 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 檔案處理一下就行了