一、類載入
Java類載入器是Java執行時環境的一部分,負責動態載入Java類到Java虛擬機器的記憶體空間中。類通常是按需載入,即第一次使用該類時才載入。由於有了類載入器,Java執行時系統不需要知道檔案與檔案系統。學習類載入器時,掌握Java的委派概念很重要。
文末有福利~
1、在java程式碼中,型別的載入,連線,初始化過程都是在程式執行期間完成的。圖示:
1.1、型別的載入——這裡的型別是指的什麼?
答:型別就是指的我們Java原始碼通過編譯後的class檔案。
(1)本地磁碟
(2)網路下載,class檔案
(3)war,jar下載入,class檔案
(4)從專門的資料庫中讀取,class檔案(少見)
(5)將java原始檔動態編譯成class檔案
1)典型的就是動態代理,通過執行期生成class檔案
2)我們的jsp會被轉換成servlet,而我們的serlvet是一個java檔案,會被編譯成class檔案
1.3、通過什麼來進行載入?(類載入器)
1.4、類載入的分類以及各種載入職責以及層級結構
(1)系統級別
1)啟動類載入器
2)擴充套件類載入器
3)系統類載入器(App類載入器)
(2)使用者級別的
自定義類載入器(繼承我們的ClassLoader)
(3)層級結構
二、類載入器載入我們的Class的時候遵循我們的雙親委派模型
在雙親委派機制中,各個載入器按照父子關係形成樹型結構,除了根載入器以外,每一個載入器有且只有一個父載入器
1、原始碼分析:1 protected Class<?> loadClass(String name, boolean resolve)2 throws ClassNotFoundException3 {4 synchronized (getClassLoadingLock(name)) {5 //檢查當前的class物件是否被載入過,被載入過就返回6 Class<?> c = findLoadedClass(name);7 if (c == null) {8 long t0 = System.nanoTime();9 try {10 //判斷當前的classLoader是否有父類11 //若存在父類12 if (parent != null) {13 //呼叫父類的loadClass14 c = parent.loadClass(name, false);15 } else {//不存在父類,表示當前的classLoader是extClassLoader16 //那麼就會呼叫啟動類判斷是否載入過17 c = findBootstrapClassOrNull(name);18 }19 } catch (ClassNotFoundException e) {20 // ClassNotFoundException thrown if class not found21 // from the non‐null parent class loader22 }23 //到目標位置,app ext boot都沒有去載入過24 if (c == null) {25 // If still not found, then invoke findClass in order26 // to find the class.27 long t1 = System.nanoTime();28 //委託我們的子類的classLoader去找29 c = findClass(name);3031 // this is the defining class loader; record the stats32 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 ‐ t0);33 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);34 sun.misc.PerfCounter.getFindClasses().increment();35 }36 }37 if (resolve) {38 resolveClass(c);39 }40 return c;41 }42 }
2、雙親委派模型載入的流程圖3、類載入器的雙親委派模型的好處:總所周知:java.lang.object類是所有類的父類,所以我們程式在執行期間會把java.lang.object類載入到記憶體中,假如java.lang.object類能夠被我們自定義類載入器去載入的話,那麼jvm中就會存在多份Object的Class物件,而且這些Class物件是不相容的。
所以雙親委派模型可以保證java核心類庫下的型別的安全。
藉助雙親委派模型,我們java核心類庫的類必須是由我們的啟動類載入器載入的,這樣可以確保我們核心類庫只會在jvm中存在一份這就不會給自定義類載入器去載入我們核心類庫的類。
根據我們的演示案例,一個class可以由多個類載入器去載入,同事可以在jvm記憶體中存在多個不同版本的Class物件,這些物件是不相容的。
4、如何手寫一個自定義類載入器(根據ClassLoader的doc文件)
(1)我們自定義類載入器必須要繼承ClassLoader
(2)我們必須要findClass(String name)方法
1 /**2 * 自定義的載入器3 * Created by smlz on 2019/10/22.4 */5 public class TulingClassLoader extends ClassLoader {67 private final static String fileSuffixExt = ".class";89 private String classLoaderName;1011 private String loadPath;1213 public void setLoadPath(String loadPath) {14 this.loadPath = loadPath;15 }1617 public TulingClassLoader(ClassLoader parent, String classLoaderName) {18 /**19 * 指定當前類載入器的父類載入器20 */21 super(parent);22 this.classLoaderName = classLoaderName;23 }2425 public TulingClassLoader(String classLoaderName) {26 /**27 * 使用appClassLoader載入器 作為本類的載入器28 */29 super();30 this.classLoaderName = classLoaderName;31 }3233 public TulingClassLoader(ClassLoader classLoader) {34 super(classLoader);35 }3637 /**38 * 方法實現說明:建立我們的class 的二進位制名稱39 * @author:smlz40 * @param name: 類的二進位制名稱41 * @return:42 * @exception:43 * @date:2019/10/22 14:4244 */45 private byte[] loadClassData(String name) {46 byte[] data = null;47 ByteArrayOutputStream baos = null;48 InputStream is = null;4950 try {51 name = name.replace(".","\\\\");52 String fileName = loadPath+name+fileSuffixExt;53 File file = new File(fileName);54 is = new FileInputStream(file);5556 baos = new ByteArrayOutputStream();57 int ch;58 while (‐1 != (ch = is.read())){59 baos.write(ch);60 }61 data = baos.toByteArray();62 }catch (Exception e) {63 e.printStackTrace();64 }finally {65 try{66 if(null != baos) {67 baos.close();68 }69 if(null !=is) {70 is.close();71 }72 }catch (Exception e) {73 e.printStackTrace();74 }75 }7677 return data;78 }7980 protected Class<?> findClass(String name) throws ClassNotFoundException{81 byte[] data = loadClassData(name);82 System.out.println("TulingClassLoader 載入我們的類:===>"+name);83 return defineClass(name,data,0,data.length);84 }85 }
(4)特別需要注意:我們自定義的類載入器預設情況下的父類載入器是我們的系統AppClassLoader
程式碼證據:
1 public TulingClassLoader(String classLoaderName) {2 /**3 * 使用appClassLoader載入器 作為本類的載入器4 */5 super();6 this.classLoaderName = classLoaderName;7 }89 //呼叫super()的時候10 protected ClassLoader() {11 //在這裡,把getSystemClassLoader()作為我們自定義類載入器的12 //父親13 this(checkCreateClassLoader(), getSystemClassLoader());14 }1516 private ClassLoader(Void unused, ClassLoader parent) {17 this.parent = parent;18 if (ParallelLoaders.isRegistered(this.getClass())) {19 parallelLockMap = new ConcurrentHashMap<>();20 package2certs = new ConcurrentHashMap<>();21 domains =22 Collections.synchronizedSet(new HashSet<ProtectionDomain>());23 assertionLock = new Object();24 } else {25 // no finer‐grained lock; lock on the classloader instance26 parallelLockMap = null;27 package2certs = new Hashtable<>();28 domains = new HashSet<>();29 assertionLock = this;30 }31 }
5、怎麼用實驗證明我們的自定義類載入器的父載入器就是系統類載入器(1)把我們的Person.class檔案copy的指定的磁碟目錄下。同時classpath下 存在我們的Person.class檔案
1 /**2 * 證明系統類載入器就是我們的自定義類載入器的父類3 * Created by smlz on 2019/11/12.4 */5 public class AppClassLoaderIsCustomerClassLoaderParent {67 public static void main(String[] args) throws ClassNotFoundException {8 /**9 * 執行test1()方法的時候列印結果式我們的系統類載入器去載入我們的10 Person的,雖然我們是通過TulingClassLoader 去載入我們的Person.class11 但是由於雙親委派模型會委託我們的AppClassLoader去我們的classes路面下去12 載入Person.class由於我們的classes目錄下存在我們的Person.class13 所以我們的Person.class被我們的AppClassLoader去載入.14151617 ===================================================18 若我們把classpath下的Person.class給刪除掉,那麼我們的19 TulingClassLoader嘗試去載入我們的Person.class,由於雙親委派模型下會委託父類AppClassLoader20 載入,但是我們人工把類路徑下的Person.class給刪除掉了後,那麼我們的AppClassLoader載入不了21 我們的Person.class,從而是由我們的TulingClassLoader去載入.22 **/23 test1();2425 }2627 //正常情況下,把我們的AppIsCustParentDemo放到D:\\\\smlz的目錄下28 public static void test1() throws ClassNotFoundException {2930 TulingClassLoader tulingClassLoader = new TulingClassLoader("tulingClassLoader");31 //設定載入路徑32 tulingClassLoader.setLoadPath("D:\\\\smlz\\\\");33 //通過自定義類載入器載入我們的AppIsCustParentDemo34 Class<?> targetClass = tulingClassLoader.loadClass("com.tuling.smlz.jvm.open.AppIsCustParentDemo");3536 System.out.println("targetClass 被class載入器載入..."+targetClass.getClassLoader());3738 }39 }
6、同一個Person.class檔案 被我們的不同的類載入器去載入,那麼我們的jvm記憶體種會生成二個對應的Person的Class物件,而且這二個對應的Class物件是相互不可見的(通過Class物件反射建立的例項物件相互是不能夠相容的不能相互轉型) 這一點也很好的解釋了1 public class Person {23 private Person person;45 public void setPerson(Object person) {6 this.person = (Person) person;7 }8 }
1 public class Demo {2 //需要把我們的ClassPath下的Person.class給刪除3 public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {45 TulingClassLoader classLoader1 = new TulingClassLoader("tulingClassLoader1");6 classLoader1.setLoadPath("D:\\\\smlz\\\\");78 TulingClassLoader classLoader2 = new TulingClassLoader("tulingClassLoader2");9 classLoader2.setLoadPath("D:\\\\smlz\\\\");1011 //通過classLoader1載入我們的Person12 Class<?> class1 = classLoader1.loadClass13("com.tuling.smlz.jvm.open.TheSameClassLoadedByDiffClassLoader.Person");14 System.out.println("class1的類載入器:‐>"+class1.getClassLoader());1516 Class<?> class2 = classLoader2.loadClass17("com.tuling.smlz.jvm.open.TheSameClassLoadedByDiffClassLoader.Person");18 System.out.println("class2的類載入器:‐>"+class2.getClassLoader());1920 System.out.println("class1==class2:"+(class1==class2));2122 //模擬問題23 Object person = class1.newInstance();2425 Object person2 = class2.newInstance();2627 Method method = class1.getMethod("setPerson",Object.class);28 //會丟擲型別轉換錯誤29 method.invoke(person,person2);30 }31 }
7、類載入器的全盤委託機制以及 類載入器的名稱空間(1)類載入器的全盤委託機制:比如我們的Person類是由我們的AClassLoader進行載入的,那麼我們Person引用的Dog類就會委託給我們的A ClassLoader進行載入
1 public class Person {23 public Person() {4 System.out.println("Dog類是由我們的類載入器:‐>"+Dog.class.getClassLoader());5 }6 }78 public class Dog {9 }1011 public class MainTest {1213 public static void main(String[] args) {1415 Person person = new Person();16 System.out.println("Person的classLoader:‐>"+person.getClass().getClassLoader());1718 }19 }
(2)類載入器的名稱空間
類載入器的名稱空間 是有類載入器本身以及所有父載入器所加載出來的binary name(full class name)組成。
1)在同一個名稱空間裡,不允許出現二個完全一樣的binary name。
2)在不同的名稱空間種,可以出現二個相同的binary name。當時二 者對應的Class物件是相互不能感知到的,也就是說Class物件的型別是不一樣的
3)子載入器的名稱空間中的binary name對應的類中可以訪問 父加 載器名稱空間中binary name對應的類,反之不行
8、驗證子載入器加載出來的類可以訪問父載入器載入的類測試環境:我們的Person是由我們的自定義類載入器(把classpath下的Person.class刪除,並且把Person.class copy到磁碟檔案上)TulingClassLoader進行載入的,Dog 是由我們的AppClassLoader進行載入的. 我們在Person中訪問Dog。
1 public class Dog {2 }34 public class Person {56 public Person() {7 new Dog();8 System.out.println("Dog的classLoader:‐‐>"+Dog.class.getClassLoader());9 }1011 }1213 public class TestDemo {14 public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {15 TulingClassLoader classLoader = new TulingClassLoader("tulingClassLoader");16 classLoader.setLoadPath("D:\\\\smlz\\\\");17 Class<?> clazz = classLoader.loadClass("com.tuling.smlz.jvm.open.classloadernamespace.Person");18 clazz.newInstance();1920 System.out.println("Person的類載入器:"+clazz.getClassLoader());21 }22 }
9、如何證明父載入載入的類不能訪問子載入器載入的類測試環境:把我們的Person.class放置在C:\\ProgramFiles\\Java\\jdk1.8.0_131\\jre\\classes這個目錄下,那麼我們的Person.class就會被我們的啟動類載入器載入,而我們的Dog類是被AppClassLoader進行載入,我們的Person類 中引用我們的Dog類會丟擲異常。
1 public class Dog {2 }34 public class Person {56 public Person() {7 new Dog();8 System.out.println("Dog的classLoader:‐‐>"+ Dog.class.getClassLoader());9 }10 }111213 public class TestDemo {1415 public static void main(String[] args) throws IllegalAccessException, InstantiationException {161718 System.out.println("Person的類載入器:"+Person.class.getClassLoader());1920 System.out.println("Dog的類載入器:"+Dog.class.getClassLoader());2122 Class<?> clazz = Person.class;23 clazz.newInstance();2425 }26 }2728 執行結果:29 Person的類載入器:null30 Dog的類載入器:sun.misc.Launcher$AppClassLoader@18b4aac231 Exception in thread "main" java.lang.NoClassDefFoundError: com/tuling/smlz/jvm/open/ParentClassLoaderNotAccessSonClassLoader/Dog32 at com.tuling.smlz.jvm.open.ParentClassLoaderNotAccessSonClassLoader.Person.<init>(Person.java:11)33 at sun.reflect.NativeConstructorAccessorImpl.newInstance0(NativeMethod)34 at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)35 at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)36 at java.lang.reflect.Constructor.newInstance(Constructor.java:423)37 at java.lang.Class.newInstance(Class.java:442)38 at com.tuling.smlz.jvm.open.ParentClassLoaderNotAccessSonClassLoader.TestDemo.main(TestDemo.java:16)
10、打破雙親委派模型之 執行緒上下文類載入器場景:JDBC介面技術之SPI之應用。
類的首次主動使用會觸發類的初始化。
1)呼叫靜態方法
2)給靜態變數賦值獲取讀取一個靜態變數
3)反射 Class.forName
4)new 出一個物件
5)執行main方法的時候
6)初始化子類會初始化他的父類
本文福利: