Java虛擬機器型別解除安裝和型別更新解析
【摘要】 前面系統討論過java型別載入(loading)的問題,在這篇文章中簡要分析一下java型別解除安裝(unloading)的問題,並簡要分析一下如何解決如何執行時載入newly compiled version的問題。【相關規範摘要】 首先看一下,關於java虛擬機器規範中時如何闡述型別解除安裝(unloading)的: A class or interface may be unloaded if and only if its class loader is unreachable. The bootstrap class loader is always reachable; as a result, system classes may never be unloaded.
只有當載入該型別的類載入器例項(非類載入器型別)為unreachable狀態時,當前被載入的型別才被解除安裝.啟動類載入器例項永遠為reachable狀態,由啟動類載入器載入的型別可能永遠不會被解除安裝.
我們再看一下Java語言規範提供的關於型別解除安裝的更詳細的資訊(部分摘錄): //摘自JLS 12.7 Unloading of Classes and Interfaces 1、An implementation of the Java programming language may unload classes. 2、Class unloading is an optimization that helps reduce memory use. Obviously,the semantics of a program should not depend on whether and how a system chooses to implement an optimization such as class unloading. 3、Consequently,whether a class or interface has been unloaded or not should be transparent to a program 通過以上我們可以得出結論: 型別解除安裝(unloading)僅僅是作為一種減少記憶體使用的效能優化措施存在的,具體和虛擬機器實現有關,對開發者來說是透明的. 縱觀java語言規範及其相關的API規範,找不到顯示型別解除安裝(unloading)的介面, 換句話說: 1、一個已經載入的型別被解除安裝的機率很小至少被解除安裝的時間是不確定的 2、一個被特定類載入器例項載入的型別執行時可以認為是無法被更新的
型別解除安裝進一步分析】 前面提到過,如果想解除安裝某型別,必須保證載入該型別的類載入器處於unreachable狀態,現在我們再看看有 關unreachable狀態的解釋: 1、A reachable object is any object that can be accessed in any potential continuing computation from any live thread. 2、finalizer-reachable: A finalizer-reachable object can be reached from some finalizable object through some chain of references, but not from any live thread. An unreachable object cannot be reached by either means. 某種程度上講,在一個稍微複雜的java應用中,我們很難準確判斷出一個例項是否處於unreachable狀態,所 以為了更加準確的逼近這個所謂的unreachable狀態,我們下面的測試程式碼儘量簡單一點. 【測試場景一】使用自定義類載入器載入, 然後測試將其設定為unreachable的狀態
public class MyURLClassLoader extends URLClassLoader { public MyURLClassLoader() { super(getMyURLs()); } private static URL[] getMyURLs() { try { return new URL[]{new File ("D:/classes/").toURL()}; } catch (Exception e) { e.printStackTrace(); return null; } } }
public class Main { 2 public static void main(String[] args) { 3 try { 4 MyURLClassLoader classLoader = new MyURLClassLoader(); 5 Class classLoaded = classLoader.loadClass("MyClass"); 6 System.out.println(classLoaded.getName()); 7 8 classLoaded = null; 9 classLoader = null; 10 11 System.out.println("開始GC"); 12 System.gc(); 13 System.out.println("GC完成"); 14 } catch (Exception e) { 15 e.printStackTrace(); 16 } 17 } 18 }
我們增加虛擬機器引數-verbose:gc來觀察垃圾收集的情況,對應輸出如下:
MyClass 開始GC [Full GC[Unloading class MyClass] 207K->131K(1984K), 0.0126452 secs] GC完成
測試場景二】使用系統類載入器載入,但是無法將其設定為unreachable的狀態 說明:將場景一中的MyClass型別位元組碼檔案放置到工程的輸出目錄下,以便系統類載入器可以載入
1 public class Main { 2 public static void main(String[] args) { 3 try { 4 Class classLoaded = ClassLoader.getSystemClassLoader().loadClass( 5 "MyClass"); 6 7 8 System.out.printl(sun.misc.Launcher.getLauncher().getClassLoader()); 9 System.out.println(classLoaded.getClassLoader()); 10 System.out.println(Main.class.getClassLoader()); 11 12 classLoaded = null; 13 14 System.out.println("開始GC"); 15 System.gc(); 16 System.out.println("GC完成"); 17 18 //判斷當前系統類載入器是否有被引用(是否是unreachable狀態) 19 System.out.println(Main.class.getClassLoader()); 20 } catch (Exception e) { 21 e.printStackTrace(); 22 } 23 } 24 }
我們增加虛擬機器引數-verbose:gc來觀察垃圾收集的情況, 對應輸出如下:
[email protected] [email protected] [email protected] 開始GC [Full GC 196K->131K(1984K), 0.0130748 secs] GC完成 [email protected]
所以我們無法將載入MyClass型別的系統類載入器例項設定為unreachable狀態,所以通過測試結果我們可以看出,
MyClass型別並沒有被解除安裝.(說明: 像類載入器例項這種較為特殊的物件一般在很多地方被引用, 會在虛擬機器中呆比較長的時間)
【測試場景三】使用擴充套件類載入器載入, 但是無法將其設定為unreachable的狀態
說明:將測試場景二中的MyClass型別位元組碼檔案打包成jar放置到JRE擴充套件目錄下,以便擴充套件類載入器可以載入的到。
所以我們無法將載入MyClass型別的系統類載入器例項設定為unreachable狀態,所以通過測試結果我們可以看出,MyClass型別並沒有被解除安裝.
1 public class Main { 2 public static void main(String[] args) { 3 try { 4 Class classLoaded = ClassLoader.getSystemClassLoader().getParent() 5 .loadClass("MyClass"); 6 7 System.out.println(classLoaded.getClassLoader()); 8 9 classLoaded = null; 10 11 System.out.println("開始GC"); 12 System.gc(); 13 System.out.println("GC完成"); 14 //判斷當前標準擴充套件類載入器是否有被引用(是否是unreachable狀態) 15 System.out.println(Main.class.getClassLoader().getParent()); 16 } catch (Exception e) { 17 e.printStackTrace(); 18 } 19 } 20 }
我們增加虛擬機器引數-verbose:gc來觀察垃圾收集的情況,對應輸出如下:
[email protected] 開始GC [Full GC 199K->133K(1984K), 0.0139811 secs] GC完成 [email protected]
關於啟動類載入器我們就不需再做相關的測試了,jvm規範和JLS中已經有明確的說明了.
型別解除安裝總結】 通過以上的相關測試(雖然測試的場景較為簡單)我們可以大致這樣概括: 1、有啟動類載入器載入的型別在整個執行期間是不可能被解除安裝的(jvm和jls規範). 2、被系統類載入器和標準擴充套件類載入器載入的型別在執行期間不太可能被解除安裝,因為系統類載入器例項或者標準擴充套件類的例項基本上在整個執行期間總能直接或者間接的訪問的到,其達到unreachable的可能性極小.(當然,在虛擬機器快退出的時候可以,因為不管ClassLoader例項或者Class(java.lang.Class)例項也都是在堆中存在,同樣遵循垃圾收集的規則). 3、被開發者自定義的類載入器例項載入的型別只有在很簡單的上下文環境中才能被解除安裝,而且一般還要藉助於強制呼叫虛擬機器的垃圾收集功能才可以做到.可以預想,稍微複雜點的應用場景中(尤其很多時候,使用者在開發自定義類載入器例項的時候採用快取的策略以提高系統性能),被載入的型別在執行期間也是幾乎不太可能被解除安裝的(至少解除安裝的時間是不確定的). 綜合以上三點,我們可以預設前面的結論1, 一個已經載入的型別被解除安裝的機率很小至少被解除安裝的時間是不確定的.同時,我們可以看的出來,開發者在開發程式碼時候,不應該對虛擬機器的型別解除安裝做任何假設的前提下來實現系統中的特定功能.
型別更新進一步分析】 前面已經明確說過,被一個特定類載入器例項載入的特定型別在執行時是無法被更新的.注意這裡說的 是一個特定的類載入器例項,而非一個特定的類載入器型別. 【測試場景四】 說明:現在要刪除前面已經放在工程輸出目錄下和擴充套件目錄下的對應的MyClass型別對應的位元組碼
1 public class Main { 2 public static void main(String[] args) { 3 try { 4 MyURLClassLoader classLoader = new MyURLClassLoader(); 5 Class classLoaded1 = classLoader.loadClass("MyClass"); 6 Class classLoaded2 = classLoader.loadClass("MyClass"); 7 //判斷兩次載入classloader例項是否相同 8 System.out.println(classLoaded1.getClassLoader() == classLoaded2.getClassLoader()); 9 10 //判斷兩個Class例項是否相同 11 System.out.println(classLoaded1 == classLoaded2); 12 } catch (Exception e) { 13 e.printStackTrace(); 14 } 15 } 16 }
輸出如下: true true 通過結果我們可以看出來,兩次載入獲取到的兩個Class型別例項是相同的.那是不是確實是我們的自定義 類載入器真正意義上載入了兩次呢(即從獲取class位元組碼到定義class型別…整個過程呢)? 通過對java.lang.ClassLoader的loadClass(String name,boolean resolve)方法進行除錯,我們可以看出來,第二 次 載入並不是真正意義上的載入,而是直接返回了上次載入的結果. 說明:為了除錯方便, 在Class classLoaded2 = classLoader.loadClass("MyClass");行設定斷點,然後單步跳入, 可以看到第二次載入請求返回的結果直接是上次載入的Class例項. 除錯過程中的截圖 最好能自己除錯一下).
測試場景五】同一個類載入器例項重複載入同一型別 說明:首先要對已有的使用者自定義類載入器做一定的修改,要覆蓋已有的類載入邏輯, MyURLClassLoader.java類簡要修改如下:重新執行測試場景四中的測試程式碼
1 public class MyURLClassLoader extends URLClassLoader { 2 //省略部分的程式碼和前面相同,只是新增如下覆蓋方法 3 /* 4 * 覆蓋預設的載入邏輯,如果是D:/classes/下的型別每次強制重新完整載入 5 * 6 * @see java.lang.ClassLoader#loadClass(java.lang.String) 7 */ 8 @Override 9 public Class<?> loadClass(String name) throws ClassNotFoundException { 10 try { 11 //首先呼叫系統類載入器載入 12 Class c = ClassLoader.getSystemClassLoader().loadClass(name); 13 return c; 14 } catch (ClassNotFoundException e) { 15 // 如果系統類載入器及其父類載入器載入不上,則呼叫自身邏輯來載入D:/classes/下的型別 16 return this.findClass(name); 17 } 18 } 19 }
說明: this.findClass(name)會進一步呼叫父類URLClassLoader中的對應方法,其中涉及到了defineClass(String name)的呼叫,
所以說現在類載入器MyURLClassLoader會針對D:/classes/目錄下的型別進行真正意義上的強制載入並定義對應的型別資訊. 測試輸出如下: Exception in thread "main" java.lang.LinkageError: duplicate class definition: MyClass at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:620) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:124) at java.net.URLClassLoader.defineClass(URLClassLoader.java:260) at java.net.URLClassLoader.access$100(URLClassLoader.java:56) at java.net.URLClassLoader$1.run(URLClassLoader.java:195) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:188) at MyURLClassLoader.loadClass(MyURLClassLoader.java:51) at Main.main(Main.java:27) 結論:如果同一個類載入器例項重複強制載入(含有定義型別defineClass動作)相同型別,會引起java.lang.LinkageError: duplicate class definition.
【測試場景六】同一個載入器型別的不同例項重複載入同一型別
1 public class Main { 2 public static void main(String[] args) { 3 try { 4 MyURLClassLoader classLoader1 = new MyURLClassLoader(); 5 Class classLoaded1 = classLoader1.loadClass("MyClass"); 6 MyURLClassLoader classLoader2 = new MyURLClassLoader(); 7 Class classLoaded2 = classLoader2.loadClass("MyClass"); 8 9 //判斷兩個Class例項是否相同 10 System.out.println(classLoaded1 == classLoaded2); 11 } catch (Exception e) { 12 e.printStackTrace(); 13 } 14 } 15 }
測試對應的輸出如下: false 【型別更新總結】 由不同類載入器例項重複強制載入(含有定義型別defineClass動作)同一型別不會引起java.lang.LinkageError錯誤, 但是載入結果對應的Class型別例項是不同的,即實際上是不同的型別(雖然包名+類名相同). 如果強制轉化使用,會引起ClassCastException.(說明: 頭一段時間那篇文章中解釋過,為什麼不同類載入器載入同名型別實際得到的結果其實是不同型別, 在JVM中一個類用其全名和一個載入類ClassLoader的例項作為唯一標識,不同類載入器載入的類將被置於不同的名稱空間). 應用場景:我們在開發的時候可能會遇到這樣的需求,就是要動態載入某指定型別class檔案的不同版本,以便能動態更新對應功能. 建議: 1. 不要寄希望於等待指定型別的以前版本被解除安裝,解除安裝行為對java開發人員透明的. 2. 比較可靠的做法是,每次建立特定類載入器的新例項來載入指定型別的不同版本,這種使用場景下,一般就要犧牲快取特定型別的類載入器例項以帶來效能優化的策略了.對於指定型別已經被載入的版本, 會在適當時機達到unreachable狀態,被unload並垃圾回收.每次使用完類載入器特定例項後(確定不需要再使用時), 將其顯示賦為null, 這樣可能會比較快的達到jvm 規範中所說的類載入器例項unreachable狀態, 增大已經不再使用的型別版本被儘快解除安裝的機會. 3. 不得不提的是,每次用新的類載入器例項去載入指定型別的指定版本,確實會帶來一定的記憶體消耗,一般類載入器例項會在記憶體中保留比較長的時間. 在bea開發者網站上找到一篇相關的文章(有專門分析ClassLoader的部分):http://dev2dev.bea.com/pub/a/2005/06/memory_leaks.html 寫的過程中參考了jvm規範和jls, 並參考了sun公司官方網站上的一些bug的分析文件。