java反射機(jī)制與類加載機(jī)制
Class (Java SE 9 & JDK 9 ) - https://docs.oracle.com/javase/9/docs/api/java/lang/Class.html
Field (Java Platform SE 7 ) - https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Field.html
ByteArrayOutputStream (Java SE 9 & JDK 9 ) - https://docs.oracle.com/javase/9/docs/api/java/io/ByteArrayOutputStream.html
URL (Java SE 9 & JDK 9 ) - https://docs.oracle.com/javase/9/docs/api/java/net/URL.html
Class熱替換與卸載 - http://www.importnew.com/22462.html
深入探討 Java 類加載器 - https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html
反射機(jī)制(Reflection) – http://blog.qiji.tech/archives/4374
深入理解Java類型信息(Class對象)與反射機(jī)制 - https://blog.csdn.net/javazejian/article/details/70768369
深入理解Java類加載器(一):Java類加載原理解析 - https://blog.csdn.net/justloveyou_/article/details/72217806
認(rèn)識Class對象之前,先來了解一個(gè)概念,RTTI(Run-Time Type Identification)運(yùn)行時(shí)類型識別,對于這個(gè)詞一直是 C++ 中的概念,至于Java中出現(xiàn)RRTI的說法則是源于《Thinking in Java》一書,其作用是在運(yùn)行時(shí)識別一個(gè)對象的類型和類的信息,這里分兩種:傳統(tǒng)的 RTTI,它假定我們在編譯期已知道了所有類型,在沒有反射機(jī)制創(chuàng)建和使用類對象時(shí),一般都是編譯期已確定其類型,如new對象時(shí)該類必須已定義好。另外一種是反射機(jī)制,它允許我們在運(yùn)行時(shí)發(fā)現(xiàn)和使用類型的信息。在Java中用來表示運(yùn)行時(shí)類型信息的對應(yīng)類就是Class類,Class類也是一個(gè)實(shí)實(shí)在在的類,存在于JDK的java.lang包中。
Class類也是類的一種,但是特別地 Class 類用來描述使用 class 關(guān)鍵字定義的類,這些描述信息用于在運(yùn)行時(shí)獲取類定義內(nèi)容,Class類的對象作用是運(yùn)行時(shí)提供或獲得某個(gè)對象的類型信息。編寫好的類在編譯后都會產(chǎn)生一個(gè)Class對象,表示的是創(chuàng)建的類的類型信息,而且這個(gè)Class對象保存在同名.class的字節(jié)碼文件中。比如創(chuàng)建一個(gè)Shapes類,編譯Shapes類后就會創(chuàng)建其包含Shapes類相關(guān)類型信息的Class對象,并保存在Shapes.class字節(jié)碼文件中。每個(gè)通過關(guān)鍵字class標(biāo)識的類,在內(nèi)存中有且只有一個(gè)與之對應(yīng)的Class對象來描述其類型信息,無論創(chuàng)建多少個(gè)實(shí)例對象,其依據(jù)的都是用一個(gè)Class對象。Class類只存私有構(gòu)造函數(shù),因此對應(yīng)Class對象只能有JVM創(chuàng)建和加載。
在運(yùn)行期間,如果我們要產(chǎn)生某個(gè)類的對象,Java虛擬機(jī)(JVM)會檢查該類型的Class對象是否已被加載。如果沒有被加載,JVM會根據(jù)類的名稱找到.class文件并加載它。一旦某個(gè)類型的Class對象已被加載到內(nèi)存,就可以用它來產(chǎn)生該類型的所有對象。
通過反射方法實(shí)例化對象及修改私有成員為公有成員
import java.lang.reflect.*;
class Gum {
private int weight = 0;
static { System.out.println("Loading Gum"); }
public Gum(){ }
public Gum(int i){ weight = i; }
public String greeting(){ return "Hi!"; }
}
public class coding {
public static void print(Object obj) {
System.out.println(obj);
}
@SuppressWarnings("unchecked")
public static void main(String[] args) {
try {
Class clz = Class.forName("Gum");
// Class clazz = Gum.class;
// Constructor[] cts = clz.getConstructors();
// for ( Constructor c:cts ) {
// print("Contructor: "+c.getName?());
// }
// Constructor constructor = clz.getConstructor();
// Object object = clz.newInstance();
Constructor constructor = clz.getConstructor(int.class);
Object object = constructor.newInstance(0);
Method method = clz.getMethod("greeting");
String msg = (String)method.invoke(object);
print(msg);
Field field = clz.getDeclaredField("weight");
field.setAccessible(true);
print("get private member:"+field.get(object));
Class<?> class1 = Class.forName("Gum");
String name = class1.getClassLoader().getClass().getName();
print("class loader is " + name);
} catch(Exception e) {
e.printStackTrace();
}
}
}
使用類反射方法 Class.forName() 載入類,返回值是對應(yīng)類的Class對象,也可以通過對象實(shí)例的 getClass() 方法獲取,這是基礎(chǔ)對象類Object定義的方法,字面常量的方式獲取Class對象如 Object.class。如果沒有定義相應(yīng)的類構(gòu)造函數(shù),getConstructor() 會引發(fā) NoSuchMethod 異常。
Field.setAccessible(true) 并不是將方法的訪問權(quán)限改成了public,而是取消java的權(quán)限控制檢查。即使是public成員,其accessible 屬性默認(rèn)也是false,即需要進(jìn)行權(quán)限檢查。
與Java反射相關(guān)的類如下:
Class類 代表類的實(shí)體,在運(yùn)行的Java應(yīng)用程序中表示類和接口
Field類 代表類的成員變量(成員變量也稱為類的屬性)
Method類 代表類的方法
Constructor類 代表類的構(gòu)造方法
類加載器
JVM預(yù)定義幾種類加載器,當(dāng)JVM啟動的時(shí)候,Java缺省開始使用如下三種類型的類加載器:
啟動(Bootstrap)類加載器:引導(dǎo)類加載器是用 本地代碼實(shí)現(xiàn)的類加載器,它負(fù)責(zé)將 <JAVA_HOME>/lib下面的核心類庫 或 -Xbootclasspath選項(xiàng)指定的jar包等 虛擬機(jī)識別的類庫 加載到內(nèi)存中。由于引導(dǎo)類加載器涉及到虛擬機(jī)本地實(shí)現(xiàn)細(xì)節(jié),開發(fā)者無法直接獲取到啟動類加載器的引用,所以 不允許直接通過引用進(jìn)行操作。
擴(kuò)展(Extension)類加載器:擴(kuò)展類加載器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實(shí)現(xiàn)的,它負(fù)責(zé)將 <JAVA_HOME >/lib/ext或者由系統(tǒng)變量-Djava.ext.dir指定位置中的類庫 加載到內(nèi)存中。開發(fā)者可以直接使用標(biāo)準(zhǔn)擴(kuò)展類加載器。
系統(tǒng)(System)類加載器:系統(tǒng)類加載器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實(shí)現(xiàn)的,它負(fù)責(zé)將 用戶類路徑(java -classpath或-Djava.class.path變量所指的目錄,即當(dāng)前類所在路徑及其引用的第三方類庫的路徑,如第四節(jié)中的問題6所述)下的類庫 加載到內(nèi)存中。開發(fā)者可以直接使用系統(tǒng)類加載器,是最常用的加載器,同時(shí)也是java中默認(rèn)的加載器。通過Java反射機(jī)制 Class.getClassLoader() 可以得到類加載器信息。
除了以上列舉的三種類加載器,還有一種比較特殊的類型就是線程上下文類加載器(context class loader),從 JDK 1.2 開始引入的。Java.lang.Thread 類中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用來獲取和設(shè)置線程的上下文類加載器。如果沒有通過 setContextClassLoader(ClassLoader cl)方法進(jìn)行設(shè)置的話,線程將繼承其父線程的上下文類加載器。Java 應(yīng)用運(yùn)行的初始線程的上下文類加載器是系統(tǒng)類加載器。在線程中運(yùn)行的代碼可以通過此類加載器來加載類和資源。使用線程上下文類加載器,可以在執(zhí)行線程中拋棄雙親委派加載鏈模式,使用線程上下文里的類加載器加載類。典型的例子有:通過線程上下文來加載第三方庫jndi實(shí)現(xiàn),而不依賴于雙親委派。大部分Java application服務(wù)器(jboss, tomcat..)也是采用contextClassLoader來處理web服務(wù)。還有一些采用hot swap特性的框架,也使用了線程上下文類加載器,比如 seasar (full stack framework in japenese)。
JVM在加載類時(shí)默認(rèn)采用的是雙親委派機(jī)制。通俗的講,就是某個(gè)特定的類加載器在接到加載類的請求時(shí),首先將加載任務(wù)委托給父類加載器,依次遞歸 (本質(zhì)上就是loadClass函數(shù)的遞歸調(diào)用)。因此,所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中。如果父類加載器可以完成這個(gè)類加載請求,就成功返回;只有當(dāng)父類加載器無法完成此加載請求時(shí),子加載器才會嘗試自己去加載。事實(shí)上,大多數(shù)情況下,越基礎(chǔ)的類由越上層的加載器進(jìn)行加載,因?yàn)檫@些基礎(chǔ)類之所以稱為“基礎(chǔ)”,是因?yàn)樗鼈兛偸亲鳛楸挥脩舸a調(diào)用的API(當(dāng)然,也存在基礎(chǔ)類回調(diào)用戶用戶代碼的情形)。 系統(tǒng)類加載器的父類加載器是標(biāo)準(zhǔn)擴(kuò)展類加載器,標(biāo)準(zhǔn)擴(kuò)展類加載器的父類加載器是啟動類加載器。
虛擬機(jī)出于安全等因素考慮,不會加載<JAVA_HOME>/lib目錄下存在的陌生類,換句話說,虛擬機(jī)只加載<JAVA_HOME>/lib目錄下它可以識別的類。因此,開發(fā)者通過將要加載的非JDK自身的類放置到此目錄下期待啟動類加載器加載是不可能的。
在絕大多數(shù)情況下,系統(tǒng)默認(rèn)提供的類加載器實(shí)現(xiàn)已經(jīng)可以滿足需求。但是在某些情況下,您還是需要為應(yīng)用開發(fā)出自己的類加載器。比如您的應(yīng)用通過網(wǎng)絡(luò)來傳輸Java類的字節(jié)代碼,為了保證安全性,這些字節(jié)代碼經(jīng)過了加密處理。這個(gè)時(shí)候您就需要自己的類加載器來從某個(gè)網(wǎng)絡(luò)地址上讀取加密后的字節(jié)代碼,接著進(jìn)行解密和驗(yàn)證,最后定義出要在Java虛擬機(jī)中運(yùn)行的類來。下面將通過兩個(gè)具體的實(shí)例來說明類加載器的開發(fā)。通過用戶自定義的類裝載器,你的程序可以加載在編譯時(shí)并不知道或者尚未存在的類或者接口,并動態(tài)連接它們并進(jìn)行有選擇的解析。
除了和本地實(shí)現(xiàn)密切相關(guān)的啟動類加載器之外,包括標(biāo)準(zhǔn)擴(kuò)展類加載器和系統(tǒng)類加載器在內(nèi)的所有其他類加載器我們都可以當(dāng)做自定義類加載器來對待,唯一區(qū)別是是否被虛擬機(jī)默認(rèn)使用。前面的內(nèi)容中已經(jīng)對java.lang.ClassLoader抽象類中的幾個(gè)重要的方法做了介紹,這里就簡要敘述一下一般 用戶自定義類加載器的工作流程(可以結(jié)合后面問題解答一起看):
1、首先檢查請求的類型是否已經(jīng)被這個(gè)類裝載器裝載到命名空間中了,如果已經(jīng)裝載,直接返回;否則轉(zhuǎn)入步驟2;
2、委派類加載請求給父類加載器(更準(zhǔn)確的說應(yīng)該是雙親類加載器,真實(shí)虛擬機(jī)中各種類加載器最終會呈現(xiàn)樹狀結(jié)構(gòu)),如果父類加載器能夠完成,則返回父類加載器加載的Class實(shí)例;否則轉(zhuǎn)入步驟3;
3、調(diào)用本類加載器的 findClass() 方法,試圖獲取對應(yīng)的字節(jié)碼。如果獲取的到,則調(diào)用 defineClass() 導(dǎo)入類型到方法區(qū);如果獲取不到對應(yīng)的字節(jié)碼或者其他原因失敗,向上拋出異常。
在Java中,一個(gè)類用其完全匹配類名(fully qualified class name)作為標(biāo)識,這里指的完全匹配類名包括包名和類名。但在JVM中,一個(gè)類用其全名和一個(gè)ClassLoader的實(shí)例 作為唯一標(biāo)識,不同類加載器加載的類將被置于不同的命名空間。
在java.net包下有一個(gè) URLClassLoader 類提供了運(yùn)程加載類的功能,它讓我們可以通過以下幾種方式進(jìn)行加載:
* 文件: (從文件系統(tǒng)目錄加載)
* jar包: (從Jar包進(jìn)行加載)
* Http: (從遠(yuǎn)程的Http服務(wù)進(jìn)行加載)
一個(gè)class被一個(gè)ClassLoader實(shí)例加載過的話,就不能再被這個(gè)ClassLoader實(shí)例再次加載,即加載類時(shí)調(diào)用了defileClass()方法,重新加載字節(jié)碼、解析、驗(yàn)證后就不能重復(fù)執(zhí)行。系統(tǒng)默認(rèn)的AppClassLoader加載器內(nèi)部會緩存加載過的class,重新加載的話,就直接取緩存。所以需要熱加載只能重新創(chuàng)建一個(gè)ClassLoader,然后再去加載已經(jīng)被加載過的class文件。實(shí)現(xiàn)熱加載需要重載 ClassLoader 的 loadClass() 方法,跳過內(nèi)部的雙親委托機(jī)制。即使不采用雙親委托機(jī)制,比如java.lang包中的相關(guān)類還是不能自定義一個(gè)同名的類來代替,主要因?yàn)镴VM解析、驗(yàn)證class的 時(shí)候,會進(jìn)行相關(guān)判斷。強(qiáng)制重復(fù)加載引發(fā) java.lang.LinkageError 異常:
Exception in thread "main" java.lang.LinkageError: loader (instance of NetworkClassLoader): attempted duplicate class definition
JVM中class和Meta信息存放在PermGen space區(qū)域。如果加載的class文件很多,那么可能導(dǎo)致PermGen space區(qū)域空間溢出。引起:java.lang.OutOfMemoryErrorPermGen space. 對于有些Class我們可能只需要使用一次,就不再需要了,JVM中的Class只有滿足以下三個(gè)條件,才能被GC回收,也就是該Class被卸載(unload):
該類所有的實(shí)例都已經(jīng)被GC。
加載該類的ClassLoader實(shí)例已經(jīng)被GC。
該類的java.lang.Class對象沒有在任何地方被引用。
System.gc()執(zhí)行后的Class的卸載是不可控的。
自定義類加載器--遠(yuǎn)程類加載器
反射機(jī)制的應(yīng)用必須要求該類是public訪問權(quán)限的類,這樣才能由加載器加載。要實(shí)現(xiàn)HotSwap熱加載,只需要?jiǎng)?chuàng)建新實(shí)例去加載目標(biāo)類就可以,熱加載邏輯可以根據(jù)文件更新時(shí)間來做判斷。
import java.lang.reflect.*;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.net.URL;
import java.net.HttpURLConnection;
/**
* NetworkClassLoader ncl = new NetworkClassLoader("http://xxx.com");
* String basicClassName = "Gum";
* Class<?> clazz = ncl.loadClass(basicClassName);
* Gum oo = (Gum)clazz.newInstance();
*/
class NetworkClassLoader extends ClassLoader {
private String rootUrl;
public NetworkClassLoader(String rootUrl) {
this.rootUrl = rootUrl;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = rootUrl + "/" + className.replace('.', '/') + ".class";
try {
URL url = new URL(path);
// HttpURLConnection con = (HttpURLConnection)url.openConnection();
// System.out.println(con.getResponseCode?() +" "+ con.getContentType());
// BufferedReader buffer = new BufferedReader(new InputStreamReader(ins));
// StringBuffer bs = new StringBuffer();
// String line = null;
// while((line=buffer.readLine())!=null){
// bs.append(line);
// }
// System.out.println("getClassData:"+bs.length()+":"+bs.substring(0));
// return bs.toString().getBytes();
InputStream ins = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int len = 0;
while ((len = ins.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
System.out.println("getClassData:"+len+":");
return baos.toByteArray();
} catch (Exception e) {
System.out.println("getClassData exception:");
e.printStackTrace();
}
System.out.println("getClassData null:");
return null;
}
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class c = findLoadedClass(name);
if( name.startsWith("java.") ){
ClassLoader system = ClassLoader.getSystemClassLoader();
c = system.loadClass(name);
}
if (null==c) c = findClass(name);
if( resolve ) resolveClass(c);
return c;
}
}