類加載器是一個用來加載類文件的類。Java源代碼經過javac編譯器編譯成類文件。而後JVM來執行類文件中的字節碼來執行程序。類加載器負責加載文件系統、網絡或其餘來源的類文件。java
Java類加載器的做用就是在運行時加載類。Java類加載器基於三個機制:委託、可見性和單一性。web
委託機制:將加載一個類的請求交給父類加載器,若是這個父類加載器不可以找到或者加載這個類,那麼再加載它。bootstrap
可見性:子類的加載器能夠看見全部的父類加載器加載的類,而父類加載器看不到子類加載器加載的類。數組
單一性:僅加載一個類一次,這是由委託機制確保子類加載器不會再次加載父類加載器加載過的類。緩存
正確理解類加載器可以幫你解決NoClassDefFoundError和java.lang.ClassNotFoundException,由於它們和類的加載相關。tomcat
Java 中的類加載器大體能夠分紅兩類,一類是系統提供的,另一類則是由 Java 應用開發人員編寫的。安全
有三種默認使用的類加載器:Bootstrap類加載器、Extension類加載器和System類加載器(或者叫做Application類加載器)。服務器
引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫(jre/lib/rt.jar)或-Xbootclasspath參數指定路徑的目錄,是用原生C++代碼來實現的,並不繼承自java.lang.ClassLoader。(出於安全考慮,Bootstrap啓動類加載器只加載包名爲java、javax、sun等開頭的類)網絡
擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫Java 虛擬機的實現會提供一個擴展庫目錄JRE/lib/ext或者java.ext.dirs指向的目錄。該類加載器在此目錄裏面查找並加載 Java 類。 數據結構
系統類加載器(system/Application class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。通常來講,Java 應用的類都是由它來完成加載的。能夠經過 ClassLoader.getSystemClassLoader()來獲取它。
自定義類加載器(custom class loader):除了系統提供的類加載器之外,開發人員能夠經過繼承 java.lang.ClassLoader類的方式實現本身的類加載器,以知足一些特殊的需求。
public static void main(String[] args) {
//application class loader
System.out.println(ClassLoader.getSystemClassLoader());
//extensions class loader
System.out.println(ClassLoader.getSystemClassLoader().getParent());
//bootstrap class loader
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
}
輸出:
sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742
null
每種類加載器都有設定好從哪裏加載類。
將class文件字節碼內容加載到內存中,並將這些靜態數據轉換成方法去中的運行時數據結構,在懟中生成一個表明這個類的Class對象 ,做爲方法去類數據的訪問入口。
類緩存
標準的類加載器能夠按要求查找類,但一旦某個類被加載到類加載器中,它將維持加載(緩存)一段時間,不過,JVM垃圾回收器能夠回收這些Class。
代理模式:交給其餘加載器來加載指定類
雙親委託機制:某個特定的類加載器在接到加載類的請求時,首先將加載任務委託交給父類加載器,父類加載器又將加載任務向上委託,直到最父類加載器,若是最父類加載器能夠完成類加載任務,就成功返回,若是不行就向下傳遞委託任務,由其子類加載器進行加載。
雙親委託機制是爲了保證Java核心庫的類型安全。保證了不會出現用戶自定義java.lang.Object類的狀況。
類加載器除了用於加載類,也是安全的最基本的屏障
雙親委託機制是代理模式的一種。
並非全部的類加載都是雙親模式,好比tomcat服務器也是使用代理模式,不一樣的是它首先嚐試去加載類,若是找不到在代理給父類加載器,這與通常加載器是相反的。
Class.forname()是一個靜態方法,最經常使用的是Class.forname(String className);根據傳入的類的全限定名返回一個Class對象.該方法在將Class文件加載到內存的同時,會執行類的初始化.
ClassLoader.loadClass():這是一個實例方法,須要一個ClassLoader對象來調用該方法,該方法將Class文件加載到內存時,並不會執行類的初始化,直到這個類第一次使用時才進行初始化,該方法由於須要獲得一個ClassLoader對象,因此能夠根據須要指定使用哪一個類加載器.
1、繼承java.lang.ClassLoader
2、首先檢查請求的類型是否已經被這個類裝載器裝載到命名空間中,若是已經裝載,直接返回。
3、委派類加載請求給父類加載器,若是父類加載器可以完成,則返回父類加載器加載的Class實例。
4、重寫本類加載器的findClass(...)方法,試圖獲取對應字節碼,若是獲取獲得,則調用defineClass(...)導入類型到方法區,若是獲取不到對應的字節碼或者其餘緣由失敗,則終止加載過程。
注意:被兩個類加載器加載的同一個類,JVM不認爲是相同的類。
/** * 自定義文件類加載器 */ public class FileSysLoader extends ClassLoader { private String rootDir; public FileSysLoader(String rootDir) { this.rootDir = rootDir; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //檢查有沒有加載過這個類,若是已經加載直接返回這個類。 Class<?> c = findLoadedClass(name); if (c!=null) { return c; }else{ ClassLoader parent = this.getParent(); try { //委派給父類加載器 c = parent.loadClass(name); } catch (Exception e) { } if(c!=null){ return c; }else{ //若是父類也沒加載,則本身加載,讀取文件 進行加載 byte[] classData = getClassData(name); if(classData==null){ //沒有讀取到文件,拋出異常 throw new ClassNotFoundException(); }else{ //生成Class對象 c = defineClass(name, classData, 0,classData.length); } } } return c; } /** * 文件內容轉爲字節數組 */ private byte[] getClassData(String classname){ String path = rootDir + File.separatorChar + classname.replace('.', File.separatorChar)+".class"; InputStream is = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try{ is = new FileInputStream(path); byte[] buffer = new byte[1024]; int temp=0; while((temp=is.read(buffer))!=-1){ baos.write(buffer, 0, temp); } return baos.toByteArray(); }catch(Exception e){ e.printStackTrace(); return null; }finally{ try { if(is!=null){ is.close(); } } catch (IOException e) { e.printStackTrace(); } try { if(baos!=null){ baos.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
使用:
public class TestFileLoader { public static void main(String[] args) throws Exception { FileSysLoader loader = new FileSysLoader("C:/Person"); FileSysLoader loader1 = new FileSysLoader("C:/Person"); Class<?> c1 = loader.loadClass("com.temp.bytecodeop.Person"); Class<?> c2 = loader1.loadClass("com.temp.bytecodeop.Person"); Class<?> c3 = loader1.loadClass("com.temp.bytecodeop.Person"); Class<?> c4 = loader.loadClass("java.lang.String"); System.out.println(c1.hashCode()); System.out.println(c2.hashCode()); System.out.println(c3.hashCode()); System.out.println(c4.hashCode()); System.out.println(c3.getClassLoader()); System.out.println(c1.getClassLoader().getParent()); System.out.println(c4.getClassLoader()); } }
public class NetClassLoader extends ClassLoader { private String rootUrl; public NetClassLoader(String rootUrl){ this.rootUrl = rootUrl; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { Class<?> c = findLoadedClass(name); if(c!=null){ return c; }else{ ClassLoader parent = this.getParent(); try { c = parent.loadClass(name); } catch (Exception e) { } if(c!=null){ return c; }else{ byte[] classData = getClassData(name); if(classData==null){ throw new ClassNotFoundException(); }else{ c = defineClass(name, classData, 0,classData.length); } } } return c; } private byte[] getClassData(String classname){ String path = rootUrl +"/"+ classname.replace('.', '/')+".class"; InputStream is = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try{ URL url = new URL(path); //經過URL獲取流 is = url.openStream(); byte[] buffer = new byte[1024]; int temp=0; while((temp=is.read(buffer))!=-1){ baos.write(buffer, 0, temp); } return baos.toByteArray(); }catch(Exception e){ e.printStackTrace(); return null; }finally{ try { if(is!=null){ is.close(); } } catch (IOException e) { e.printStackTrace(); } try { if(baos!=null){ baos.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
熱部署就是利用同一個class文件不一樣的類加載器在內存建立出兩個不一樣的class對象因爲JVM在加載類以前會檢測請求的類是否已加載過,若是被加載過,則直接從緩存獲取,不會從新加載。
同一個加載器只能加載一次,屢次加載將報錯,所以實現的熱部署必須讓同一個class文件能夠根據不一樣的類加載器重複加載,
實現所謂的熱部署。自定義加載器後,直接調用findClass()方法,而不是調用loadClass()方法,由於ClassLoader中loadClass()方法體回調用findLoadedClass()方法進行了檢測是否已被加載,所以咱們直接調用findClass()方法就能夠繞過這個問題。
前面的文件加載,使用了,父類委託方式,咱們這裏直接寫本身加載。
/** * 熱部署類加載器 */ public class FileDeployLoader extends ClassLoader { private String rootDir; public FileDeployLoader(String rootDir) { this.rootDir = rootDir; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //檢查有沒有加載過這個類,若是已經加載直接返回這個類。 byte[] classData = getClassData(name); Class<?> c = null; if(classData==null){ //沒有讀取到文件,拋出異常 throw new ClassNotFoundException(); }else{ //生成Class對象 c = defineClass(name, classData, 0,classData.length); } return c; } /** * 文件內容轉爲字節數組 */ private byte[] getClassData(String classname){ String path = rootDir + File.separatorChar + classname.replace('.', File.separatorChar)+".class"; InputStream is = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try{ is = new FileInputStream(path); byte[] buffer = new byte[1024]; int temp=0; while((temp=is.read(buffer))!=-1){ baos.write(buffer, 0, temp); } return baos.toByteArray(); }catch(Exception e){ e.printStackTrace(); return null; }finally{ try { if(is!=null){ is.close(); } } catch (IOException e) { e.printStackTrace(); } try { if(baos!=null){ baos.close(); } } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) throws ClassNotFoundException { FileSysLoader loader = new FileSysLoader("C:/Person"); //直接調用findClass()方法,而不是調用loadClass()方法 //由於ClassLoader中loadClass()方法體回調用findLoadedClass()方法進行了檢測是否已被加載 Class<?> c1 = loader.findClass("com.temp.bytecodeop.Person"); } }
/** * 簡單加密類 * 字節取反加密 */ public class EncrptUtil { public static void main(String[] args) { //調用加密方法加密一個*.class文件 } public static void encrpt(String src, String dest){ FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream(src); fos = new FileOutputStream(dest); int temp = -1; while((temp=fis.read())!=-1){ fos.write(temp^0xff); //加密 } } catch (Exception e) { e.printStackTrace(); }finally{ try { if(fis!=null){ fis.close(); } } catch (IOException e) { e.printStackTrace(); } try { if(fos!=null){ fos.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
/** * 加密解密類加載器 */ class DecrptClassLoader extends ClassLoader { private String rootDir; public DecrptClassLoader(String rootDir){ this.rootDir = rootDir; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { Class<?> c = findLoadedClass(name); if(c!=null){ return c; }else{ ClassLoader parent = this.getParent(); try { c = parent.loadClass(name); } catch (Exception e) { } if(c!=null){ return c; }else{ byte[] classData = getClassData(name); if(classData==null){ throw new ClassNotFoundException(); }else{ c = defineClass(name, classData, 0,classData.length); } } } return c; } private byte[] getClassData(String classname){ String path = rootDir +File.separatorChar+ classname.replace('.', File.separatorChar)+".class"; InputStream is = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try{ is = new FileInputStream(path); int temp = -1; while((temp=is.read())!=-1){ baos.write(temp^0xff); //解密,再取反 } return baos.toByteArray(); }catch(Exception e){ e.printStackTrace(); return null; }finally{ try { if(is!=null){ is.close(); } } catch (IOException e) { e.printStackTrace(); } try { if(baos!=null){ baos.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
在Java中存在着不少服務提供者接口(Service Provider Interface,SPI),SPI 的接口屬於 Java 核心庫,通常存在rt.jar包中,由Bootstrap類加載器加載。這些接口容許第三方爲它們提供實現,如常見的 SPI 有 JDBC、JCE、JAXP、JBI、JNDI等,而這些SPI 的第三方實現代碼則是做爲Java應用所依賴的 jar 包被存放在classpath路徑下,由系統加載器來加載,但SPI的核心接口類是由引導類加載器來加載的,而Bootstrap類加載器沒法直接加載SPI的實現類,同時因爲雙親委派模式的存在,Bootstrap類加載器也沒法反向委託AppClassLoader加載器SPI的實現類。在這種狀況下,咱們就須要一種特殊的類加載器來加載第三方的類庫。
所以,線程類加載器是很好的選擇,它爲了拋棄雙親委派加載鏈模式。
每一個線程都有一個關聯的上下文加載器,若是new一個新的線程,若是沒有手動設置上下文類加載器,線程將繼承其父線程的上下文類加載器,經過java.lang.Thread類中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法來獲取和設置線程的上下文類加載器。
rt.jar核心包是有Bootstrap類加載器加載的,其內包含SPI核心接口類,因爲SPI中的類常常須要調用外部實現類的方法,而jdbc.jar包含外部實現類(jdbc.jar存在於classpath路徑)沒法經過Bootstrap類加載器加載,所以只能委派線程上下文類加載器把jdbc.jar中的實現類加載到內存以便SPI相關類使用。它在執行過程當中拋棄雙親委派加載鏈模式,使程序能夠逆向使用類加載器。
public class LoaderTest { public static void main(String[] args) throws ClassNotFoundException { ClassLoader loader = LoaderTest.class.getClassLoader(); System.out.println(loader); ClassLoader loader2 = Thread.currentThread().getContextClassLoader(); System.out.println(loader2); Thread.currentThread().setContextClassLoader(new FileSysLoader("C:/Person")); System.out.println(Thread.currentThread().getContextClassLoader()); Class<LoaderTest> c = (Class<LoaderTest>) Thread.currentThread().getContextClassLoader().loadClass("com.temp.bytecodeop.Person"); System.out.println(c); System.out.println(c.getClassLoader()); } }
輸出:
sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$AppClassLoader@73d16e93
com.temp.classloader.FileSysLoader@6d06d69c
class com.temp.bytecodeop.Person
sun.misc.Launcher$AppClassLoader@73d16e93
Tomcat不能使用系統默認的類加載器
若是使用默認類加載器,能夠直接操做系統目錄,不安全。對於web應用服務器,類加載器的實現方式和通常的Java運用不一樣。
每一個web應用都有一個對應類加載器實例。該類加載器也使用代理模式(不一樣於雙親模式)所不一樣的是它首先嚐試去加載某個類,若是找不到再代理給父類加載器。這與通常類加載器的順序是相反的,也是爲了保證安全,這樣核心庫就不在查詢範圍內。
Tomcat爲了安全須要實現本身的類加載器。爲每一個webapp提供本身的加載器。
(Open Service Gateway Initative)是面向Java的動態模塊系統,它爲開發人員提供了面向服務和基於組件的運行環境,並提供標準的方式用來管理軟件的生命週期。
OSGI已經被實現和部署在不少產品上,在開源社區也獲得普遍的支持,Eclipse也是基於OSGI技術來構建的。
OSGi 中的每一個模塊(bundle)都包含 Java 包和類。模塊能夠聲明它所依賴的須要導入(import)的其它模塊的 Java 包和類(經過 Import-Package),也能夠聲明導出(export)本身的包和類,供其它模塊使用(經過 Export-Package)。也就是說須要可以隱藏和共享一個模塊中的某些 Java 包和類。這是經過 OSGi 特有的類加載器機制來實現的。OSGi 中的每一個模塊都有對應的一個類加載器。它負責加載模塊本身包含的 Java 包和類。當它須要加載 Java 核心庫的類時(以 java開頭的包和類),它會代理給父類加載器(一般是啓動類加載器)來完成。當它須要加載所導入的 Java 類時,它會代理給導出此 Java 類的模塊來完成加載。模塊也能夠顯式的聲明某些 Java 包和類,必須由父類加載器來加載。只須要設置系統屬性 org.osgi.framework.bootdelegation的值便可。
OSGi 模塊的這種類加載器結構,使得一個類的不一樣版本能夠共存在 Java 虛擬機中,帶來了很大的靈活性。
Equinox是OSGI的一個實現框架