歡迎你們關注 github.com/hsfxuebao/j… ,但願對你們有所幫助,要是以爲能夠的話麻煩給點一下Star哈html
轉自www.cnblogs.com/xjwhaha/p/1…java
類加載器是 JVM 執行類加載機制的前提git
ClassLoader 的做用:github
ClassLoader 是 Java 的核心組件,全部的 Class 都是由 ClassLoader 進行加載的,ClassLoader 負責經過各類方式將 Class 信息的二進制數據流讀入 JVM 內部,轉換爲一個與目標類對應的 java.lang.Class 對象實例。而後交給 Java 虛擬機進行連接、初始化等操做。所以,ClassLoader 在整個裝載階段,只能影響到類的加載,而沒法經過 ClassLoader 去改變類的連接和初始化行爲。至於它是否能夠運行,則由 Execution Engine (執行引擎)決定sql
類加載:shell
類加載器最先出如今 Java 1.0 版本中,那個時候只是單純地爲了知足 Java Applet 應用而被研發出來,但現在類加載器卻在 OSGI、字節碼加解密領域大放 異彩。這主要歸功於 Java 虛擬機的設計者們當初在設計類加載器的時候,並沒 有考慮將它綁定在 JVM 內部,這樣作的好處就是可以更加靈活和動態地執行類 加載操做數據庫
類加載的分類bootstrap
類的加載分類:數組
顯式加載 vs 隱式加載緩存
Class 文件的顯式加載與隱式加載的方式是指 JVM 加載 Class 文件到內存 的方式
在平常開發中以上兩種方式通常會混合使用
類加載器的必要性
通常狀況下,Java 開發人員並不須要在程序中顯式地使用類加載器,可是了 解類加載器的加載機制卻顯得相當重要。從如下幾個方面說:
java.lang.ClassNotFoundException
異 常 或 java.lang.NoClassDeFoundError
異常時手足無措。只有瞭解類加載器的加載機制纔可以在出現異常的時候快速地根據錯誤異常日誌定位問題和解決問 題命名空間
何爲類的惟一性?
對於任意一個類,都須要由加載它的類加載器和這個類自己一同確認其在 Java 虛擬機中的惟一性。每個類加載器,都擁有一個獨立的類名稱空間:比較兩個類是否相等,只有在這兩個類是由同一個類加載器加載的前提下才有意義。 不然,即便這兩個類源自同一個 Class 文件,被同一個虛擬機加載,只要加載他們的類加載器不一樣,那這兩個類就一定不相等
命名空間
在大型應用中,咱們每每藉助這一特性,來運行同一個類的不一樣版本
代碼示例: 兩個不一樣的類加載器示例,獲取同一個類的 Class對象, 是不同的兩個
public class User {
private int id;
@Override
public String toString() {
return "User{" +
"id=" + id +
'}';
}
}
//自定義類加載器
public class UserClassLoader extends ClassLoader {
private String rootDir;
public UserClassLoader(String rootDir) {
this.rootDir = rootDir;
}
/**
* 編寫findClass方法的邏輯
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 獲取類的class文件字節數組
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//直接生成class對象
return defineClass(name, classData, 0, classData.length);
}
}
/**
* 編寫獲取class文件並轉換爲字節碼流的邏輯 * @param className * @return
*/
private byte[] getClassData(String className) {
// 讀取類文件的字節
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
// 讀取類文件的字節碼
while ((len = ins.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 類文件的徹底路徑
*/
private String classNameToPath(String className) {
return rootDir + "\\" + className.replace('.', '\\') + ".class";
}
public static void main(String[] args) {
String rootDir = "D:\\dev\\workspace\\demo\\代碼\\JVMDemo1\\chapter04\\src\\";
try {
//建立自定義的類的加載器1
UserClassLoader loader1 = new UserClassLoader(rootDir);
Class clazz1 = loader1.findClass("com.atguigu.java.User");
//建立自定義的類的加載器2
UserClassLoader loader2 = new UserClassLoader(rootDir);
Class clazz2 = loader2.findClass("com.atguigu.java.User");
System.out.println(clazz1 == clazz2); //clazz1與clazz2對應了不一樣的類模板結構。
System.out.println(clazz1.getClassLoader());
System.out.println(clazz2.getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
複製代碼
類加載機制的基本特徵
一般類加載機制有三個基本特徵:
JVM 支持兩種類型的類加載器,分別爲引導類加載器(Bootstrap ClassLoader) 和自定義類加載器(User-Defined ClassLoader)
從概念上來說,自定義類加載器通常指的是程序中由開發人員自定義的一類 類加載器,可是 Java 虛擬機規範卻沒有這麼定義,而是將全部派生於抽象類 ClassLoader 的類加載器都劃分爲自定義類加載器。不管類加載器的類型如何劃 分,在程序中咱們最多見的類加載器結構主要是以下狀況:
簡要代碼:
class ClassLoader {
ClassLoader parent; //父類加載器
public ClassLoader(ClassLoader parent) {
this.parent = parent;
}
}
class ParentClassLoader extends ClassLoader {
public ParentClassLoader(ClassLoader parent) {
super(parent);
}
}
class ChildClassLoader extends ClassLoader {
public ChildClassLoader(ClassLoader parent) {
//parent = new ParentClassLoader();
super(parent);
}
}
複製代碼
啓動類加載器(引導類加載器)
可使用-XX:+TraceClassLoading
參數,打印類的加載過程
擴展類加載器(Extension ClassLoader)
應用程序類加載器(系統類加載器,AppClassLoader)
每一個 Class 對象都會包含一個定義它的 ClassLoader 的一個引用 獲取 ClassLoader 的途徑
各個加載器的獲取方式:
//獲取系統該類加載器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//獲取擴展類加載器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
//試圖獲取引導類加載器:失敗
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
複製代碼
說明:
站在程序的角度看,引導類加載器與另外兩種類加載器(系統類加載器和擴展 類加載器)並非同一個層次意義上的加載器,引導類加載器是使用 C++ 語言 編寫而成的,而另外兩種類加載器則是使用 Java 語言編寫的。因爲引導類加載 器壓根兒就不是一個 Java 類,所以在 Java 程序中只能打印出空值
數組類的加載器:
public class ClassLoaderTest1 {
public static void main(String[] args) {
//關於數組類型的加載:使用的類的加載器與數組元素的類的加載器相同
String[] arrStr = new String[10];
System.out.println(arrStr.getClass().getClassLoader());//null:表示使用的是引導類加載器
ClassLoaderTest1[] arr1 = new ClassLoaderTest1[10];
System.out.println(arr1.getClass().getClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2
int[] arr2 = new int[10];
System.out.println(arr2.getClass().getClassLoader());//null:不須要類的加載器
System.out.println(Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
複製代碼
說明:
數組類的 Class 對象,不是由類加載器去建立的,而是在 Java 運行期 JVM 根據須要自動建立的。對於數組類的類加載器來講,是經過 Class.geetClassLoader() 返回的,與數組當中元素類型的類加載器是同樣的:如 果數組當中的元素類型是基本數據類型,數組類是沒有類加載器的,由於基本類型不須要加載器加載
ClassLoader 與現有類加載的關係:
除了以上虛擬機自帶的加載器外,用戶還能夠定製本身的類加載器。Java 提 供了抽象類 java.lang.ClassLoader,全部用戶自定義的類加載器都應該繼承 ClassLoader 類
ClassLoader的主要方法
抽象類 ClassLoader 的主要方法:(內部沒有抽象方法)
public final ClassLoader getParent()
返回該類加載器的"父類"加載器
public Class loadClass(String name) throws ClassNotFoundException
加載全類名稱爲 name 的類,返回結果爲 java.lang.Class 類的實例。若是找不到 類,則返回 ClassNotFountException 異常。該方法中的邏輯就是雙親委派模式的 實現
源碼: 體現出雙親委派機制
//參數 var2 表明是否須要解析,默認false
protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
//同步加鎖,保證只加載一次
synchronized(this.getClassLoadingLock(var1)) {
//首先,在緩存中判斷是否已經加載同名類
Class var4 = this.findLoadedClass(var1);
//不爲空直接返回
if (var4 == null) {
long var5 = System.nanoTime();
try {
//若是父類加載器不爲空,則調用父類的加載,遞歸,若是返回空,則繼續執行
if (this.parent != null) {
var4 = this.parent.loadClass(var1, false);
} else {
//若是父類的加載器爲空,則表明爲啓動類加載器,使用啓動類加載器加載
var4 = this.findBootstrapClassOrNull(var1);
}
} catch (ClassNotFoundException var10) {
;
}
//若是沒能加載此類, 則 說明啓動類加載器沒能加載,由其上一層加載器加載,若是仍然返回空
//則繼續向上,
if (var4 == null) {
long var7 = System.nanoTime();
//若是不是啓動類加載器加載類,最終是使用findClass方法加載類
var4 = this.findClass(var1);
PerfCounter.getParentDelegationTime().addTime(var7 - var5);
PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
PerfCounter.getFindClasses().increment();
}
}
if (var2) {
this.resolveClass(var4);
}
return var4;
}
}
複製代碼
protected Class findClass(String name) throws ClassNotFoundException
protected final Class defineClass(String name, byte[] b, int off, int len)
根據給定的字節數組 b 轉換爲 Class 的實例,off 和 len 參數表示實際 Class 信息在 byte 數組中的位置和長度,其中 byte 數組 b 是 ClassLoader 從 外部獲取的。這是受保護的方法,只有在自定義 ClassLoader 子類中可使用
defineClass() 方法是用來將 byte 字節流解析成 JVM 可以識別的 Class 對 象(ClassLoader 中已實現該方法邏輯),經過這個方法不只可以經過 Class 文件 實例化 Class 對象,也能夠經過其它方式實例化 Class 對象,如經過網絡中接 收一個類的字節碼,而後轉換爲 byte 字節流建立對應的 Class 對象
//findClass 和 defineClass 方法的關係
protected Class<?> findClass(String name) throws ClassNotFoundException {
//獲取類的字節數組
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//使用 defineClass 生成 Class 對象
return defineClass(name, classData, 0, classData.length);
}
}
複製代碼
protected final void resolveClass(Class c)
連接指定的一個 Java 類。使用該方法可使用類的 Class 對象建立完成的 同時也被解析。前面咱們說連接階段主要是對字節碼進行驗證,爲類變量分配內 存並設置初始值同時將字節碼文件中的符號引用轉換爲直接引用
protected final Class findLoadedClass(String name)
查找名稱爲 name 的已經被加載過的類,返回結果爲 java.lang.Class 類的實 例。這個方法是 final 方法,沒法被修改
private final ClassLoader parent;
它也是一個 ClassLoader 的實例,這個字段所表示的 ClassLoader 也稱爲這個 ClassLoader 的雙親。在類加載的過程當中,ClassLoader 可能會將某些請求交予本身的雙親處理
加載器的繼承關係
根據 加載器的 繼承關係圖能夠看到
擴展類加載器和系統類加載器,都是繼承的URLClassLoader, -> SecureClassLoader
SecureClassLoader 與 URLClassLoader
SecureClassLoader 擴展了 ClassLoader,新增了幾個與使用相關的代碼源(對代碼源的位置及其證書的驗證)和權限定義類驗證(主要針對 Class 源碼的 訪問權限)的方法,通常咱們不會直接跟這個類打交道,更多的是與它的子類 URLClassLoader 有所關聯
前面說過,ClassLoader 是一個抽象類,不少方法是空的沒有實現,好比 findClass()、findResource() 等。而 URLClassLoader 這個實現類爲這些方法提供 了具體的實現。並新增了 URLClassPath 類協助取得 Class 字節碼流等功能。 在編寫自定義類加載器時,若是沒有太過於複雜的需求,能夠直接繼承 URLClassLoader 類,這樣就能夠避免本身去編寫 findClass() 方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔
ExtClassLoader 與 AppClassLoader
ExtClassLoader 並無重寫 loadClass() 方法,這足以說明其遵循雙親委派模式,而 AppClassLoader 重載了 loadClass() 方法,但最終調用的仍是父類 loadClass() 方法,所以依然遵循雙親委派模式
Class.forName() 與 ClassLoader.loadClass()
類加載器用來把類加載到 Java 虛擬機中。從 JDK 1.2 版本開始,類的加載過程採用雙親委派機制,這種機制能更好地保證 Java 平臺的安全
定義:
若是一個類加載器在接到加載類的請求時,它首先不會本身嘗試去加載這個 類,而是把這個請求任務委託給父類加載器去完成,依次遞歸,若是父類加載器 能夠完成類加載任務,就成功返回。只有父類加載器沒法完成此加載任務時,才 本身去加載
本質:
規定了類加載的順序是:引導類加載器先加載,若加載不到,由擴展類加載器加載,若還加載不到,纔會由系統類加載器或自定義的類加載器進行加載
雙親委派機制簡圖和流程圖以下:
雙親委派機制優點 :
代碼支持:
雙親委派機制在 java.lang.ClassLoader.loadClass(String, boolean) 接口中體現。 該接口的邏輯以下:
雙親委派的模型就隱藏在第 2 和第 3 步中
舉例:
假設當前加載的是 java.lang.Object 這個類,很顯然,該類屬於 JDK 中核 心的不能再核心的一個類,所以必定只能由引導類加載器進行加載。當 JVM 準 備加載 java.lang.Object 時,JVM 默認會使用系統類加載器去加載,按照上面 5 步加載的邏輯,在第 1 步從系統類的緩存中確定查找不到該類,因而進入第 2 步。因爲從系統類加載器的父類加載器是擴展類加載器,因而擴展類加載器繼續 從第 1 步開始重複。因爲擴展類加載器的緩存中也必定查找不到該類,所以進入 第 2 步。擴展類的父加載器是 null,所以系統調用 findClass(String),最終經過 引導類加載器進行加載
思考
若是在自定義的類加載器中重寫 java.lang.ClassLoader.loadClass(String) 或 java.lang.ClassLoader.loadClass(String, boolean) 方法,抹去其中的雙親委派機制, 僅保留上面這 4 步中的第 1 步和第 4 步,那麼是否是就可以加載核心類庫了呢?
這也不行!由於 JDK 還爲核心類庫提供了一層保護機制。無論是自定義的 類 加 載 器 , 還 是 系 統 類 加 載 器 抑 或 擴 展 類 加 載 器 , 最 終 都 必 須 調 用 java.lang.ClassLoader.defineClass(String, byte[], int, int, ProtectionDomain)
方法, 而該方法會執行 preDefineClass()
接口,該接口中提供了對 JDK 核心類庫的保護
雙親委派模式的弊端
檢查類是否加載的委派過程是單向的,這個方式雖然從結構上說比較清晰, 使各個 ClassLoader 的職責很是明確,可是同時會帶來一個問題,即頂層的 ClassLoader 沒法訪問底層的 ClassLoader 所加載的類
一般狀況下,啓動類加載器中的類爲系統核心類,包括一些重要的系統接口, 而在應用類加載器中,爲應用類。按照這種模式,**應用類訪問系統類天然是沒 有問題,可是系統類訪問應用類就會出現問題。**好比在系統類中提供了一個接 口,該接口須要在應用類中得以實現,該接口還綁定一個工廠方法,用於建立該 接口的實例,而接口和工廠方法都在啓動類加載器中。這時,就會出現該工廠方 法沒法建立由應用類加載器加載的應用實例的問題
結論
因爲 Java 虛擬機規範並無明確要求類加載器的加載機制必定要使用雙親委派模型,只是建議採用這種方式而已。
好比 Tomcat 中,類加載器所採用 的加載機制就和傳統的雙親委派模型有必定區別,當缺省的類加載器接收到一個 類的加載任務時,首先會由它自行加載,當它加載失敗時,纔會將類的加載任務 委派給它的超類加載器去執行,
這同時也是 Servlet 規範推薦的一種作法
雙親委派模型並非一個具備強制性約束的模型,而是 Java 設計者推薦給 開發者們的類加載器實現方式
在 Java 的世界中大部分的類加載器都遵循這個模型,但也有例外狀況,直到 Java 模塊化出現爲止,雙親委派模型主要出現過 3 次較大規模"被破壞"的狀況
第一次破壞雙親委派機制: JDK1.2以前自己就沒有實現雙親委派機制
雙親委派模型的第一次"被破壞"其實發生在雙親委派模型出現以前——即 JDK 1.2 面世之前的"遠古"時代
因爲雙親委派模型在 JDK 1.2 以後才被引入,可是類加載器的概念和抽象類 java.lang.ClassLoader 則在 Java 的第一個版本中就已經存在,面對已經存在的 用戶自定義類加載器的代碼,Java 設計者們引入雙親委派模型時不得不作出一 些妥協,爲了兼容這些已有的代碼,沒法再以技術手段避免 loadClass() 被子類 覆蓋的可能性,只能在 JDK 1.2 以後的 java.lang.ClassLoader 中添加一個新的 protected 方法 findClass(),並引導用戶編寫的類加載邏輯時儘量去重寫這個 方法,而不是在 loadClass() 中編寫代碼。上節咱們已經分析過 loadClass() 方法, 雙親委派的具體邏輯就實如今這裏面,按照 loadClass() 方法的邏輯,若是父類 加載失敗,會自動調用本身的 findClass() 方法來完成加載,這樣既不影響用戶 按照本身的意願去加載類,又能夠保證新寫出來的類加載器是符合雙親委派規則 的
**第二次破壞雙親委派機制:**線程上下文類加載器
雙親委派模型的第二次"被破壞"是由這個模型自身的缺陷致使的,雙親委派 很好地解決了各個類加載器協做時基礎類型的一致性問題(越基礎的類由越上層 的加載器進行加載),基礎類型之因此被稱爲"基礎",是由於它們老是做爲被用戶代碼繼承、調用的 API 存在,但程序設計每每沒有絕對不變的完美規則,若是有基礎類型又要調用回用戶代碼,那該怎麼辦?
這並不是是不可能出現的事情,一個典型的例子即是 JNDI 服務,JNDI 如今 已是 Java 的標準服務,它的代碼由啓動類加載器來完成加載(在 JDK 1.3 時 加入到 rt.jar),確定屬於 Java 中很基礎的類型了。但 JNDI 存在的目的就是對 資源進行查找和集中管理,它須要調用由其它廠商實現並部署在應用程序的 ClassPath 下的 JNDI 服務提供者接口(Service Provider Interface. SPI) 的代碼, 如今問題來了,啓動類加載器時絕對不可能認識、加載這些代碼的,那該怎麼辦? (SPI:在 Java 平臺中,一般把核心類 rt.jar 中提供外部服務、可由應用層自行實現的接口稱爲 SPI)
爲了解決這個困境,Java 的設計團隊只好引入了一個不太優雅的設計:**線程上下文類加載器(Thread Context ClassLoader). **這個類加載器能夠經過 java.lang.Thread 類的 setContextClassLoader() 方法進行設置,若是建立線程時 還未設置,它將會從父線程中繼承一個,若是在應用程序的全局範圍內都沒有設 置過的話,那這個類加載器默認就是應用程序類加載器
有了線程上下文類加載器,程序就能夠作一些"舞弊"的事情了。JNDI 服務 使用這個線程上下文類加載器去加載所需的 SPI 服務代碼。這是一種負累加載 器去請求子類加載器完成類加載的行爲,這種行爲其實是打通了雙親委派模型 的層次結構來逆向使用類加載器,已經違背了雙親委派模型的通常性原則,但也是迫不得已的事情。Java 中涉及 SPI 的加載基本上都採用這種方式來完成,例 如 JNDI、JDBC、JCE、JAXB 和 JBI 等。不過,當 SPI 的服務提供者多於一 個的時候,代碼就只能根據具體提供者的類型來硬編碼判斷,爲了消除這種極不 優 雅 的 方 式 , 在 JDK 6 時 , JDK 提 供 了 java.util.ServiceLoader 類 , 以 META-INF/Services 中的配置信息,輔以責任鏈模式,這纔算是給 SPI 的加載 提供了一種相對合理的解決方案
結構圖:
默認上下文加載器就是應用類加載器,這樣以上下文加載器爲中介,使得啓 動類加載器中的代碼也能夠訪問應用類加載器中的類
第三次破壞雙親委派機制: 模塊化
雙親委派模型的第三次"被破壞"是因爲用戶對程序動態性的追求而致使的。 如:代碼熱替換(Hot Swap)、**模塊熱部署(Hot Deployment)**等
IBM 公司主導的 JSR-291(即 OSGI R4.2)實現模塊化熱部署的關鍵是它自 定義的類加載器機制的實現,每一個程序模塊(OSGI 中稱爲 Bundle)都有一個本身 的類加載器,當須要更換一個 Bundle 時,就把 Bundle 連同類加載器一塊兒換掉 以實現代碼的熱替換。在 OSGI 環境下,類加載器再也不雙親委派模型推薦的樹 狀結構,而是進一步發展爲更加複雜的網狀結構 當收到類加載請求時,OSGI 將按照下面的順序進行類搜索:
說明:只有開頭兩點仍然符合雙親委派模型的原則,其他的類查找都是在平 級的類加載器中進行的
小結:
這裏,咱們使用了"被破壞"這個詞來形容上述不符合雙親委派模型原則的行 爲,但這裏"被破壞"並不必定是帶有貶義的。只要有明確的目的和充分的理由, 突破舊有原則無疑是一種創新
正如:OSGI 中的類加載器的設計不符合傳統的雙親委派的類加載器架構, 且業界對其爲了實現熱部署而帶來的額外的高複雜度還存在很多爭議,但對這方 面有瞭解的技術人員基本仍是能達成一個共識,認爲 OSGI 中對類加載器的運 用是值得學習的,徹底弄懂了 OSGI 的實現,就算是掌握了類加載器的精髓
熱替換是指在程序運行過程當中,不中止服務,只經過替換程序文件來修改程 序的行爲。熱替換的關鍵需求在於服務不能中斷,修改必須當即表現正在運行的 系統之中。基本上大部分腳本語言都是天生支持熱替換的,好比:PHP,只要替 換了 PHP 源文件,這種改動就會當即生效,而無需重啓 Web 服務器
但對 Java 來講,熱替換並不是天生就支持,若是一個類已經加載到系統中, 經過修改類文件,並沒有法讓系統再來加載並重定義這個類。
所以,在 Java 中實 現這一功能的一個可行的方法就是靈活運用 ClassLoader
**注意:由不一樣 ClassLoader 加載的同名類屬於不一樣的類型,不能相互轉換和兼容。即兩個不一樣的 ClassLoader 加載同一個類,在虛擬機內部,會認爲這 2 個類是徹底不一樣的 **
根據這個特色,能夠用來模擬熱替換的實現,基本思路以下圖所示:
代碼實現:
將下面這個類使用javac編譯爲class文件
public class Demo1 {
//原始的方法打印
public void hot() {
System.out.println("OldDemo1");
}
}
複製代碼
自定義的ClassLoader, 無需過多關注
public class MyClassLoader extends ClassLoader {
private String rootDir;
public MyClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String className) throws ClassNotFoundException {
Class clazz = this.findLoadedClass(className);
FileChannel fileChannel = null;
WritableByteChannel outChannel = null;
if (null == clazz) {
try {
String classFile = getClassFile(className);
FileInputStream fis = new FileInputStream(classFile);
fileChannel = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
outChannel = Channels.newChannel(baos);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
int i = fileChannel.read(buffer);
if (i == 0 || i == -1) {
break;
}
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
byte[] bytes = baos.toByteArray();
clazz = defineClass(className, bytes, 0, bytes.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileChannel != null)
fileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (outChannel != null)
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return clazz;
}
/**
* 類文件的徹底路徑
*/
private String getClassFile(String className) {
return rootDir + "\\" + className.replace('.', '\\') + ".class";
}
}
複製代碼
使用自定義類加載加載類,並使用
public class LoopRun {
public static void main(String args[]) {
//循環調用Demo1 類的方法,打印
while (true) {
try {
//1. 每次使用類的方法時,都建立自定義類加載器的實例,去加載
MyClassLoader loader = new MyClassLoader("D:\\code\\workspace_idea5\\JVMDemo1\\chapter04\\src\\");
//2. 加載指定的類,因爲每次都是不用的類加載器加載的,因此會重複加載爲新的類
Class clazz = loader.findClass("com.atguigu.java1.Demo1");
//3. 建立運行時類的實例
Object demo = clazz.newInstance();
//4. 獲取運行時類中指定的方法
Method m = clazz.getMethod("hot");
//5. 調用指定的方法
m.invoke(demo);
Thread.sleep(5000);
} catch (Exception e) {
System.out.println("not find");
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
}
複製代碼
在運行時修改Demo1類方法,並從新編譯,打印結果:OldDemo1
OldDemo1
OldDemo1
OldDemo1---> NewDemo1
OldDemo1---> NewDemo1
OldDemo1---> NewDemo1
OldDemo1---> NewDemo1
複製代碼
做用:
Java 安全模型的核心就是 Java 沙箱(Sandbox),什麼是沙箱?沙箱就是一個 限制程序運行的環境
沙箱機制就是將 Java 代碼**限定在虛擬機(JVM)特定的運行範圍中,而且 嚴格限制代碼對本地系統資源訪問。**經過這樣的措施來保證對代碼的有限隔離, 防止對本地系統形成破壞
沙箱主要限制系統資源訪問,那系統資源包括什麼?CPU、內存、文件系統、 網絡。不一樣級別的沙箱對這些資源訪問的限制也能夠不同 全部的 Java 程序運行均可以指定沙箱,能夠定製安全策略
JDK 1.0 時期
在 Java 中將執行程序分紅本地代碼和遠程代碼兩種,本地代碼默認視爲可 信任的,而遠程代碼則被看做是不受信的。對於授信的本地代碼,能夠訪問一切 本地資源。而對於非授信的遠程代碼在早期的 Java 實現中,安全依賴於沙箱 (Sandbox)機制。以下圖所示 JDK 1.0 安全模型
JDK 1.1 時期
JDK 1.0 中如此嚴格的安全機制也給程序的功能擴展帶來障礙,好比當用戶 但願遠程代碼訪問本地系統的文件時候,就沒法實現
所以在後續的 JDK 1.1 版本中,針對安全機制作了改進,增長了安全策略。 容許用戶指定代碼對本地資源的訪問權限
以下圖所示 JDK 1.1 安全模型
JDK 1.2 時期
在 JDK 1.2 版本中,再次改進了安全機制,增長了代碼簽名。不論本地代碼 或是遠程代碼,都會按照用戶的安全策略設定,由類加載器加載到虛擬機中權限 不一樣的運行空間,來實現差別化的代碼執行權限控制。以下圖所示 JDK 1.2 安 全模型:
JDK 1.6 時期
當前最新的安全機制實現,則引入了**域(Domain)**的概念
虛擬機會把全部代碼加載到不一樣的系統域和應用域。系統域部分專門負責與 關鍵資源進行交互,而各個應用域部分則經過系統域的部分代理來對各類須要的 資源進行訪問。虛擬機中不一樣的受保護域(Protected Domain),對應不同的權限 (Permission)。存在於不一樣域中的類文件就具備了當前域的所有權限,以下圖所 示,最新的安全模型(JDK 1.6)
爲何要自定義類加載器?
隔離加載類
在某些框架內進行中間件與應用的模塊隔離,把類加載到不一樣的環境。好比: 阿里內某容器框架經過自定義類加載器確保應用中依賴的 jar 包不會影響到中 間件運行時使用的 jar 包。再好比:Tomcat 這類 Web 應用服務器,內部自定 義了好幾種類加載器,用於隔離同一個 Web 應用服務器上的不一樣應用程序。(類 的仲裁 --> 類衝突)
修改類加載的方式
類的加載模型並不是強制,除 Bootstrap 外,其餘的加載並不是必定要引入,或 者根據實際狀況在某個時間點按需進行動態加載
擴展加載源
好比從數據庫、網絡、甚至是電視機機頂盒進行加載
Java 代碼容易被編譯和篡改,能夠進行編譯加密。那麼類加載也須要自定義, 還原加密的字節碼
常見的場景
實現相似進程內隔離,類加載器實際上用做不一樣的命名空間,以提供相似容 器、模塊化的效果。例如,兩個模塊依賴於某個類庫的不一樣版本,若是分別被不 同的容器加載,就能夠互不干擾。這個方面的集大成者是 Java EE 和 OSGI、 JPMS 等框架
應用須要從不一樣的數據源獲取類定義信息,例如網絡數據源,而不是本地文 件系統。或者是須要本身操縱字節碼,動態修改或者生成類型
注意:
在通常狀況下,使用不一樣的類加載器去加載不一樣的功能模塊,會提升應用程 序的安全性。可是,若是涉及 Java 類型轉換,則加載器反而容易產生不美好的 事情。在作 Java 類型轉換時,只有兩個類型都是由同一個加載器所加載,才能進行類型轉換,不然轉換時會發生異常
代碼實現
用戶經過定製本身的類加載器,這樣能夠從新定義類的加載規則,以便實現 一些自定義的處理邏輯
實現方式
對比:
這兩種方法本質上差很少,畢竟 loadClass() 也會調用 findClass(),可是從邏輯上講咱們最好不要直接修改 loadClass() 的內部邏輯。建議的作法是隻在 findClass() 裏重寫自定義類的加載方法,根據參數指定類的名字,返回對應的 Class 對象的引用
說明 :
代碼實現:
public class MyClassLoader extends ClassLoader {
// class文件的存放路徑
private String byteCodePath;
public MyClassLoader(String byteCodePath) {
this.byteCodePath = byteCodePath;
}
public MyClassLoader(ClassLoader parent, String byteCodePath) {
super(parent);
this.byteCodePath = byteCodePath;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
BufferedInputStream bis = null;
ByteArrayOutputStream baos = null;
try {
//獲取字節碼文件的完整路徑
String fileName = byteCodePath + className + ".class";
//獲取一個輸入流
bis = new BufferedInputStream(new FileInputStream(fileName));
//獲取一個輸出流
baos = new ByteArrayOutputStream();
//具體讀入數據並寫出的過程
int len;
byte[] data = new byte[1024];
while ((len = bis.read(data)) != -1) {
baos.write(data, 0, len);
}
//獲取內存中的完整的字節數組的數據
byte[] byteCodes = baos.toByteArray();
//調用defineClass(),將字節數組的數據轉換爲Class的實例。
Class clazz = defineClass(null, byteCodes, 0, byteCodes.length);
return clazz;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (baos != null)
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (bis != null)
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
複製代碼
使用:
public class MyClassLoaderTest {
public static void main(String[] args) {
MyClassLoader loader = new MyClassLoader("D:\\dev\\workspace\\demo\\代碼\\JVMDemo1\\chapter04\\src\\com\\atguigu\\java1\\");
try {
Class clazz = loader.loadClass("Demo1");
System.out.println("加載此類的類的加載器爲:" + clazz.getClassLoader().getClass().getName());
System.out.println("加載當前Demo1類的類的加載器的父類加載器爲:" + clazz.getClassLoader().getParent().getClass().getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
複製代碼
打印結果:
加載此類的類的加載器爲:com.atguigu.java2.MyClassLoader //使用成功
加載當前Demo1類的類的加載器的父類加載器爲:sun.misc.Launcher$AppClassLoader //說明自定義加載器的默認父類就是系統加載器
複製代碼
爲了保證兼容性,JDK 9 沒有從根本上改變三層類加載器架構和雙親委派模 型,但爲了模塊化系統的順利運行,仍然發生了一些值得被注意的變更
擴展機制被移除,擴展類加載器因爲向後兼容性的緣由被保留,不過被重命 名爲平臺類加載器(Platform Class Loader)。能夠經過 ClassLoader 的新方法 getPlatformClassLoader() 來獲取
JDK 9 時基於模塊化進行構建(原來的 rt.jar 和 tools.jar 被拆分紅數十個 JMOD 文件),其中的 Java 類庫就已自然地知足了可擴展的需求,那天然無需 再保留 \lib\ext 目錄,此前使用這個目錄或者 java.ext.dirs 系統 變量來擴展 JDK 功能的機制已經沒有繼續存在的價值了
平臺類加載器和應用程序類加載器都再也不繼承自 java.net.URLClassLoader
如今啓動類加載器、平臺類加載器、應用程序類加載器全都繼承於 jdk.internal.loader.BuiltinClassLoader
若是有程序直接依賴了這種繼承關係,或者依賴了 URLClassLoader 類的特 定方法,那代碼極可能會在 JDK 9 及更高版本的 JDK 中崩潰
在 Java 9 中,類加載器有了名稱。該名稱在構造方法中指定,能夠經過 getName() 方法來獲取。平臺類加載器的名稱是 Platform,應用類加載器的 名稱是 App。類加載器的名稱在調試與類加載器相關的問題時會很是有用
啓動類加載器如今是在 JVM 內部和 Java 類庫共同協做實現的類加載器 (之前是 C++ 實現),但爲了與以前代碼兼容,在獲取啓動類加載器的場景 中仍然會返回 null,而不會獲得 BootClassLoader 實例
類加載的委派關係也發生了變更
當平臺及應用程序類加載器收到類加載請求,在委派給父加載器加載前,要 先判斷該類是否可以歸屬到某一個系統模塊中,若是能夠找到這樣的歸屬關係, 就要優先委派給負責哪一個模塊的加載器完成加載
附加信息:
在 Java 模塊化系統明確規定了三個類加載器負責各自加載的模塊:
啓動類加載器負責加載的模塊
java.base java.security.sasl
java.datatransfer java.xml
java.desktop jdk.httpserver
java.instrument jdk.internal.vm.ci
java.logging jdk.management
java.management jdk.management.agent
java.management.rmi jdk.naming.rmi
java.naming jdk.net
java.prefs jdk.sctp
java.rmi jdk.unsupported
複製代碼
平臺類加載器負責加載的模塊
java.activation* jdk.accessibility
java.compiler* jdk.charsets
java.corba* jdk.crypto.cryptoki
java.scripting jdk.crypto.ec
java.se jdk.dynalink
java.se.se jdk.incubator.httpclient
java.security.jgss jdk.internal.vm.compiler*
java.smartcardio jdk.jsobject
java.sql jdk.localedata
java.sql.rowset jdk.naming.dns
java.transaction* jdk.scripting.nashorn
java.xml.bind* jdk.security.auth
java.xml.crypto jdk.security.jgss
java.xml.ws* jdk.xml.dom
java.xml.ws.annotation* jdk.zipfs
複製代碼
應用程序類加載器負責加載的模塊
jdk.aot jdk.jdeps
jdk.attach jdk.jdi
jdk.compiler jdk.jdwp.agent
jdk.editpad jdk.jlink
jdk.hotspot.agent jdk.jshell
jdk.internal.ed jdk.jstatd
複製代碼