java中類加載與雙親委派機制詳解
類加載是什麼
把磁盤中的java文件加載到內存中的過程叫做類加載
當我們用java命令運行某個類的main函數啟動程序時,首先需要通過類加載器把主類加載到JVM. 有如下 User 類
package dc.dccmmtop; public Class User { public static void main(String[] args) { System.out.println("hello"); } }
運行 java dc.dccmmtop.User
時, 先要找到 User.Class 文件,查找全路徑就是 Class_PATH + {{package name}},對於User類來說,就是 {$Class_APTH}/dc/dccmmtop.User.Class
假如 User.java
在F:\code
, 並且不在Class_PATH 下,可以通過 java -Classpath "F:\code"
臨時指定。
加載類之後還有後續的步驟:
- 驗證
- 準備
- 解析
- 初始化
- 使用
- 卸載
這篇文章主要來講講類加載
類加載器
不瞭解類加載機制的,可能就認為,隻需找到java文件所在的磁盤位置,然後進行一次讀文件的操作不就完成瞭加載嘛,其實遠非如此。
總有一個加載類的工具,這個工具叫做類加載器,在java代碼中可以通過如下方式獲取當前類的類加載器是什麼
package dccmmtop; public Class User { public static void main(String[] args) { System.out.println("hello"); System.out.println(User.Class.getClassLoader()); } }
如圖可以看到類加載器的名字叫做 AppClassLoader
我們全局搜索一下這個類,會發現在 sun.misc.Launcher.java
文件中找到。
那麼這個AppClassLoader
本身也是一個 java 文件,它又是什麼時候被加載並初始化的呢?
我們滾動到文件頂部,看到 Launcher 類的構造方法部分:
標記1 和標記2 實現瞭一個單例模式,在5 處獲取到瞭 AppClassLoader
實例。也就是說在某一個地方通過調用 Launcher 類中的 getLauncher()
方法,會得到 AppClassLoader
實例, 那麼 getLauncher()
方法又是在哪裡調用的呢?追蹤到這裡已經無法在java代碼中找到上一步瞭,其實這個方法是jvm (c++實現)調用的,如下圖:
以上就是類加載的主要步驟瞭。下面看一下雙親委派機制
雙親委派機制
我們繼續看AppClassLoader
實例化的過程:
在5處,實例化瞭一個AppClassLoader
的對象,同時傳進去瞭一個參數 var1
, 這個 var1 是另外一個類加載器ExtClassLoader
, 我們在進入 getAppClassLoader
方法看一看是怎麼實現的:
先看一下 幾個ClassLoad的繼承關系:
有上面的繼承關系圖可以看出來,AppClassLoader
和 ExtClassLoader
都是從 ClassLoader
繼承來的。
在 Launcher()
中可知,調用 AppClassLoader.getAppClassLoader()
方法時, 把 ExtClassLoader
的實例作為參數傳遞進來,最終到4這一步,作為 var2 參數,調用父類的構造方法,繼續追蹤父類的構造方法直到 ClassLoader
:
在 ClassLoader
構造方法中,維護瞭一個 parent 變量,到此我們知道瞭 AppClassLoader
中 parent 變量保存的是 ExtClassLoader
的實例, 如下圖表示
繼續看Launcher 構造方法:
loadClass()
方法將 Class 文件加載到jvm中,我們跟蹤一下這個方法,會發現最後會調到 根類ClassLoader
中:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the Class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if Class not found // from the non-null parent Class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the Class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining Class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
上面代碼塊中的弟6行,findLoadedClass()
, 先從已加載到的類集合中查找有沒有這個類,如果有的話,直接返回,沒有再進行下一步, findLoadedClass
方法源碼如下:
到 native finnal Class<?> findLoadedClass0(String name);
這裡已經無法在向後追蹤瞭,看到 naive
,要明白 使用native關鍵字說明這個方法是原生函數,也就是這個方法是用C/C++語言實現的,並且被編譯成瞭DLL,由java去調用.
此時 User.Class 是第一次加載,AppClassLoader
中肯定無法在已加載的集合中找到,所以繼續向下走到第 10,11 行. 上面已經分析過,AppClassLoader
中的 parent 是 ExtClassLoader
, 所以在11行由 ExtClassLoader
的實例執行 laodClass
方法。 ExtClassLoader
沒有覆寫根類ClassLoader
的loaderClass
方法,所以也會到這裡,隻不過 ExtClassLoader
的 parent 是 NUll, 會走到13行,調用findBootstrapClassOrNull()
方法,再看一下這個方法的實現:
會發現這個方法也是C++實現的,雖然我們無法看到源碼,但是根據註釋可以知道,這個是保存瞭啟動類加載器加載過的類。
到此為止,我們已經見識過3中不同的類加載器瞭:
- AppClassLoader
- ExtClassLoader
- BootStrapClassLoader
我們先不管這個後面兩個類加載器是什麼, 假定他們也找不到 User.Class. 繼續向下看:
執行到第21行findClas()
這裡,再看源碼
在A-2 這一步,ucp
其實保存的就是當前 ClassLoader 的類加載路徑,就不再展開。要記住此時的 ClassLoader 是 ExtClassLoader
, 假如仍然找不到User.Class 會執行到 A-3.然後返回到 loadClass 方法中, 此時 c 是空,繼續執行到33行,返回到 AppClassLoader
調用 parent.getAppClassLoader
處,在 AppClassLoader
實例的范圍下繼續向後執行,然後再繼續調用 findClass
方法,如果在AppClassLoader
的類加載路徑中找到User.Class 文件,就會 執行 defindClass(name,res)
方法去加載類文件瞭。
整個過程用文字描述起來比較復雜,來張圖就很清楚瞭,為什麼叫做雙親委派:
把 loadedClassList 集合稱作緩存:
- 先在 AppClassLoader 中緩存中找,如果找不到向 ExtClassLoader 找,如果能找到,直接返回
- 在 ExtClassLoader 中緩存找,如果找不到向 BootStrapClassLoader 找,如果能找到,直接返回
- 在 BootStrapClassLoader 找,如果找不到, 在 ExtClassLoader 類路徑集合中找,
- 如果在 ExtClassLoader 類路徑集合找不到,在 AppClassLoader 類路徑集合找
- 如果在 AppClassLoader 類路徑集合中能找到,加載該類,並放入緩存。找不到則報錯
雙親指的是 ExtClassLoader
和 BootStrapClassLoader
, AppClassLoader 先不加載,而是向上讓其“父”加載,父加載不到時,自己再加載。這裡的父不是父類,而是調用層級的關系。
是時候介紹一下 這三個類加載器
BootStrapClassLoader
引導類加載器
負責加載支撐JVM運行的位於JRE的lib目錄下的核心類庫,比如 rt.jar、charsets.jar等
ExtClassLoader
擴展類加載器
負責加載支撐JVM運行的位於JRE的lib目錄下的ext擴展目錄中的JAR 類包
AppClassLoader
應用程序加載器
負責加載ClassPath路徑下的類包,主要就是加載你自己寫的那些類
我們可以寫代碼驗證一下:
package dccmmtop; import sun.misc.Launcher; import java.net.URL; public Class User { public static void main(String[] args) { System.out.println(String.Class.getClassLoader()); // null System.out.println(com.sun.crypto.provider.DESKeyFactory.Class.getClassLoader().getClass().getName()); //sun.misc.Launcher$ExtClassLoader System.out.println(User.Class.getClassLoader().getClass().getName()); // sun.misc.Launcher$AppClassLoader System.out.println(); System.out.println("bootstrapLoader加載以下文件:"); URL[] urls = Launcher.getBootstrapClassPath().getURLs(); for (int i = 0; i < urls.length; i++) { System.out.println(urls[i]); } System.out.println(); System.out.println("extClassloader加載以下文件:"); System.out.println(System.getProperty("java.ext.dirs")); System.out.println(); System.out.println("appClassLoader加載以下文件:"); System.out.println(System.getProperty("java.Class.path")); } }
輸入如下:
null // 因為調用瞭 c++ 實現。無法獲取到java對象 sun.misc.Launcher$ExtClassLoader sun.misc.Launcher$AppClassLoader the bootstrapLoader : null the extClassloader : sun.misc.Launcher$ExtClassLoader@77459877 the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2 bootstrapLoader加載以下文件: file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/resources.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/rt.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/sunrsasign.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jsse.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jce.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/charsets.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jfr.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/Classes extClassloader加載以下文件: C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext appClassLoader加載以下文件: C:\Program Files\Java\jdk1.8.0_261\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\jaccess.jar;...省略
為什麼使用雙親委派機制
- 沙箱安全機制: 自己寫的java.lang.String.Class類不會被加載,這樣便可以防止核心 API庫被隨意篡改
- 避免類的重復加載:當父親已經加載瞭該類時,就沒有必要子ClassLoader再加載一 次,保證被加載類的唯一性
全盤負責委托機制
“全盤負責”是指當一個ClassLoder裝載一個類時,除非顯示的使用另外一個ClassLoder,該類
所依賴及引用的類也由這個ClassLoder載入
自定義類加載器
從上述源碼的描述可知,類加載器的核心方法是 findClass , 和 defineClass 。
defindClass 將class文件從磁盤加載文件到內存,defineClass 開始解析class文件:
所以自定義類加載器隻需繼承 ClassLoader,然後從寫 findClass 文件就行瞭:
目錄如下:
App.java:
import java.io.FileInputStream; import java.lang.reflect.Method; public class App { static class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } // 從磁盤加載文件 private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } // 重寫 protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); // defineClass將一個字節數組轉為Class對象,這個字節數組是class文件讀取後最終的字節 數組。 return defieClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } } public static void main(String args[]) throws Exception { // 初始化自定義類加載器,會先初始化父類ClassLoader,其中會把自定義類加載器的父加載 器設置為應用程序類加載器AppClassLoader MyClassLoader classLoader = new MyClassLoader("D:/dc_code/java"); // D盤創建 // 創建 io/dc 幾級目錄,將User類的復制類User.class丟入該目錄 Class clazz = classLoader.loadClass("io.dc.User"); Object obj = clazz.newInstance(); // 使用反射調用 User 類的 sout 方法 Method method = clazz.getDeclaredMethod("sout", null); method.invoke(obj, null); System.out.println(clazz.getClassLoader().getClass().getName()); } }
打破雙親委派機制
經過上面的源碼分析發現,主要是 ClassLoader
類中的laodClass
方法來實現的雙親委派機制,自己不加載而是先讓其父加載。
所以直接復寫 loadClass 方法即可,不再指定父級加載,當前類直接加載,如下:
import java.io.FileInputStream; import java.lang.reflect.Method; public class App { static class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } // 從磁盤加載文件 private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); // defineClass將一個字節數組轉為Class對象,這個字節數組是class文件讀取後最終的字節 數組。 return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); if (name.startsWith("io.dc")) { // 直接查找, 限定包名 c = findClass(name); } else { // 其他包中的類還是使用雙親委派機制 // 否則會報找不到 Object 類 c = this.getParent().loadClass(name); } // this is the defining class loader; record the stats sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } if (resolve) { resolveClass(c); } return c; } } } public static void main(String args[]) throws Exception { // 初始化自定義類加載器,會先初始化父類ClassLoader,其中會把自定義類加載器的父加載 器設置為應用程序類加載器AppClassLoader MyClassLoader classLoader = new MyClassLoader("D:/dc_code/java"); // D盤創建 // 創建 io/dc 幾級目錄,將User類的復制類User.class丟入該目錄 Class clazz = classLoader.loadClass("io.dc.User"); Object obj = clazz.newInstance(); // 使用反射調用 User 類的 sout 方法 Method method = clazz.getDeclaredMethod("sout", null); method.invoke(obj, null); System.out.println(clazz.getClassLoader().getClass().getName()); } }
到此這篇關於java中類加載與雙親委派機制詳解的文章就介紹到這瞭,更多相關java類加載內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!