爲了弄清楚這個問題,首先還要看看System類的API doc文檔。
3、Bootstrap Loader(啓動類加載器)是最頂級的類加載器了,其父加載器爲null.
public static void main(String[] args) {
HelloWorld hello = new HelloWorld();
Class c = hello.getClass();
ClassLoader loader = c.getClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}
sun.misc.Launcher$ExtClassLoader@addbf1
null
Process finished with exit code 0
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = HelloWorld. class.getClassLoader();
System.out.println(loader);
//使用ClassLoader.loadClass()來加載類,不會執行初始化塊
loader.loadClass( "Test2");
//使用Class.forName()來加載類,默認會執行初始化塊
// Class.forName("Test2");
//使用Class.forName()來加載類,並指定ClassLoader,初始化時不執行靜態塊
// Class.forName("Test2", false, loader);
}
}
static {
System.out.println( "靜態初始化塊執行了!");
}
}
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
/**
* 自定義ClassLoader
*
* @author leizhimin 2009-7-29 22:05:48
*/
public class MyClassLoader {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
URL url = new URL( "file:/E://projects//testScanner//out//production//testScanner");
ClassLoader myloader = new URLClassLoader( new URL[]{url});
Class c = myloader.loadClass( "test.Test3");
System.out.println( "----------");
Test3 t3 = (Test3) c.newInstance();
}
}
static {
System.out.println( "Test3的靜態初始化塊執行了!");
}
}
Test3的靜態初始化塊執行了!
Process finished with exit code 0
在java.lang包裏有個ClassLoader類,ClassLoader 的基本目標是對類的請求提供服務,按需動態裝載類和資
源,只有當一個類要使用(使用new 關鍵字來實例化一個類)的時候,類加載器纔會加載這個類並初始化。
一個Java應用程序可以使用不同類型的類加載器。例如Web Application Server中,Servlet的加載使用開發
商自定義的類加載器, java.lang.String在使用JVM系統加載器,Bootstrap Class Loader,開發商定義的其他類
則由AppClassLoader加載。在JVM裏由類名和類加載器區別不同的Java類型。因此,JVM允許我們使用不同
的加載器加載相同namespace的java類,而實際上這些相同namespace的java類可以是完全不同的類。這種
機制可以保證JDK自帶的java.lang.String是唯一的。
2. 加載類的兩種方式:
(1) 隱式方式
使用new關鍵字讓類加載器按需求載入所需的類
(2) 顯式方式
由 java.lang.Class的forName()方法加載
public static Class forName(String className)
public static Class forName(String className, boolean initialize,ClassLoader loader)
參數說明:
className - 所需類的完全限定名
initialize - 是否必須初始化類(靜態代碼塊的初始化)
loader - 用於加載類的類加載器
調用只有一個參數的forName()方法等效於 Class.forName(className, true, loader)。
這兩個方法,最後都要連接到原生方法forName0(),其定義如下:
private static native Class forName0(String name, boolean initialize,ClassLoader loader)
throws ClassNotFoundException;
只有一個參數的forName()方法,最後調用的是:
forName0(className, true, ClassLoader.getCallerClassLoader());
而三個參數的forName(),最後調用的是:
forName0(name, initialize, loader);
所以,不管使用的是new 來實例化某個類、或是使用只有一個參數的Class.forName()方法,內部都隱含
了“載入類 + 運行靜態代碼塊”的步驟。而使用具有三個參數的Class.forName()方法時,如果第二個參數
爲false,那麼類加載器只會加載類,而不會初始化靜態代碼塊,只有當實例化這個類的時候,靜態代碼塊
纔會被初始化,靜態代碼塊是在類第一次實例化的時候才初始化的。
直接使用類加載器
獲得對象所屬的類 : getClass()方法
獲得該類的類加載器 : getClassLoader()方法
3.執行java XXX.class的過程
找到JRE——》找到jvm.dll——》啓動JVM並進行初始化——》產生Bootstrap Loader——》
載入ExtClassLoader——》載入AppClassLoader——》執行java XXX.class
ClassLoader是用來處理類加載的類,它管理着具體類的運行時上下文。
1.ClassLoader存在的模塊意義:
1)從java的package定義出發:
classloader是通過分層的關聯方式來管理運行中使用的類,不同的classloader中管理的類是不相同的,或者即便兩個類毫無二致(除了路徑)也是不同的兩個類,在進行強制轉換時也會拋出ClassCastException。所以,通過classloader的限制,我們可以建立不同的package路徑以區別不同的類(注意這裏的“不同”是指,命名和實現完全一致,但是有不同的包路徑。)。那麼也是因爲有特定的classloader,我們可以實現具體模塊的加載,而不影響jvm中其他類,即發生類加載的衝突。
2)但是,如果兩個在不同路徑下的類(我們假定,這兩個類定義中,不存在package聲明,完全一樣的兩個類),經過不同的classloader加載,這兩個類在jvm中產生的實例可以相互轉換嗎?
答案是否定的。即便這兩個類除了存在位置不同之外,都完全一樣。經由不同classloader加載的兩個類依然是不同的兩個對象。通過Class.newInstance()或者Class.getConstructor().newInstance()產生的對象是完全不同的實例。
以上兩種情況,package可以使得我們的軟件架構清晰,但那不是最終作用,如果跟classloader結合起來理解,效果更好。
2.ClassLoader的類加載機制:
ClassLoader作爲java的一個默認抽象類,給我們帶來了極大的方便,如果我們要自己實現相應的類加載算法的話。
每個類都有一個對應的class與之綁定,並且可以通過MyClass.class方式來獲取這個Class對象。通過Class對象,我們就能獲取加載這個類的classloader。但是,我們現在要研究的是,一個類,是如何通過classloader加載到jvm中的。
其中有幾個關鍵方法,值得我們瞭解一番:
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException;
我們可以假設一個實例在建立時,例如通過new方式,是經由如此步驟實現:ClassLoader.loadClass("classname",false).newInstance()。
接下來需要考慮的是loadClass方法爲我們做了哪些工作?如何跟對應的.class文件結合,如何將對應的文件變成我們的Class對象,如何獲得我們需要的類?
在ClassLoader類中,已經有了loadClass默認實現。我們結合源代碼說明一下:
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 首先檢查,jvm中是否已經加載了對應名稱的類,findLoadedClass(String )方法實際上是findLoadedClass0方法的wrapped方法,做了檢查類名的工
//作,而findLoadedClass0則是一個native方法,通過底層來查看jvm中的對象。
Class c = findLoadedClass(name);
if (c == null) {//類還未加載
try {
if (parent != null) {
//在類還未加載的情況下,我們首先應該將加載工作交由父classloader來處理。
c = parent.loadClass(name, false);
} else {
//返回一個由bootstrap class loader加載的類,如果不存在就返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);//這裏是我們的入手點,也就是指定我們自己的類加載實現
}
}
if (resolve) {
resolveClass(c);//用來做類鏈接操作
}
return c;
}
在這段代碼中,應該已經說明了很多問題,那就是jvm會緩存加載的類,所以,在我們要求classloader爲我們加載類時,要先通過findLoadedClass方法來查看是否已經存在了這個類。不存在時,就要先由其parent class loader 來loadClass,當然可以迭代這種操作一直到找到這個類的加載定義。如果這樣還是不能解決問題,對於我們自己實現的class loader而言,可以再交由system class loader來loadClass,如果再不行,那就讓findBootstrapClassOrNull。經歷瞭如此路程,依然不能解決問題時,那就要我們出馬來擺平,通過自己實現的findClass(String)方法來實現具體的類加載。
這段實現代碼摘自Andreas Schaefer寫的文章中的代碼(這篇文章相當精彩)
protected Class findClass( String pClassName )
throws ClassNotFoundException {
try {
System.out.println( "Current dir: " + new File( mDirectory ).getAbsolutePath() );
File lClassFile = new File( mDirectory, pClassName + ".class" );
InputStream lInput = new BufferedInputStream( new FileInputStream( lClassFile ) );
ByteArrayOutputStream lOutput = new ByteArrayOutputStream();
int i = 0;
while( ( i = lInput.read() ) >= 0 ) {
lOutput.write( i );
}
byte[] lBytes = lOutput.toByteArray();
return defineClass( pClassName, lBytes, 0, lBytes.length );
} catch( Exception e ) {
throw new ClassNotFoundException( "Class: " + pClassName + " could not be found" );
}
}
findClass方法主要的工作是在指定路徑中查找我們需要的類。如果存在此命名的類,那麼就將class文件加載到jvm中,再由defineClass方法(一個native方法)來生成具體的Class對象。
一般來說,經過上述方式來加載類的話,我們的類可能都在一個classloader中加載完成。但是,再強調一下,那就是如果類有不同路徑或者不同包名,那就是不同類定義。
java classLoader 體系結構
- Bootstrap ClassLoader/啓動類加載器
主要負責jdk_home/lib目錄下的核心 api 或 -Xbootclasspath 選項指定的jar包裝入工作。 - Extension ClassLoader/擴展類加載器
主要負責jdk_home/lib/ext目錄下的jar包或 -Djava.ext.dirs 指定目錄下的jar包裝入工作。 - System ClassLoader/系統類加載器
主要負責java -classpath/-Djava.class.path所指的目錄下的類與jar包裝入工作。 - User Custom ClassLoader/用戶自定義類加載器(java.lang.ClassLoader的子類)
在程序運行期間, 通過java.lang.ClassLoader的子類動態加載class文件, 體現java動態實時類裝入特性。
類加載器的特性:
- 每個ClassLoader都維護了一份自己的名稱空間, 同一個名稱空間裏不能出現兩個同名的類。
- 爲了實現java安全沙箱模型頂層的類加載器安全機制, java默認採用了 " 雙親委派的加載鏈 " 結構。
classloader-architecture
classloader-class-diagram
類圖中, BootstrapClassLoader是一個單獨的java類, 其實在這裏, 不應該叫他是一個java類。因爲,它已經完全不用java實現了。它是在jvm啓動時, 就被構造起來的, 負責java平臺核心庫。
自定義類加載器加載一個類的步驟
classloader-load-class
ClassLoader 類加載邏輯分析, 以下邏輯是除 BootstrapClassLoader 外的類加載器加載流程:
- // 檢查類是否已被裝載過
- Class c = findLoadedClass(name);
- if (c == null ) {
- // 指定類未被裝載過
- try {
- if (parent != null ) {
- // 如果父類加載器不爲空, 則委派給父類加載
- c = parent.loadClass(name, false );
- } else {
- // 如果父類加載器爲空, 則委派給啓動類加載加載
- c = findBootstrapClass0(name);
- }
- } catch (ClassNotFoundException e) {
- // 啓動類加載器或父類加載器拋出異常後, 當前類加載器將其
- // 捕獲, 並通過findClass方法, 由自身加載
- c = findClass(name);
- }
- }
線程上下文類加載器
java默認的線程上下文類加載器是 系統類加載器(AppClassLoader)。
- // Now create the class loader to use to launch the application
- try {
- loader = AppClassLoader.getAppClassLoader(extcl);
- } catch (IOException e) {
- throw new InternalError(
- "Could not create application class loader" );
- }
- // Also set the context class loader for the primordial thread.
- Thread.currentThread().setContextClassLoader(loader);
以上代碼摘自sun.misc.Launch的無參構造函數Launch()。
使用線程上下文類加載器, 可以在執行線程中, 拋棄雙親委派加載鏈模式, 使用線程上下文裏的類加載器加載類.
典型的例子有, 通過線程上下文來加載第三方庫jndi實現, 而不依賴於雙親委派.
大部分java app服務器(jboss, tomcat..)也是採用contextClassLoader來處理web服務。
還有一些採用 hotswap 特性的框架, 也使用了線程上下文類加載器, 比如 seasar (full stack framework in japenese).
線程上下文從根本解決了一般應用不能違背雙親委派模式的問題.
使java類加載體系顯得更靈活.
隨着多核時代的來臨, 相信多線程開發將會越來越多地進入程序員的實際編碼過程中. 因此,
在編寫基礎設施時, 通過使用線程上下文來加載類, 應該是一個很好的選擇。
當然, 好東西都有利弊. 使用線程上下文加載類, 也要注意, 保證多根需要通信的線程間的類加載器應該是同一個,
防止因爲不同的類加載器, 導致類型轉換異常(ClassCastException)。
爲什麼要使用這種雙親委託模式呢?
- 因爲這樣可以避免重複加載,當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。
- 考慮到安全因素,我們試想一下,如果不使用這種委託模式,那我們就可以隨時使用自定義的String來動態替代java核心api中定義類型,這樣會存在非常大的安全隱患,而雙親委託的方式,就可以避免這種情況,因爲String已經在啓動時被加載,所以用戶自定義類是無法加載一個自定義的ClassLoader。
java動態載入class的兩種方式:
- implicit隱式,即利用實例化才載入的特性來動態載入class
- explicit顯式方式,又分兩種方式:
- java.lang.Class的forName()方法
- java.lang.ClassLoader的loadClass()方法
用Class.forName加載類
Class.forName使用的是被調用者的類加載器來加載類的。
這種特性, 證明了java類加載器中的名稱空間是唯一的, 不會相互干擾。
即在一般情況下, 保證同一個類中所關聯的其他類都是由當前類的類加載器所加載的。
- public static Class forName(String className)
- throws ClassNotFoundException {
- return forName0(className, true , ClassLoader.getCallerClassLoader());
- }
- /** Called after security checks have been made. */
- private static native Class forName0(String name, boolean initialize,
- ClassLoader loader)
- throws ClassNotFoundException;
上面中 ClassLoader.getCallerClassLoader 就是得到調用當前forName方法的類的類加載器
static塊在什麼時候執行?
- 當調用forName(String)載入class時執行,如果調用ClassLoader.loadClass並不會執行.forName(String,false,ClassLoader)時也不會執行.
- 如果載入Class時沒有執行static塊則在第一次實例化時執行.比如new ,Class.newInstance()操作
- static塊僅執行一次
各個java類由哪些classLoader加載?
- java類可以通過實例.getClass.getClassLoader()得知
- 接口由AppClassLoader(System ClassLoader,可以由ClassLoader.getSystemClassLoader()獲得實例)載入
- ClassLoader類由bootstrap loader載入
NoClassDefFoundError和ClassNotFoundException
- NoClassDefFoundError:當java源文件已編譯成.class文件,但是ClassLoader在運行期間在其搜尋路徑load某個類時,沒有找到.class文件則報這個錯
- ClassNotFoundException:試圖通過一個String變量來創建一個Class類時不成功則拋出這個異常
垃圾回收分爲兩大步驟:識別垃圾 和回收垃圾
識別垃圾有兩大基本方法
1.計數器法
每個對象有一個相應的計數器,統計當前被引用的個數,每次被引用或者失去引用都會更新該計數器。
優點:識別垃圾快,只需判斷計數器是否爲零。
缺點:增加了維護計數器的成本,無法在對象互相引用的情況下識別垃圾,因此,適用於對實時性要求非常高的系統。
2.追蹤法
從根對象(例如局部變量)出發,逐一遍歷它的引用。若無法被掃描到,即認定爲垃圾,實際情況中一般採用該方法。
回收垃圾最重要的是要最大限度地減少內存碎片。
兩種兩大基本方法:
1.移動活對象覆蓋內存碎片,使對象間的內存空白增大。
2.拷貝所有的活對象到另外一塊完整的空白內存,然後一次釋放原來的內存。
通常第二種方法能夠最大的減少內存碎片,但是缺點是在拷貝過程中會終止程序的運行。
引入分級的概念,通常一個程序中大部分對象的生命週期很短,只有小部分的對象有比較長的生命。而恰恰使得拷貝方法性能打折扣的是重複拷貝那些長命的對象。因此,把對象分成幾個級別,在低級別呆到一定時間就將其升級。相應地越高級別,回收的次數越少。最理想的情況是,每次回收最低級別的對象全部失效,一次性就可以回收該級別所有內存,提高效率。同時,由於每次只回收一個級別,不需遍歷所有對象,控制了整個回收的時間。
由於垃圾識別是通過識別引用來達到,爲了增加程序對垃圾回收的控制。提供了引用對象的概念,細化了引用的類型,分別是StrongReference,SoftReference, WeakReference, PhantomReference。其中強引用就是普通的java引用,其他三種類型相當於一個包裝器,一方面使得垃圾回收器區分引用類型做不同的處理,另一方面程序通過他們仍然可以得到強引用。
分代垃圾回收機制:
如上圖所示,現代GC採用分區管理機制的JVM將JVM所管理的所有內存資源分爲2個大的部分。永久存儲區(Permanent Space)和堆空間(The Heap Space)。其中堆空間又分爲新生區(Young (New) generation space)和養老區(Tenure (Old) generation space),新生區又分爲伊甸園(Eden space),倖存者0區(Survivor 0 space)和倖存者1區(Survivor 1 space)。具體分區如下圖:
那JVM他的這些分區各有什麼用途,請看下面的解說。
永久存儲區(Permanent Space):永久存儲區是JVM的駐留內存,用於存放JDK自身所攜帶的Class,Interface的元數據,應用服務器允許必須的Class,Interface的元數據和Java程序運行時需要的Class和Interface的元數據。被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉JVM時,釋放此區域所控制的內存。
堆空間(The Heap Space):是JAVA對象生死存亡的地區,JAVA對象的出生,成長,死亡都在這個區域完成。堆空間又分別按JAVA對象的創建和年齡特徵分爲養老區和新生區。
新生區(Young (New) generation space ):新生區的作用包括JAVA對象的創建和從JAVA對象中篩選出能進入養老區的JAVA對象。
伊甸園(Eden space):JAVA對空間中的所有對象在此出生,該區的名字因此而得名。也即是說當你的JAVA程序運行時,需要創建新的對象,JVM將在該區爲你創建一個指定的對象供程序使用。創建對象的依據即是永久存儲區中的元數據。
倖存者0區(Survivor 0 space)和倖存者1區(Survivor1 space):當伊甸園的控件用完時,程序又需要創建對象;此時JVM的垃圾回收器將對伊甸園區進行垃圾回收,將伊甸園區中的不再被其他對象所引用的對象進行銷燬工作。同時將伊甸園中的還有其他對象引用的對象移動到倖存者0區。倖存者0區就是用於存放伊甸園垃圾回收時所幸存下來的JAVA對象。當將伊甸園中的還有其他對象引用的對象移動到倖存者0區時,如果倖存者0區也沒有空間來存放這些對象時,JVM的垃圾回收器將對倖存者0區進行垃圾回收處理,將倖存者0區中不在有其他對象引用的JAVA對象進行銷燬,將倖存者0區中還有其他對象引用的對象移動到倖存者1區。倖存者1區的作用就是用於存放倖存者0區垃圾回收處理所幸存下來的JAVA對象。
養老區(Tenure (Old) generation space):用於保存從新生區篩選出來的JAVA對象。
上面我們看了JVM的內存分區管理,現在我們來看JVM的垃圾回收工作是怎樣運作的。首先當啓動J2EE應用服務器時,JVM隨之啓動,並將JDK的類和接口,應用服務器運行時需要的類和接口以及J2EE應用的類和接口定義文件也及編譯後的Class文件或JAR包中的Class文件裝載到JVM的永久存儲區。在伊甸園中創建JVM,應用服務器運行時必須的JAVA對象,創建J2EE應用啓動時必須創建的JAVA對象;J2EE應用啓動完畢,可對外提供服務。
JVM在伊甸園區根據用戶的每次請求創建相應的JAVA對象,當伊甸園的空間不足以用來創建新JAVA對象的時候,JVM的垃圾回收器執行對伊甸園區的垃圾回收工作,銷燬那些不再被其他對象引用的JAVA對象(如果該對象僅僅被一個沒有其他對象引用的對象引用的話,此對象也被歸爲沒有存在的必要,依此類推),並將那些被其他對象所引用的JAVA對象移動到倖存者0區。
如果倖存者0區有足夠控件存放則直接放到倖存者0區;如果倖存者0區沒有足夠空間存放,則JVM的垃圾回收器執行對倖存者0區的垃圾回收工作,銷燬那些不再被其他對象引用的JAVA對象(如果該對象僅僅被一個沒有其他對象引用的對象引用的話,此對象也被歸爲沒有存在的必要,依此類推),並將那些被其他對象所引用的JAVA對象移動到倖存者1區。
如果倖存者1區有足夠控件存放則直接放到倖存者1區;如果倖存者0區沒有足夠空間存放,則JVM的垃圾回收器執行對倖存者0區的垃圾回收工作,銷燬那些不再被其他對象引用的JAVA對象(如果該對象僅僅被一個沒有其他對象引用的對象引用的話,此對象也被歸爲沒有存在的必要,依此類推),並將那些被其他對象所引用的JAVA對象移動到養老區。
如果養老區有足夠控件存放則直接放到養老區;如果養老區沒有足夠空間存放,則JVM的垃圾回收器執行對養老區區的垃圾回收工作,銷燬那些不再被其他對象引用的JAVA對象(如果該對象僅僅被一個沒有其他對象引用的對象引用的話,此對象也被歸爲沒有存在的必要,依此類推),並保留那些被其他對象所引用的JAVA對象。如果到最後養老區,倖存者1區,倖存者0區和伊甸園區都沒有空間的話,則JVM會報告“JVM堆空間溢出(java.lang.OutOfMemoryError: Java heap space)”,也即是在堆空間沒有空間來創建對象。
這就是JVM的內存分區管理,相比不分區來說;一般情況下,垃圾回收的速度要快很多;因爲在沒有必要的時候不用掃描整片內存而節省了大量時間。