簡介:html
本文是對Java的類加載機制,Class對象,反射原理等相關概念的理解、驗證和Java虛擬機中內存佈局的一些推測。本文重點講述瞭如何理解Class對象以及Class對象的做用。java
歡迎探討,若有錯誤敬請指正 設計模式
如需轉載,請註明出處 http://www.cnblogs.com/nullzx/數組
當咱們編寫好一個「.java」文件,經過javac編譯器編譯後會造成一個「.class」文件。當咱們運行該文件時,Java虛擬機就經過類加載器(類加載器本質就是一段程序)把「.class」文件加載到內存,在方法區造成該類各方法的代碼段和描述該類細節信息的常量池,同時在堆區造成一個表示該類的Class對象(一個java.lang.Class類的實例)。Class對象中存儲了指向了該類全部屬性和方法的詳細信息的指針(同時,還存儲了指向該類的父類的Class對象的指針)。咱們可以經過Class對象直接建立該類的實例,並調用該類的全部方法,這就是咱們所說的反射。安全
類加載器不只僅能夠加載本地文件系統中的「.class」文件,還能夠經過各類形式進行加載,好比經過網絡上的獲取的數據流做爲 「.class」。網絡
類加載器本質上實現一個解析的工做,把表示該類的字節數據變成方法區中的字節碼和並在堆區產生表示該類的Class對象。ide
1.1 類加載器(ClassLoader)的層次結構函數
Java默認提供的三個ClassLoader(JAVA_HOME表示JDK的安裝目錄)工具
BootStrapClassLoader:稱爲啓動類加載器,是Java類加載層次中最頂層的類加載器,負責加載JAVA_HOME\jre\lib目錄下JDK中的核心類庫,如:rt.jar、resources.jar、charsets.jar等。該加載器不是ClassLoader的子類,由C/C++語言實現其功能。佈局
ExtensionClassLoader:稱爲擴展類加載器,負責加載Java的擴展類庫,默認加載JAVA_HOME\jre\lib\ext目下的全部jar。它是ClassLoader的子類,由Java語言實現。
AppClassLoader:稱爲應用程序類加載器,負責加載當前應用程序目錄下的全部jar和class文件以及環境變量CLASSPATH指定的jar(即JAVA_HOME/lib/dt.jar和JAVA_HOME/lib/tools.jar)和第三方jar。AppClassLoader是ClassLoader的子類,由Java語言實現。
注意JDK中有兩個lib目錄,一個是JAVA_HOME/lib,另外一個是JAVA_HOME/jre/lib。
在java中,還存在兩個概念,分別是系統類加載器和線程上下文類加載器,其實都是指是AppClassLoader加載器。
1.2 類加載器雙親委派模型
ClassLoader使用的是雙親委託來搜索類。每一個ClassLoader實例都有一個父類加載器的引用(不是繼承的關係,是一個包含的關係)。
AppClassLoader的父加載器是ExtensionClassLoader,而Extension ClassLoader的父加載器是BootstrapClassLoader,而Bootstrap ClassLoader是虛擬機內置的類加載器,自己沒有父加載器。
(圖片來自於http://blog.csdn.net/u011080472/article/details/51332866)
當一個ClassLoader對象須要加載某個類時,在它試圖親自搜索某個類以前,先把這個任務委託給它的父類加載器,父類加載器繼續向上委託,直到BootstrapClassLoader類加載器爲止。即,首先由最頂層的類加載器BootstrapClassLoader在指定目錄試圖加載目標類,若是沒加載到,則把任務回退給ExtensionClassLoader,讓它在指定目錄進行加載,若是它也沒加載到,則繼續回退給AppClassLoader 進行加載,以此類推。若是全部的加載器都沒有找到該類,則拋出ClassNotFoundException異常。不然將這個找到的「*.class」文件進行解析,最後返回表示該類的Class對象。
java代碼中咱們只能使用ExtensionClassLoader和AppClassLoader的實例,這兩種類加載器分別有且只有一個實例。咱們沒法經過任何方法建立這兩個類的額外的實例,能夠理解爲設計模式中的單例模式。
1.3 爲何要使用雙親委託這種模型?
1)這樣能夠避免重複加載,當父親已經加載了該類的時候,子類就沒有必要,也不該該再加載一次。
2)核心類經過Java自帶的加載器加載,能夠確保這些類的字節碼沒有被篡改,保證代碼的安全性。
JVM在斷定兩個Class對象是否相同時,不只要知足兩個類名相同,並且要知足由同一個類加載器加載。只有二者同時知足的狀況下,JVM才認爲這兩個Class對象是相同的。
1.4 自定義類加載器
除了Java默認提供的三個類加載器以外,用戶還能夠根據須要定義自已的類加載器,自定義的類加載器都必須繼承自java.lang.ClassLoader類。
既然JVM已經提供了默認的類加載器,爲何還要定義自已的類加載器呢?
1)由於Java中提供的默認ClassLoader,只加載指定目錄下的jar和class,若是咱們想加載其它位置的class文件或jar時就須要定義本身的ClassLoader。
2)對於那些已經加密過的Class文件,自定義ClassLoader能夠在解析Class文件前,進行解密操做。這樣相互配合的方式保證了代碼的安全性。
1.5 自定義類加載器的步驟
主要分爲兩步
1)繼承java.lang.ClassLoader
2)重寫父類的findClass方法
下面是API文檔中給出的自定義加載器的實現模型
class NetworkClassLoader extends ClassLoader { String host; int port; public Class findClass(String name) { byte[] b = loadClassData(name); return defineClass(name, b, 0, b.length); } private byte[] loadClassData(String name) { // load the class data from the connection . . . } }
下面的代碼是一個類加載器的具體實現。MyClassLoader類加載器主要加載任意指定目錄下的「*.class」文件,而這個指定的目錄又不在環境變量ClassPath所表示的目錄中。
package demo; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class MyClassLoader extends ClassLoader { private String path; @Override public Class<?> findClass(String name){ byte[] data = null; try { data = loadClassData(path); } catch (IOException e) { e.printStackTrace(); } return defineClass(name, data, 0, data.length); } private byte[] loadClassData(String path) throws IOException{ File f = new File(path); FileInputStream fis = new FileInputStream(f); byte[] data = new byte[(int) f.length()]; fis.read(data); fis.close(); return data; } /* * 定義了帶兩個參數的loadClass方法,爲了多傳遞一個path參數 * 內部必定要調用父類的loadClass方法,由於該方法內實現了雙親委派模型 */ public Class<?> loadClass(String path, String name) throws ClassNotFoundException{ this.path = path; return super.loadClass(name); } public static void main(String[] args) throws ClassNotFoundException{ MyClassLoader mcl = new MyClassLoader(); /*打印當前類加載器的父加載器*/ System.out.println(mcl.getParent()); System.out.println("=========="); Class<?> cls1 = mcl.loadClass("D:/用戶目錄/個人文檔/Eclipse/Person.class" ,"javalearning.Person"); System.out.println(cls1.getClassLoader()); System.out.println("=========="); Class<?> cls2 = mcl.loadClass(null, "java.lang.Thread"); System.out.println(cls2.getClassLoader()); System.out.println("=========="); } }
經過代碼實現能夠看出,自定義類加載器的核心精髓是調用ClassLoader類中的defineClass方法。
下面是運行結果
sun.misc.Launcher$AppClassLoader@4e0e2f2a ========== demo.MyClassLoader@2a139a55 ========== null ==========
從運行結果看出,MyClassLoader的父加載器是AppClassLoader(這是在ClassLoader的構造函數中缺省的實現方式)。Person.Class由MyClassLoader加載(父類加載器都沒有加載成功),而當MyClassLoader加載String.class時,委託到BootstrapClassLoader加載,發現BootstrapClassLoader已加載完畢,結果null表示String類的加載器是BootstrapClassLoader。
Person類
package javalearning; public class Person{ public int age; public String name; public Person(){ name = "zx"; age = 18; } @Override public String toString(){ return name +" "+ age; } }
經過java的語法學習,咱們知道如下三點
1)java.lang.Class類繼承java.lang.Object類
2)按照語法規則,建立一個java.lang.Class對象必須先建立它的父類(java.lang.Object)的一個對象(準確的說是開闢一片內存區域做爲Class對象,並它其中的一部分區域做爲Object對象)
3)按照語法規則,建立一個類的對象,必須先存在表示該類的java.lang.Class對象
可是這三點又是矛盾的。這兩個對象的建立沒有辦法順序實現。因此不是先建立好一個,再建立另外一個,而是經過自舉實現的。也就是說是經過自舉程序將兩個對象建立好,而後才進入java的運行環境。而自舉程序自己不是由Java語言實現的,而是由C和C++實現的。
全部的java.lang.Class對象的建立不是經過構造函數建立的,而是經過加載器生成的。每一個類都有對應的用於反射該類的Class對象,每一個類有且只對應一個Class對象。
每個類都從Object類中繼承了一個getClass的實例方法,返回表示該類的Class對象。
在java的堆區中,有一個特殊的Class對象,即Class.class。Class.class對象有兩層含義。
第一,能夠把它當作一個普通的對象,一個屬於Class類的實例。
第二,它又表示是Class類自己用於反射的對象(因此該對象的getClass方法返回它自己)或者說表示Class類自己的Class對象。
咱們不能經過Class.class的newInstance方法產生Class類的實例,若是這麼作,會拋出異常。另外一個方面,假設可以產生這樣的對象,咱們怎麼知道這個對象應該對應哪個類呢?
類加載器也是一個類,也有對應的Class對象,可是Class對象又必須經過加載器的實例產生,顯然這兩點又是矛盾的。
三個默認的類加載器中ExtensionClassLoader和AppClassLoader是由java代碼實現的,而BootstrapClassLoader是由C/C++實現的。也就是說BootstrapClassLoader沒有,不須要有,也不可能有對應Class對象。ExtensionClassLoader類的實例和它對應ExtensionClassLoader.class對象都是由BootstrapClassLoader一併加載建立完成。建立完成後,再由ExtensionClassLoader對象加載AppClassLoader.class。
4.1 Class對象中到底存了什麼?
從已有資料來看,Class對象在不一樣的虛擬機在實現上存儲的內容都不一致,可是理論上來說, Class對象內部必定存儲了方法區中該類的全部方法簽名,屬性簽名,和每一個方法對應的字節碼的地址。
4.2 實例和實例方法之間的關係?
obj.setName(「zhang san」)
在實際執行過程當中等價於
setName(obj, 「zhang san」)
也就是對象時做爲參數傳遞到實例方法裏面的,對象自己不含指向該類方法的指針(Class對象並不含有Class類的方法的指針,但含有表示該類的全部方法的指針,可能有點繞,本身要理解一下)。方法的具體實現都位於方法區中相應的代碼段中。當虛擬機調用該方法時,只要將虛擬機執行引擎的PC(程序計數器)指向該方法的地址,而後將實例存入該方法的棧幀中便可。經過實例直接調用方法時,實際上沒有,也沒有必要經過Class對象。
下面的示例表示了,鎖住Person.Class對象不能阻止其它線程的代碼建立Person類的實例,並調用實例方法。
package javalearning; public class ClassLockTest { public static class T1 extends Thread{ private Class<?> cls; private boolean done; public T1(Class<?> cls){ this.cls = cls; } @Override public void run() { synchronized(cls){ while(!done){ } } } public void done(){ done = true; } } public static void main(String[] args) throws InterruptedException{ /*咱們先讓線程t1鎖住Person.class對象,而後在主線程中建立該對象的實例,並調用toString方法*/ Class<?> cls = Person.class; T1 t1 = new T1(cls); t1.start(); while(!t1.isAlive()){ System.out.println("t1 is not alive"); Thread.sleep(500); } Person p = new Person(); System.out.println(p); t1.done(); System.out.println("over"); } }
運行結果
zx 18 over
4.3 Class對象有哪些功能?
1)反射(關於反射的使用會在後續博客中講解)
2)多態的實現。
咱們經過如下代碼來說解Class對象在多態中的應用
package demo; public class ClassObjectDemo1 { /*定義兩個具備繼承關係的類,兩個類內部有同一個方法的不一樣實現*/ public static class Person{ public void speak(){ System.out.println("i am a person"); } } public static class Coder extends Person{ public void speak(){ System.out.println("i am a coder"); } } /*定義了一個靜態方法,靜態方法會調用對應類型的speak方法*/ public static void speakByType(Person p){ p.speak(); } public static void main(String[] args) { Person p0 = new Coder(); speakByType(coder); Person p1 = new Person(); speakByType(person); } }
運行結果
i am a coder i am a person
咱們定義的靜態方法speakByType,顯然編譯器在編譯這個方法的時候不能肯定到底調用哪個speak方法,須要依據對象的具體類型才能肯定符號引用。
如今咱們經過字節碼工具重點查看一下speakByType的字節碼
public static void speakByType(demo.ClassObjectDemo1$Person p) { /* L20 */ 0 aload_0; /* p */ 1 invokevirtual 16; /* void speak() */ /* L21 */ 4 return; }
咱們發現裏面出現了一條字節碼調用語句 invokevirtual方法。而invokevirtual指令在執行時,首先會找到當前對象的類的Class對象,而後經過該Class對象查找sepak方法,若是經過該Class對象查找到了簽名一致的的sepak方法就會調用它。每一個Class對象都會持有表示父類的Class對象的引用(經過Class的getSuperClass方法獲取),Object.class除外,本身想一想爲啥?當在子類的Class對象中沒有找到簽名一致的speak方法時,就從其父類的Class對象中繼續查找簽名一致的speak方法。顯然若是尚未找到,則會沿着有繼承關係的Class的路徑繼續向下查找,若是直到Object.class對象中還未找到就會拋出異常。
3)instace of和向上轉型
當咱們判斷某個對象是否屬於某個類時,好比 a instance of A,顯然只要判斷
a.getClass() == A.class && a.getClass().classLoader() == A.classLoader()便可,若是不知足就沿着getSuperClass的路徑繼續向下找,若是直到Object.class還不知足條件就返回false。同理在運行時,咱們還能依據Class對象判斷向上轉型是否正確。
4.4 Class.class對象存在的意義是什麼?
咱們一般不會經過Class.class對象來間接訪問forName方法和其它相應方法,而是直接使用該類的方法。因此一種可能的狀況就是利用Class對象進行類型判斷,即判斷一個對象是否是Class對象仍是普通對象(判斷obj.getClass() == Class.class是否成立)。另外一種可能就是保持概念的完整性,每個類都有一個Class對象與之對應。
4.5 假設B類繼承了A類,那麼B類的實例在內存中應該是什麼樣子的?
java語言的設計者考慮到對象向上轉型等問題,每個類的數據成員顯然要按照繼承關係的前後順序排列,同時考慮執行效率,還存在數據對齊等問題。
4.6 Java在運行時的內存佈局
用一個例子來看看javaVM運行時,類、對象、Class對象、ClassLoader的關係
package demo; import java.lang.reflect.InvocationTargetException; public class ClassObjectDemo0 { /*定義兩個具備繼承關係的類,兩個類內部都爲空*/ public static class Person{ } public static class Coder extends Person{ } /*哈哈,main函數拋出的異常彷佛有點多*/ public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InterruptedException{ /*經過對象找到該類的Class對象*/ Coder coder = new Coder(); System.out.println(coder.getClass()); System.out.println("========================"); /*經過Class對象能夠看出繼承關係*/ System.out.println(Coder.class); System.out.println(Coder.class.getSuperclass()); System.out.println(Coder.class.getSuperclass().getSuperclass()); System.out.println("========================"); /*查看每一個Class對象的類加載器*/ System.out.println(Coder.class.getClassLoader()); System.out.println(Person.class.getClassLoader()); System.out.println(Object.class.getClassLoader()); System.out.println("========================"); /*每一個Class對象都是Class類的實例*/ System.out.println(Coder.class.getClass()); System.out.println(Person.class.getClass()); System.out.println(Object.class.getClass()); System.out.println("========================"); /*Class.class的getClass方法會返回它自己*/ System.out.println(Class.class.getClass()); /*產看Class對象的的父類*/ System.out.println(Class.class.getSuperclass()); } }
運行結果
/*經過對象找到該類的Class對象*/ class demo.ClassObjectDemo$Coder ======================== /*經過Class對象能夠看出繼承關係*/ class demo.ClassObjectDemo$Coder class demo.ClassObjectDemo$Person class java.lang.Object ======================== /*查看每一個Class對象的類加載器*/ sun.misc.Launcher$AppClassLoader@4e0e2f2a sun.misc.Launcher$AppClassLoader@4e0e2f2a null /*說明Object類的加載器是BootstrapClassLoader*/ ======================== /*每一個Class對象都是Class類的實例*/ class java.lang.Class class java.lang.Class class java.lang.Class ======================== /*Class.class的getClass方法會返回它自己*/ class java.lang.Class ======================== /*查看Class.class對象的父類*/ class java.lang.Object
經過上面的結果,咱們能夠推測這些對象,方法,加載器等在堆和棧中佈局的一種可能。
在堆區中,咱們通常的對象咱們用淺黃色表示,Class對象用淺藍色表示(原諒個人辨色能力,什麼顏色請自行體會)。
在堆區中,綠色箭頭表示getClass方法返回的對象。顯然,非Class對象的getClass方法返回這個類對應的Class對象。Class類繼承Object類,Object類定義了getClass方法,全部的Class對象也有getClass方法。若是把Class對象當作普通對象,那麼它的getClass方法就會返回表示整個Class類的Class對象,即Class.class。而Class.class對象的getClass方法返回它自己。
堆區中,每一個Class對象的黑色虛線都指向了方法區中表示該類的所有信息,因此咱們可以經過Class對象進行反射操做。
堆區中的黑色實線表示Class對象的getSupperClass方法返回的對象,因爲全部的類都繼承於Object類,因此有的Class對象最終都指向於Object.clss,而Object.class沒有父類。
咱們想要實現反射,通常使用Class.forName方法進行類加載,forName方法本質上就是調用ClassLoadr實例的loadClass方法。推測,爲了方便每一個加載器查找某個類是否已加載器過,每一個類加載器可能都有一張表,記錄每一個已加載的類和對應Class對象的地址。
4.7 數組與Class對象
不一樣數據類型,不一樣維度的數組都對應不一樣的Class對象
全部具備相同元素類型和維數的數組都共享該Class對象。
package javalearning; public class ArrayTest { public static void main(String[] args){ int[] a1 = new int[10]; int[][] a2 = new int[5][3]; Class<?> c1 = a1.getClass(); Class<?> c2 = a2.getClass(); System.out.println(c1 == c2);/*結果false*/ System.out.println(c2.getComponentType() == c1);/*結果true*/ } }
因爲數組沒有構造函數,咱們也就沒有辦法經過它的Class對象直接建立數組對象。爲了實現這個功能,JDK中就提供了Array類(java.lang.reflect.Array)來彌補這個缺陷。有關Array類的功能和使用,請參考Java的API文檔,注意區分java.util.Arrays類。
[1]. classpath、path、JAVA_HOME的做用及JAVA環境變量配置
[3]. Java魔法堂:類加載器入了個門