java物件導向--類載入器及反射,java物件導向--載入
類載入器jvm 和 類的關係
當調用 java命令運行一個java程式時,會啟動一個java虛擬機器進程。同一個jvm的所有線程、所有變數都處於同一個進程裡,都使用該jvm進程的記憶體區。
jvm進程終止的情況:
1.程式運行到最後正常結束。
2.遇到System.exit()或Runtime.getRuntime.exit()。
3.遇到未捕獲的異常或錯誤
4.程式所在的平台強制結束了JVM進程
jvm進程終止,jvm記憶體中的資料將全部丟失。
類載入
當程式主動使用某個類時,如果該類還未被載入到記憶體中,系統會進行類載入。類載入指的是將類的class檔案讀入記憶體,並為之建立一個java.lang.Class的執行個體。因為java中萬物皆為對象,類也是java.lang.Class類型的對象。
類載入具體有載入、串連和初始化3個步驟,載入階段需要完成的事情有:
1)通過一個類的全限定名來擷取定義此類的二進位位元組流。
2)將這個位元組流所代表的靜態儲存結構轉化為方法區的運行時資料結構。
3)在java堆中產生一個代表這個類的Class對象,作為存取方法區中這些資料的入口。
類的載入,是由類載入器來完成的,類載入器通常由JVM提供。通過使用不同的類載入器,可以載入以下不同來源的類的位元據:
1.從本地檔案系統載入class檔案
2.從jar包中直接載入class檔案
3.通過網路載入class檔案
4.把一個java源檔案動態編譯並載入
類的串連
負責把類的位元據合并到JRE(java運行環境)中
1.驗證:檢測被載入的類是否有正確的內部結構,並和其他類協調一致
2.準備:負責為類的靜態屬性分配記憶體,並設定預設初始值
3.解析:將類的位元據中的符號引用替換成直接引用
類初始化
主要對類的靜態屬性進行初始化。可以在聲明靜態屬性時指定初始值和通過靜態初始化塊指定初始值,JVM會按照這些語句在程式中的順序依次執行。
類初始化的時機
java虛擬機器規範沒有強制性約束在什麼時候開始類載入的過程,由虛擬機器的具體實現自由把握,但是對於類的初始化,虛擬機器規範則嚴格規定了幾種情況,在類沒有進行過初始化時必須先觸發其初始化。
1.建立類的執行個體時:通過new操作符、通過反射或通過還原序列化的方式建立執行個體
2.使用類的靜態方法
3.訪問類的靜態變數(除被final修飾的靜態變數),或為靜態屬性賦值
4.通過反射建立某類或介面的java.lang.Class對象,如Class.forName("Hello")。注意ClassLoader.loadClass(name,false)方法中不會連結類,只是裝載該類,所以不會執行初始化。但使用Class的forName()靜態方法,才會導致強制初始化該類。
5.初始化某類的子類時,該子類的所有父類都會被初始化。
6.main()方法所在的主類最先會被初始化。
對於final類型的靜態屬性,如果在編譯時間(轉成.class檔案)就可以確定屬性值,那麼這個屬性可被當成編譯時間常量。編譯時間,直接替換成具體的值。所以,即使使用這個靜態屬性,也不會導致該類的初始化,相當於使用常量!!
以上情況稱為對一個類進行“主動引用”。除此之外,均不會觸發類的初始化,稱為“被動引用”,比如以下幾種情況:
1.子類訪問父類的靜態變數,子類不一定會被初始化,父類會被初始化。
2.通過數組定義來引用類,不會觸發類的初始化。
3.訪問類的常量,常量在編譯階段會存入調用類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
class SuperClass { static { System.out.println("superclass init"); } public static int value = 111;} class SubClass extends SuperClass { static { System.out.println("subclass init"); }}public class Test { public static void main(String[] args) { System.out.println(SubClass.value);// 被動引用,子類不會被初始化 SubClass[] scArr = new SubClass[5];// 被動引用,沒有輸出 }}類載入器
每個類載入器(除了根類載入器)都是java.lang.ClassLoader的執行個體,負責將.class檔案載入到記憶體中,並為之產生對應的java.lang.Class對象。
同一個.class不會被同一個類載入器載入兩次,如何判斷是同一個類:
java中,一個類用其全限定類名標識--包名+類名;jvm中,一個類用其全限定類名+其類載入器標識---包名+類名+類載入器名
載入器階層:
JVM啟動時,會形成由三個類載入器組成的初始類載入器階層:
1.Bootstrap ClassLoader 根類載入器(引導類載入器),載入java的核心類(限定的載入路徑:jdk1.8_02/jre/lib/rt.jar)。它不是java.lang.ClassLoader的子類,而是由JVM自身實現的。虛擬機器為了安全性以及功能的完整性,並不是任何存在於啟動類載入器路徑下的jar都會被載入,只有JDK內建的類會被載入,所以開發人員不要將自訂的類放在此目錄。
2.Extension ClassLoader 擴充類載入器,載入JRE的擴充目錄中JAR的類包(限定的載入路徑:%JAVA_HOME%/jre/lib/ext/或java.ext.dirs系統屬性指定的目錄)
3.System ClassLoader 系統類別載入器,載入java.class.path系統屬性或CLASSPATH環境變數所指定的jar包和類路徑,可通過ClassLoader的靜態方法ClassLoader.getSystemClassLoader()擷取系統類別載入器。如果沒有特別指定,使用者自訂的類載入器以該類載入器作為它的父載入器。也可以通過Class執行個體的getClassLoader()方法當前類的類載入器。
4.自訂類載入器
JVM系統內建的類載入器在程式運行中只能載入對應路徑的.class檔案,無法改變其搜尋路徑。如果想在運行時從其他路徑載入類,就要編寫自訂的類載入器。
程式基本的載入流程
1. 根據<JAVA_HOME>/jre/lib/i386/jvm.cfg決定是以client還是server模式運行JVM,然後載入<JAVA_HOME>/jre/bin/client|server/jvm.dll,開始啟動JVM;
2. 在啟動JVM的同時將載入Bootstrap ClassLoader(啟動類載入器,使用C/C++編寫,屬於JVM自身的一部分);
3. 通過Bootstrap ClassLoader載入Java核心API,包括sun.misc.Launcher類(ExtClassLoader和AppClassLoader是它的內部類);
4. sun.misc.Launcher類在執行初始化階段時,會建立一個自己的執行個體,在建立過程中會建立一個ExtClassLoader執行個體(指定父類載入器為null)、一個AppClassLoader執行個體(指定父類載入器為ExtClassLoader執行個體),並將AppClassLoader執行個體設定為主線程的ThreadContextClassLoader(線程上下文類載入器)。
5. 最後AppClassLoader執行個體就開始載入classpath路徑中需要的類。
類載入的機制
1.全盤負責:某類所依賴及其引用的所有類,都由同一個載入器負責載入,除非顯示使用另外一個載入器。
2.雙親委託: 當一個類載入器收到類載入的請求,首先將請求委派給父類載入器,遞迴到Bootstrap ClassLoader。然後載入器根據請求嘗試搜尋和載入類,若無法載入該類時則向子類載入器反饋資訊(拋出ClassNotFoundException),由子類載入器自己去載入。
類載入期之間的父子關係並不是類繼承上的父子關係,是採用組合的方式實現雙親委派模型的。
根類載入器是由c++實現的,不是由java語言實現,沒有繼承ClassLoader,所以擴充類載入器調用parent()返回的是null。但擴充類載入器實際上仍可以委派給根類載入器。
使用雙親委託模式的原因:被父類載入器加了的類可以避免避免被子類重新載入,因為在JVM中由全限定名+類載入器名標識類。另外可以避免載入到同sun公司核心API同名的惡意類。
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false);}//在ClassLoader中只提供了findClass()的定義,具體實現需要由子類提供。protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name);}protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先判斷該類是否已經被載入,如果已載入,則直接返回執行個體 Class c = findLoadedClass(name); if (c == null) { //如果沒有被載入,則委託給父類載入器 try { if (parent != null) { //如果存在父類載入器,就委託父類載入器載入 c = parent.loadClass(name, false); } else { //如果不存在父類載入器,則當前為擴充類載入器 //會調用啟動類載入器的native method c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果所有父類載入器都不能載入,才調用當前類載入器的載入方法 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
3.緩衝機制:保證所有載入過的Class都會被緩衝。當程式需要Class時,先從緩衝區中搜尋Class對象,沒有的話,才載入該類。
自訂類載入器
JVM中除了根載入器之外,所有類載入器都是ClassLoader子類的執行個體,開發人員通過擴充ClassLoader並重寫ClassLoader所包含的方法來實現自訂的類載入器。
如上源碼所示,loadClass()方法內部主要是雙親委派模型的實現,在最後調用findClass()方法載入類,所以自訂類載入器重寫findClass()比重寫loadClass()方便,可以避免覆蓋預設類載入器的父類委託和緩衝機制兩種策略。
ClassLoader類的核心方法:
使用自訂類載入器可以實現如下功能:
執行代碼前自動驗證數位簽章;
根據使用者提供的密碼解密代碼,從而可以實現代碼混淆器來避免反編譯class檔案;
根據使用者需求來動態載入類;
根據應用需求把其他資料以位元組碼的形式載入到應用中;
URLClassLoader 類
java 為ClassLoader提供了一個實作類別URLClassLoader ,該類也是系統類別載入器和擴充類載入器的父類,它可以從本地檔案系統和遠程主機擷取二進位檔案來載入類。
URLClassLoader的兩個構造器
URLClassLoader(URL[] urls):使用預設的父類載入器建立一個ClassLoader對象,該對象將從urls所指定的系列路徑來查詢並載入類。
URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父類載入器建立。。。
一旦獲得URLClassLoader對象後,就可以調用類載入器的 loadClass()方法來載入指定的類。
載入圖片、視頻、文本等非類資源
ClassLoader除了用於載入類外,還可以用於載入圖片、視頻等非類資源,也是採用雙親委派模型將載入資源的請求傳遞到頂層的Bootstrap ClassLoader,在其對應的目錄下搜尋資源,若失敗才逐層返回並搜尋。
相關的執行個體方法:
URL getResource(String name):資源名稱name是以 '/' 分隔的標識資源的路徑名稱,返回資源的 URL 對象的枚舉。
InputStream getResourceAsStream(String name):返回讀取指定資源的輸入資料流。
Enumeration<URL> getResources(String name)
動態載入類的方法
1. 利用現有的類載入器
// 會執行類的初始化Class.forName(String name);Class.forName(String name, true, this.getClass().getClassLoader());// 不執行類的初始化Class.forName(String name, false, this.getClass().getClassLoader());this.getClass().loadClass(String name);// 通過系統類別載入器載入,不執行初始化ClassLoader.getSystemClassLoader().loadClass(String name);// 通過線程上下文類載入器載入,不執行初始化Thread.currentThread().getContextClassLoader().loadClass(String name);
2. 利用URLClassLoader
URL[] baseUrls = {new URL("file:/d:/test/demo.jar")};URLClassLoader loader = new URLClassLoader(baseUrls);Class clazz = loader.loadClass("com.demo.Hello");Hello hello=(Hello)class.newInstance();
3. 繼承ClassLoader的自訂類載入器
public class MyClassLoader extends ClassLoader { public Class<?> findClass(String name) throws ClassNotFoundException { String classNameWithPackage = name; Class<?> clazz = null; try { name = name.replace(".", "/"); name += ".class"; URL url = MyClassLoader.class.getClassLoader().getResource(name); System.err.println(">>:" + url.getPath()); File file = new File(url.getPath()); FileInputStream fis = new FileInputStream(file); byte[] b = new byte[fis.available()]; int len = fis.read(b); fis.close(); System.err.println(len); clazz = defineClass(classNameWithPackage, b, 0, len); } catch (Exception e) { e.printStackTrace(); } return clazz; }}反射
程式在運行時接收到外部傳入的一個對象,編譯時間無法預知該對象和類可能屬於哪些類,但程式又需要調用該對象運行時類型的方法,此時可以使用反射,依靠運行時資訊(RTTI:Runtime type information)來發現該對象和類的詳細資料。
獲得Class對象
.class檔案由類裝載器裝載後,在JVM中將產生一個描述Class結構的元資訊對象,通過該java.lang.Class的對象就可以訪問到JVM中這個類的結構資訊。
java中萬事萬物皆對象,類是對象的抽象,但類本身也是對象,java.lang.Class類就是對類的抽象。
在Java程式中獲得Class對象通常由如下3中方式:
(1)使用Class類的靜態方法Class.forName(String className),字串參數的值是某個類的全限定類名。
(2)調用某個類的class屬性來擷取該類對應的Class對象。列如,Person.class將會返回Person類對應的Class對象。
(3)調用某個對象的getClass()方法,該方法定義在java.lang.Object類中。
從Class中擷取資訊
Class類提供了大量的執行個體方法來擷取該Class對象所對應類的詳細資料:
擷取Class對象對應的類所包含的構造器:(不會返回父類的構造器)
Constructor<T> getConstructor(class<?>...parameterTypes):返回對應類指定的public構造器
Constuctor<?> [ ] getConnstuctor():返回對應類所有的public構造器
Constructor<T> getDeclaredConstructor(class<?>...parameterTypes):返回對應類指定的構造器,與構造器的存取權限無關
Constuctor<?> [ ] getDeclaredConstructor():返回對應類定義的全部構造器,與構造器的存取權限無關
擷取Class對象對應類所包含的方法:
Method getMethod(String name,class<?>...parameterTypes):返回對應類指定的public方法
Method [ ] getMethods():返回對應類的所有public方法,包括繼承的方法
Method getDeclaredMethod(String name,class<?>...parameterTypes):返回對應類的指定方法,與方法的存取權限無關
Method [ ] getDeclaredMethods():返回對應類本身定義的全部方法,與方法的存取權限無關
訪問Class對象對應類所包含的Filed:
Filed getFiled(String name):返回對應類指定的public Filed
Filed [ ] getFiled():返回對應類所有的public Filed
Filed getDeclaredFiled(String name):返回對應類指定的 Filed,與 Filed的存取權限無關
Filed [ ] getDeclaredFiled():返回對應類全部 Filed,與 Filed的存取權限無關
Class類還提供了其他API,可以查看類包含的注釋、內部類、對應的父類、所實現的介面等詳細資料。
上述方法中getMethod()和getDeclaredMethod()的區別:前者返回public方法,而且包含從父類繼承的public方法,後者只返回對應類本身定義的一切方法。
getMethod()方法中,需要傳入多個類型為Class<?>的參數,具體是類似於String.class或Interger.class樣子的型別參數。因為java中有方法的重載,必鬚根據參數列表才能區分不同的方法。
public class ClassTest { private ClassTest() { } public ClassTest(String name) { System.out.println("執行有參數的構造器"); } private void info1(){ System.out.println("private方法"); } public void info() { System.out.println("執行無參數的info方法"); } public void info(String str) { System.out.println("執行有參數的info方法" + ",其str參數值:" + str); } public static void main(String[] args) throws Exception { Class clazz = Class.forName("demo3.ClassTest"); // 擷取該Class對象所對應類的全部構造器 Constructor[] ctors = clazz.getDeclaredConstructors(); System.out.println("ClassTest的全部構造器如下:"); for (Constructor c : ctors) { System.out.println(c); } // 擷取該Class對象所對應類的全部public構造器 Constructor[] publicCtors = clazz.getConstructors(); System.out.println("ClassTest的全部public構造器如下:"); for (Constructor c : publicCtors) { System.out.println(c); } // 擷取該Class對象所對應類自身定義的全部public方法 Method[] mtds = clazz.getMethods(); System.out.println("ClassTest定義的全部方法如下:"); for (Method md : mtds) { System.out.println(md); } // 擷取該Class對象所對應類的指定方法 System.out.println("ClassTest裡帶一個字串參數的info()方法為:" + clazz.getMethod("info", String.class)); // 通過getDeclaringClass()訪問該類所在的外部類 System.out.println("ClassTest的包為:" + clazz.getPackage()); System.out.println("ClassTest的父類為:" + clazz.getSuperclass()); }}