Java類加載器(Class loader)是一個很重要的概念,一直想寫一篇關于這個的博客,今天看了不少別人的博客,也來寫一下,希望能寫的明白。
首先明白類加載器的概念:
顧名思義,類加載器(class loader)用來加載 Java 類到 Java 虛擬機中。一般來說,Java 虛擬機使用 Java 類的方式如下:Java 源程序(.java 文件)在經過 Java 編譯器編譯之后就被轉換成 Java 字節代碼(.class 文件)。類加載器負責讀取 Java 字節代碼,并轉換成java.lang.Class類的一個實例。每個這樣的實例用來表示一個 Java 類。通過此實例的newInstance()方法就可以創建出該類的一個對象。實際的情況可能更加復雜,比如 Java 字節代碼可能是通過工具動態生成的,也可能是通過網絡下載的。例如:當我們
有一個A類,先是javac.A.java,生成了A.class文件,這個時候,再java A ,此時jvm就需要加載A類,它只得向類加載器要,此時類加載器用處的時候就來了,將A類加載給jvm,具體怎么加載后面介紹。
明白了類加載器的基本概念,我們就會想在java中一切都是對象,類加載器也是一個java類,那么誰來加載它呢?--->jvm
jvm在啟動時,會啟動jre/rt.jar里的類加載器:bootstrap classloader,用來加載java核心api;然后啟動擴展類加載器ExtClassLoader加載擴展類,并加載用戶程序加載器AppClassLoader,并指定ExtClassLoader為他的父類;
Java 中的類加載器大致可以分成兩類,一類是系統提供的,另外一類則是由 Java 應用開發人員編寫的。系統提供的類加載器主要有下面三個:
java.lang.ClassLoader。ClassLoader.getSystemClassLoader()來獲取它。系統類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導類加載器,在java中使用的是雙親委托模式,-->加載一個類時,首先BootStrap進行尋找,找不到再由ExtensionClassLoader尋找,最后才是AppClassLoader,這樣,可以有效避免安全問題。
基本概念講到這里,接下來我們看一下java.lang.ClassLoader這個類:ClassLoader為一個抽象類,由前面可知該類是由bootstrap ClassLoader加載,所有的類加載器器實現類(除了bootstrap ClassLoader)都需要繼承該類,
getParent() | 返回該類加載器的父類加載器。如果該類的父類加載器為bootstrap classloader則返回null |
loadClass(String name) | 加載名稱為name的類,返回的結果是java.lang.Class類的實例。 |
findClass(String name) | 查找名稱為name的類,返回的結果是java.lang.Class類的實例。 |
findLoadedClass(String name) | 查找名稱為name的已經被加載過的類,返回的結果是java.lang.Class類的實例。 |
defineClass(String name, byte[] b, int off, int len) | 把字節數組b中的內容轉換成 Java 類,返回的結果是java.lang.Class類的實例。這個方法被聲明為final的。 |
resolveClass(Class<?> c) | 鏈接指定的 Java 類。 |
PRotected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {// First, check if the class has already been loadedClass c = findLoadedClass(name);if (c == null) { try {if (parent != null) { c = parent.loadClass(name, false);} else { c = findBootstrapClass0(name);} } catch (ClassNotFoundException e) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); }}if (resolve) { resolveClass(c);}return c; }
//如果沒有被加載過 則調用findBootstrapClass0()方法,官方原文:
Invoke findLoadedClass(String) to check if the class has already been loaded.
Invoke the loadClass method on the parent class loader. If the parent is null the class loader built-in to the virtual machine is used, instead.
Invoke the findClass(String) method to find the class.
從上面可以看出:類加載器加載一個類,首先通過類名去判別該類是否被加載過,如果被加載過,直接去緩存中取,如果沒有被加載過,則用系統類加載器去加載它(system class loader),沒有加載成功,調用findBootstrapClass0(name)方法加載,如果兩個都沒有成功,則調用findclass(String name)方法再找該類。
線程上下文類加載器:
在Thread類里面有兩個方法:getContextClassLoader(),setContextClassLoader(cl);分別用來獲取和設置線程的上下文類加載器器,如果沒有通過setContextClassLoader(ClassLoader cl)方法進行設置的話,線程將繼承其父線程的上下文類加載器。Java 應用運行的初始線程的上下文類加載器是系統類加載器。(sun.misc.Launcher$AppClassLoader)在線程中運行的代碼可以通過此類加載器來加載類和資源。
前面提到的類加載器的代理模式并不能解決 Java 應用開發中會遇到的類加載器的全部問題。Java 提供了很多服務提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的接口由 Java 核心庫來提供,如 JAXP 的 SPI 接口定義包含在javax.xml.parsers包中。這些 SPI 的實現代碼很可能是作為 Java 應用所依賴的 jar 包被包含進來,可以通過類路徑(CLASSPATH)來找到,如實現了 JAXP SPI 的Apache Xerces所包含的 jar 包。SPI 接口中的代碼經常需要加載具體的實現類。如 JAXP 中的javax.xml.parsers.DocumentBuilderFactory類中的newInstance()方法用來生成一個新的DocumentBuilderFactory的實例。這里的實例的真正的類是繼承自javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實現所提供的。如在 Apache Xerces 中,實現的類是org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而問題在于,SPI 的接口是 Java 核心庫的一部分,是由引導類加載器來加載的;SPI 實現的 Java 類一般是由系統類加載器來加載的。引導類加載器是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫。它也不能代理給系統類加載器,因為它是系統類加載器的祖先類加載器。也就是說,類加載器的代理模式無法解決這個問題。
線程上下文類加載器正好解決了這個問題。如果不做任何的設置,Java 應用的線程的上下文類加載器默認就是系統上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就可以成功的加載到 SPI 實現的類。線程上下文類加載器在很多 SPI 的實現中都會用到。
實際上,在Java應用中所有程序都運行在線程里,如果在程序中沒有手工設置過ClassLoader,對于一般的java類如下兩種方法獲得的ClassLoader通常都是同一個this.getClass.getClassLoader();Thread.currentThread().getContextClassLoader();方法一得到的Classloader是靜態的,表明類的載入者是誰;方法二得到的Classloader是動態的,誰執行(某個線程),就是那個執行者的Classloader。對于單例模式的類,靜態類等,載入一次后,這個實例會被很多程序(線程)調用,對于這些類,載入的Classloader和執行線程的Classloader通常都不同。
Class.forNameClass.forName是一個靜態方法,同樣可以用來加載類。該方法有兩種形式:Class.forName(String name, boolean initialize, ClassLoader loader)和Class.forName(String className)。第一種形式的參數name表示的是類的全名;initialize表示是否初始化類;loader表示加載時使用的類加載器。第二種形式則相當于設置了參數initialize的值為true,loader的值為當前類的類加載器。Class.forName的一個很常見的用法是在加載數據庫驅動的時候。如Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用來加載 Apache Derby 數據庫的驅動。
/********基本概念到此為止,接下來開發自己的類加載器******************/
首先我們需要知道為什么需要開發自己的類加載器,。比如您的應用通過網絡來傳輸 Java 類的字節代碼,為了保證安全性,這些字節代碼經過了加密處理。這個時候您就需要自己的類加載器來從某個網絡地址上讀取加密后的字節代碼,接著進行解密和驗證,最后定義出要在 Java 虛擬機中運行的類來
我們自己寫的類加載器一般重寫findClass(name)這個方法即可
假設:我們有一個HelloWorld類,代碼如下:
1 public class HelloWorld 2 { 3 public HelloWorld() { 4 System.out.println("Test"); 5 } 6 public static void main(String[] args) { 7 System.out.println("Hello World"); 8 } 9 } 這時候 我們不想Java的系統類加載器(system class loader)加載它,而是我們自己的類加載器加載它,則讓我們的類加載器器類繼承java.lang.ClassLoader類,我們自己實現
findclass()方法,步驟:
1:根據構造方法傳入的類名找出自己要加載的類
2:利用java.lang.ClassLoader的findLoadedClass(name)方法,判斷是否緩存中有該類,如果有直接返回,沒有的話,則進行下一步
3:因為我們的目標是將我們要加載的類轉換成byte[]數組,然后用this.defineClass(name, b, off, len)方法定義出類,給虛擬機所以我們朝這個方向努力即可,代碼不做過多解釋,里面有一些java.nio.*的類,會專門寫一篇博客講述
1 public class MyClassLoader extends ClassLoader{ 2 3 private String rootDir; 4 5 public MyClassLoader(String rootDir){ 6 this.rootDir = rootDir; 7 } 8 9 10 @Override11 protected Class<?> findClass(String name) throws ClassNotFoundException {12 Class<?> clazz = this.findLoadedClass(name);13 if(clazz == null){14 try {15 String classFile = nameToPath(name);17 FileInputStream in = new FileInputStream(classFile);18 //將目標類寫入19 FileChannel channel = in.getChannel();20 21 ByteArrayOutputStream out = new ByteArrayOutputStream();22 WritableByteChannel byteChannel = Channels.newChannel(out);23 //定義一個人長度為1024大小的byte緩沖區24 ByteBuffer buffer = ByteBuffer.allocate(1024);25 while(true){27 int readNum = channel.read(buffer);28 if(readNum == -1){30 break;31 }33 buffer.flip();34 byteChannel.write(buffer);35 buffer.clear();37 }38 39 channel.close();40 byte[] byteArray = out.toByteArray();41 clazz = this.defineClass(name, byteArray, 0, byteArray.length);42 43 } catch (FileNotFoundException e) {44 e.printStackTrace();45 } catch (IOException e) {46 e.printStackTrace();47 } 49 }50 return clazz;51 }52 53 54 public String nameToPath(String className){55 StringBuffer sb = new StringBuffer(rootDir); 56 className = className.replace('.', File.separatorChar) + ".class"; 57 sb.append(File.separator + className);58 return sb.toString(); 59 }60 61 }最后測試一下我們寫的類就可以了:
1 public class Test{ 2 public static void main(String[] args) { 3 try { 4 MyClassLoader myclassloader = new MyClassLoader("f:"+File.separator+"workbase"); 5 Class<?> clazz = myclassloader.findClass("HelloWorld"); 6 clazz.newInstance(); 7 System.out.println(clazz.getClassLoader()); 8 9 } catch (Exception e) {10 e.printStackTrace();11 }12 }13 }運行結果:MyClassLoader@17609872,可以看出確實使用了我們自己寫的類加載器。
總結:個人感覺明白這些應該就OK了吧,當然看到別人博客還有許多更加深層次的東西就不說了
新聞熱點
疑難解答