三句話總結JDK8的類加載機制:
- 類緩存:每個類加載器對他加載過的類都有一個緩存。
- 雙親委派:向上委托查找,向下委托加載。
- 沙箱保護機制:不允許應用程序加載JDK內部的系統類。
JDK8的類加載體系
類加載器的核心方法
//protected聲明可以被子類覆蓋
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// 每個類加載起對他加載過的類都有一個緩存,先去緩存中查看有沒有加載過Class<?> c = findLoadedClass(name);if (c == null) {//沒有加載過,就走雙親委派,找父類加載器進行加載。long t0 = System.nanoTime();try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);//沒有父類加載器(表示parent是BootstrapClassLoad)就查詢BootstrapClassLoad的緩存不存在則創建,但是BootstrapClassLoad只創建核心類庫(java.lang.*、java.util.* 等,非核心類會返回空或者或拋異常)}} catch (ClassNotFoundException e) {}if (c == null) {//BootstrapClassLoaderlong t1 = System.nanoTime();// 父類加載起沒有加載過,就自行解析class文件加載。c = findClass(name);sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}//這一段就是加載過程中的鏈接Linking部分,分為驗證、準備,解析三個部分。// 運行時加載類,默認是無法進行鏈接步驟的。if (resolve) {resolveClass(c);}return c;}}
可以保護JDK內部的核心類不會被應用覆蓋,因為本加載器緩存沒有就會往父類加載器緩存找,核心類在bootstarp加載器緩存中存在。并且緩存相關代碼是native無法直接被java代碼操作
沙箱保護機制
private ProtectionDomain preDefineClass(String name,ProtectionDomain pd){if (!checkName(name))throw new NoClassDefFoundError("IllegalName: " + name);// 不允許加載核心類if ((name != null) && name.startsWith("java.")) {throw new SecurityException("Prohibited package name: " +name.substring(0, name.lastIndexOf('.')));}if (pd == null) {pd = defaultDomain;}if (name != null) checkCerts(name, pd.getCodeSource());return pd;}
Linking鏈接過程
在ClassLoader的loadClass方法中還有一個resolveClass,是一個native方法,其實現的過程稱為linking-鏈接。
- 加載:通過類加載器將class文件加載到jvm中(應用唯一可插手的步驟)
- 驗證:驗證class規范
- 準備:為靜態變量分配內存賦默認值(例如int為0)
- 解析:符號引用解析為直接引用
- 初始化:靜態變量賦值,執行類的靜態代碼塊,初始化當前類的父類
Class.forName默認會直接進行Linking并初始化,ClassLoader.loadClass不會而是只加載(懶加載)
通過類加載器引入外部Jar包
public class OADemo2 {public static void main(String[] args) throws Exception {Double salary = 15000.00;Double money = 0.00;URL jarPath = new URL("file:/Users/roykingw/DevCode/ClassLoadDemo/out/artifacts/SalaryCaler_jar/SalaryCaler.jar");URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {jarPath});//模擬不停機狀態while (true) {try {money = calSalary(salary,urlClassLoader);System.out.println("實際到手Money:" + money);}catch(Exception e) {e.printStackTrace();System.out.println("加載出現異常 :"+e.getMessage());}Thread.sleep(5000);}}private static Double calSalary(Double salary,ClassLoader classloader) throws Exception {Class<?> clazz = classloader.loadClass("com.roy.oa.SalaryCaler");if(null != clazz) {Object object = clazz.newInstance();return (Double)clazz.getMethod("cal", Double.class).invoke(object, salary);}return -1.00;}
}
-
哪些jar包適合放到外部加載?
規則引擎、統一審批規則、訂單狀態規則,因為會直接加載到jvm,要考慮安全性 -
外部jar包可以放到哪些地方?
URLClassLoader可以定義URL從遠程Web服務器加載Jar包。
drools規則引擎實現了從maven倉庫遠程加載核心規則文件。
自定義類加載器實現Class代碼混淆
對class進行MD5、對稱加密、非對稱加密,類加載器再進行解密。同時把解密類加載器通過另一個加載器內網遠程加載,或者u盤加載來實現Class代碼混淆加密
自定義類加載器實現熱加載
在 Java 中,類加載器一旦加載某個類,該類就無法被“卸載”(除非整個類加載器被 GC 回收)。所以熱加載的關鍵是:每次都使用一個新的 ClassLoader 實例來加載 class 文件,不復用舊的 ClassLoader,否則類會復用舊版本
public class OADemo5 {public static void main(String[] args) throws Exception {Double salary = 15000.00;Double money = 0.00;//模擬不停機狀態while (true) {try {money = calSalary(salary);System.out.println("實際到手Money:" + money);}catch(Exception e) {System.out.println("加載出現異常 :"+e.getMessage());}Thread.sleep(5000);}}private static Double calSalary(Double salary) throws Exception {SalaryJARLoader salaryClassLoader = new SalaryJARLoader("/Users/roykingw/lib/SalaryCaler.jar");//每次都創建新的ClassLoader實例來加載 class 文件,新的ClassLoader里當然沒有緩存System.out.println(salaryClassLoader.getParent());Class<?> clazz = salaryClassLoader.loadClass("com.roy.oa.SalaryCaler");if(null != clazz) {Object object = clazz.newInstance();return (Double)clazz.getMethod("cal", Double.class).invoke(object, salary);}return -1.00;}
}
這種熱加載機制需要創建出非常多的ClassLoader對象。而這些不用的ClassLoader對象加載過的緩存對象也會隨之成為垃圾。這會讓JVM中本來就不大的元數據區帶來很大的壓力,極大的增加GC線程的壓力。
把SalaryJARLoader加載過的類打印出來,你會發現,在加載SalaryCaler時,其實不光加載了這個類,同時還加載了Double和Object兩個類。這兩個類哪里來的?這就是JVM實現的懶加載機制。JVM為了提高類加載的速度,并不是在啟動時直接把進程當中所有的類一次加載完成,而是在用到的時候才去加載。也就是懶加載。
打破雙親委派,實現同類多版本共存
外部加載的jar包,使用自定義加載器,這時如果代碼中使用一樣的類,那么AppClassLoader就有了緩存,就不會走自定義加載器的邏輯了
tomcat打破雙親委派
tomcat的幾個主要類加載器:
- commonLoader:Tomcat最基本的類加載器,加載路徑中的class可以被Tomcat容器本身以及各個Webapp訪問;
- catalinaLoader:Tomcat容器私有的類加載器,加載路徑中的class對于Webapp不可見;
- sharedLoader:各個Webapp共享的類加載器,加載路徑中的class對于所有Webapp可見,但是對于Tomcat容器不可見;
- WebappClassLoader:各個Webapp私有的類加載器,加載路徑中的class只對當前Webapp可見,比如加載war包里相關的類,每個war包應用都有自己的WebappClassLoader,實現相互隔離,比如不同war包應用引入了不同的spring版本,這樣實現就能加載各自的spring版本;
- Jsp類加載器:針對每個JSP頁面創建一個加載器。這個加載器比較輕量級,所以Tomcat還實現了熱加載,也就是JSP只要修改了,就創建一個新的加載器,從而實現了JSP頁面的熱更新。
使用類加載器能不能不用反射?
強行的類型轉換會報錯Exception in thread “main” java.lang.ClassCastException: com.roy.oa.SalaryCaler cannot be cast to com.roy.oa.SalaryCaler
JDK的SPI擴展機制
ServiceLoader.load(SalaryCalService.class) 就可以查找到某一個接口的全部實現類。應用所需要的,是提供一個配置文件。 這個配置文件需要放在 ${classpath}/META-INF/services這個固定的目錄下。然后文件名是傳入接口的全類名。而文件的內容則是一行表示一個實現類的全類名。
SPI機制也是傳入了ClassLoader的。
public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);}
SPI配置文件可以放入jar包中,但是必須要使用URLClassLoader,由于向上委托查找會找到AppClassLoader,那么我們可以通過構造函數設置它的parent為salaryJARLoader就可以實現了。還可以通過java -cp這個啟動參數直接把jar包導入