問題記錄:更換OpenJDK後,丟擲NoClassDefFoundError
背景
由於最近Oracle宣佈JDK8的新收費政策之後,公司決定遷移java環境到OpenJDK上面。在完成了遷移之後,發現了有兩個介面丟擲了NoClassDefFoundError。調查之後發現是openJDK裡面缺少了sun.lwawt.macosx.LWCToolkit這個包導致的問題。
藉此機會,記錄一下這個問題,也順便回顧了一下JVM載入和初始化class的過程,同時感慨一下果然冒煙測試和UT跑過之後還是要放心很多 :)
具體問題 以及 解決方案
總的來說,還是比較容易定位到問題的,因為stack trace還是滿明顯的:
Caused by: java.lang.NoClassDefFoundError: Could not initialize class sun.lwawt.macosx.LWCToolkit at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:264) at java.awt.Toolkit$2.run(Toolkit.java:860) at java.awt.Toolkit$2.run(Toolkit.java:855) at java.security.AccessController.doPrivileged(Native Method) at java.awt.Toolkit.getDefaultToolkit(Toolkit.java:854) at java.awt.Image.getScaledInstance(Image.java:178)
具體程式碼拋異常的地方:
public static synchronized Toolkit getDefaultToolkit() { if (toolkit == null) { java.security.AccessController.doPrivileged( new java.security.PrivilegedAction<Void>() { public Void run() { Class<?> cls = null; String nm = System.getProperty("awt.toolkit"); try { // 凶手在這裡~ cls = Class.forName(nm); } catch (ClassNotFoundException e) { ClassLoader cl = ClassLoader.getSystemClassLoader(); if (cl != null) { try { cls = cl.loadClass(nm); } catch (final ClassNotFoundException ignored) { throw new AWTError("Toolkit not found: " + nm); } } } try { if (cls != null) { toolkit = (Toolkit)cls.newInstance(); if (GraphicsEnvironment.isHeadless()) { toolkit = new HeadlessToolkit(toolkit); } } } catch (final InstantiationException ignored) { throw new AWTError("Could not instantiate Toolkit: " + nm); } catch (final IllegalAccessException ignored) { throw new AWTError("Could not access Toolkit: " + nm); } return null; } }); loadAssistiveTechnologies(); } return toolkit; }
可以看到這裡主要通過Class.forName(str)來動態載入的Class,所以在編譯階段沒有丟擲相關的異常,而是在執行到這部分之後才發現這個問題。
這裡想到之前想通過java -verbose方式來精簡JDK的時候,也是很容易因為這些動態載入的情況,造成包的誤刪除。
知道原因之後,解決方案就比較容易了,一個是更換這裡的處理方式,使用其它的方式;當然有同事還提出了一個更為亮眼的解決方式:把sun jdk下面的jar包直接拷貝過來……(是的,我也被這個思路震驚了,哈哈哈)
Java的類載入例項化步驟
過程中,順便回顧了一下Java對類的載入和例項化步驟(注意這裡是開始順序,並非一定是結束順序相關聯),這裡也記錄一下,以免後面又搞混了。
- 載入: 通過載入器將二進位制檔案讀入到JVM中(這裡主要涉及到classloader的雙親委託機制)
- 驗證:
2.1 驗證檔案格式:例如CAFEBABE的標識(是的,我第一次看《深入JVM虛擬機器》的時候對這個型別字也震驚了,果然程式設計師還是浪漫,不知道算不算彩蛋,哈哈哈)
2.2 驗證元資料
2.3 驗證位元組碼
2.4 驗證符號引用 - 準備(option):
3.1 為靜態變數分配記憶體
3.2 初始化靜態變數為預設值(注意這裡是預設的0值,而不是我們賦予的初始值) - 解析: 將符號引用轉化為直接引用
- 初始化:對類和類中的變數進行初始化(賦予初始值等)
關於初始化的時機,也記錄一下:
- 例項化物件,如new
- 訪問靜態變數
- 訪問靜態方法
- 反射
- 初始化子類
- 啟動時被標記為啟動的類(比如main的入口類)
結語
在處理過程中,還發現OpenJDK對JPG圖片的處理上,和sun JDK也有一些區別,查了一下資料,感覺主要是在alpha通道的處理上有不一樣的地方。
這麼一看,感覺sun對這部分的處理還有點高階,居然能支援alpha通道,也不知道是不是以後可以玩帶透明度的JPG了 :P
參考如下:

image.png