首先看一下,關於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.
Java虛擬機規范中關於類型卸載的內容就這麼簡單兩句話,大致意思就是:只有當加載該類型的類加載器實例(非類加載器類型)為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的狀態
說明:
1、自定義類加載器(為了簡單起見,這裡就假設加載當前工程以外D盤某文件夾的class)
2、假設目前有一個簡單自定義類型MyClass對應的字節碼存在於D:/classes目錄下
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來觀察垃圾收集的情況,對應輸出如下:
sun.misc.Launcher$AppClassLoader@197d257
sun.misc.Launcher$AppClassLoader@197d257
sun.misc.Launcher$AppClassLoader@197d257
開始GC...
[FullGC196K->131K(1984K),0.0130748 secs]
GC完成...
sun.misc.Launcher$AppClassLoader@197d257
由於系統ClassLoader實例(AppClassLoader@197d257">sun.misc.Launcher$AppClassLoader@197d257)加載了很多類型,而且又沒有明確的接口將其設置為null,所以我們無法將加載MyClass類型的系統類加載器實例設置為unreachable狀態,所以通過測試結果我們可以看出,MyClass類型並沒有被卸載.(說明: 像類加載器實例這種較為特殊的對象一般在很多地方被引用,會在虛擬機中呆比較長的時間)
【測試場景三】使用擴展類加載器加載,但是無法將其設置為unreachable的狀態
說明:將測試場景二中的MyClass類型字節碼文件打包成jar放置到JRE擴展目錄下,以便擴展類加載器可以加載的到。由於標志擴展ClassLoader實例(ExtClassLoader@7259da">sun.misc.Launcher$ExtClassLoader@7259da)加載了很多類型,而且又沒有明確的接口將其設置為null,所以我們無法將加載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來觀察垃圾收集的情況,對應輸出如下:
sun.misc.Launcher$ExtClassLoader@7259da
開始GC...
[FullGC199K->133K(1984K),0.0139811 secs]
GC完成...
sun.misc.Launcher$ExtClassLoader@7259da
關於啟動類加載器我們就不需再做相關的測試了,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的分析文檔。
歡迎大家批評指正!