面試官:談談類載入器吧,你有沒有看過類載入器的原始碼
一、類載入
1.1、在java程式碼中,型別的載入,連線,初始化過程都是在程式執行期間完成的。
圖示:
1.2、型別的載入——這裡的型別是指的什麼?
答:型別就是指的我們Java原始碼通過編譯後的class檔案。
1.3、型別的來源有哪些?
(1)本地磁碟
(2)網路下載,class檔案
(3)war,jar下載入,class檔案
(4)從專門的資料庫中讀取,class檔案(少見)
(5)將java原始檔動態編譯成class檔案
1)典型的就是動態代理,通過執行期生成class檔案
2)我們的jsp會被轉換成servlet,而我們的serlvet是一個java檔案,會被編譯成class檔案
1.4、通過什麼來進行載入?(類載入器)
1.5、類載入的分類以及各種載入職責以及層級結構
(1)系統級別
1)啟動類載入器
2)擴充套件類載入器
3)系統類載入器(App類載入器)
(2)使用者級別的
自定義類載入器(繼承我們的ClassLoader)
(3)層級結構
二、類載入器載入我們的Class的時候遵循我們的雙親委派模型
在雙親委派機制中,各個載入器按照父子關係形成樹型結構,除了根載入器以外,每一個載入器有且只有一個父載入器
1、原始碼分析:
1 protected Class<?> loadClass(String name, boolean resolve) 2 throws ClassNotFoundException 3 { 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 //呼叫父類的loadClass 14 c = parent.loadClass(name, false); 15 } else {//不存在父類,表示當前的classLoader是extClassLoader 16 //那麼就會呼叫啟動類判斷是否載入過 17 c = findBootstrapClassOrNull(name); 18 } 19 } catch (ClassNotFoundException e) { 20 // ClassNotFoundException thrown if class not found 21 // from the non‐null parent class loader 22 } 23 //到目標位置,app ext boot都沒有去載入過 24 if (c == null) { 25 // If still not found, then invoke findClass in order 26 // to find the class. 27 long t1 = System.nanoTime(); 28 //委託我們的子類的classLoader去找 29 c = findClass(name); 30 31 // this is the defining class loader; record the stats 32 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 * Finds the class with the specified <a href="#name">binary name</a>. 3 * This method should be overridden by class loader implementations that 4 * follow the delegation model for loading classes, and will be invoked b y 5 * the {@link #loadClass <tt>loadClass</tt>} method after checking the 6 * parent class loader for the requested class. The default implementatio n 7 * throws a <tt>ClassNotFoundException</tt>. 8 * 9 * @param name 10 * The <a href="#name">binary name</a> of the class 11 * 12 * @return The resulting <tt>Class</tt> object 13 * 14 * @throws ClassNotFoundException 15 * If the class could not be found 16 * 17 * @since 1.2 18 */ 19 protected Class<?> findClass(String name) throws ClassNotFoundException { 20 throw new ClassNotFoundException(name); 21 }
(3)寫方法loadClassData(String name)去讀取我們的class資料(非必須)
1 /** 2 * 自定義的載入器 3 * Created by smlz on 2019/10/22. 4 */ 5 public class TulingClassLoader extends ClassLoader { 6 7 private final static String fileSuffixExt = ".class"; 8 9 private String classLoaderName; 10 11 private String loadPath; 12 13 public void setLoadPath(String loadPath) { 14 this.loadPath = loadPath; 15 } 16 17 public TulingClassLoader(ClassLoader parent, String classLoaderName) { 18 /** 19 * 指定當前類載入器的父類載入器 20 */ 21 super(parent); 22 this.classLoaderName = classLoaderName; 23 } 24 25 public TulingClassLoader(String classLoaderName) { 26 /** 27 * 使用appClassLoader載入器 作為本類的載入器 28 */ 29 super(); 30 this.classLoaderName = classLoaderName; 31 } 32 33 public TulingClassLoader(ClassLoader classLoader) { 34 super(classLoader); 35 } 36 37 /** 38 * 方法實現說明:建立我們的class 的二進位制名稱 39 * @author:smlz 40 * @param name: 類的二進位制名稱 41 * @return: 42 * @exception: 43 * @date:2019/10/22 14:42 44 */ 45 private byte[] loadClassData(String name) { 46 byte[] data = null; 47 ByteArrayOutputStream baos = null; 48 InputStream is = null; 49 50 try { 51 name = name.replace(".","\\"); 52 String fileName = loadPath+name+fileSuffixExt; 53 File file = new File(fileName); 54 is = new FileInputStream(file); 55 56 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 } 76 77 return data; 78 } 79 80 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 } 8 9 //呼叫super()的時候 10 protected ClassLoader() { 11 //在這裡,把getSystemClassLoader()作為我們自定義類載入器的 12 //父親 13 this(checkCreateClassLoader(), getSystemClassLoader()); 14 } 15 16 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 instance 26 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 { 6 7 public static void main(String[] args) throws ClassNotFoundException { 8 /** 9 * 執行test1()方法的時候列印結果式我們的系統類載入器去載入我們的 10 Person的,雖然我們是通過TulingClassLoader 去載入我們的Person.class 11 但是由於雙親委派模型會委託我們的AppClassLoader去我們的classes路面下去 12 載入Person.class由於我們的classes目錄下存在我們的Person.class 13 所以我們的Person.class被我們的AppClassLoader去載入. 14 15 16 17 =================================================== 18 若我們把classpath下的Person.class給刪除掉,那麼我們的 19 TulingClassLoader嘗試去載入我們的Person.class,由於雙親委派模型下會委託父類A ppClassLoader 20 載入,但是我們人工把類路徑下的Person.class給刪除掉了後,那麼我們的AppClassLo ader載入不了 21 我們的Person.class,從而是由我們的TulingClassLoader去載入. 22 **/ 23 test1(); 24 25 } 26 27 //正常情況下,把我們的AppIsCustParentDemo放到D:\\smlz的目錄下 28 public static void test1() throws ClassNotFoundException { 29 30 TulingClassLoader tulingClassLoader = new TulingClassLoader("tulingClas sLoader"); 31 //設定載入路徑 32 tulingClassLoader.setLoadPath("D:\\smlz\\"); 33 //通過自定義類載入器載入我們的AppIsCustParentDemo 34 Class<?> targetClass = tulingClassLoader.loadClass("com.tuling.smlz.jv m.open.AppIsCustParentDemo"); 35 36 System.out.println("targetClass 被class載入器載入..."+targetClass.getCla ssLoader()); 37 38 } 39 }
6、同一個Person.class檔案 被我們的不同的類載入器去載入,那麼我們的jvm記憶體種會生成二個對應的Person的Class物件,而且這二個對應的Class物件是相互不可見的(通過Class物件反射建立的例項物件相互是不能夠相容的不能相互轉型) 這一點也很好的解釋了
1 public class Person { 2 3 private Person person; 4 5 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, Il legalAccessException, InstantiationException, NoSuchMethodException, Invoca tionTargetException { 4 5 TulingClassLoader classLoader1 = new TulingClassLoader("tulingClassLoade r1"); 6 classLoader1.setLoadPath("D:\\smlz\\"); 7 8 TulingClassLoader classLoader2 = new TulingClassLoader("tulingClassLoade r2"); 9 classLoader2.setLoadPath("D:\\smlz\\"); 10 11 //通過classLoader1載入我們的Person 12 Class<?> class1 = classLoader1.loadClass 13 ("com.tuling.smlz.jvm.open.TheSameClassLoadedByDiffClassLoader.Person"); 14 System.out.println("class1的類載入器:‐>"+class1.getClassLoader()); 15 16 Class<?> class2 = classLoader2.loadClass 17 ("com.tuling.smlz.jvm.open.TheSameClassLoadedByDiffClassLoader.Person"); 18 System.out.println("class2的類載入器:‐>"+class2.getClassLoader()); 19 20 System.out.println("class1==class2:"+(class1==class2)); 21 22 //模擬問題 23 Object person = class1.newInstance(); 24 25 Object person2 = class2.newInstance(); 26 27 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 { 2 3 public Person() { 4 System.out.println("Dog類是由我們的類載入器:‐ >"+Dog.class.getClassLoader()); 5 } 6 } 7 8 public class Dog { 9 } 10 11 public class MainTest { 12 13 public static void main(String[] args) { 14 15 Person person = new Person(); 16 System.out.println("Person的classLoader:‐>"+person.getClass().getClassLo ader()); 17 18 } 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 } 3 4 public class Person { 5 6 public Person() { 7 new Dog(); 8 System.out.println("Dog的classLoader:‐‐>"+Dog.class.getClassLoader()); 9 } 10 11 } 12 13 public class TestDemo { 14 public static void main(String[] args) throws ClassNotFoundException, I llegalAccessException, InstantiationException { 15 TulingClassLoader classLoader = new TulingClassLoader("tulingClassLoade r"); 16 classLoader.setLoadPath("D:\\smlz\\"); 17 Class<?> clazz = classLoader.loadClass("com.tuling.smlz.jvm.open.classl oadernamespace.Person"); 18 clazz.newInstance(); 19 20 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 } 3 4 public class Person { 5 6 public Person() { 7 new Dog(); 8 System.out.println("Dog的classLoader:‐‐>"+ Dog.class.getClassLoader()); 9 } 10 } 11 12 13 public class TestDemo { 14 15 public static void main(String[] args) throws IllegalAccessException, I nstantiationException { 16 17 18 System.out.println("Person的類載入器:"+Person.class.getClassLoader()); 19 20 System.out.println("Dog的類載入器:"+Dog.class.getClassLoader()); 21 22 Class<?> clazz = Person.class; 23 clazz.newInstance(); 24 25 } 26 } 27 28 執行結果: 29 Person的類載入器:null 30 Dog的類載入器:sun.misc.Launcher$AppClassLoader@18b4aac2 31 Exception in thread "main" java.lang.NoClassDefFoundError: com/tuling/sm lz/jvm/open/ParentClassLoaderNotAccessSonClassLoader/Dog 32 at com.tuling.smlz.jvm.open.ParentClassLoaderNotAccessSonClassLoader.Pe rson.<init>(Person.java:11) 33 at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) 34 at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstruc torAccessorImpl.java:62) 35 at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Delegating ConstructorAccessorImpl.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.Te stDemo.main(TestDemo.java:16)
10、打破雙親委派模型之 執行緒上下文類載入器
場景:JDBC介面技術之SPI之應用。
類的首次主動使用會觸發類的初始化。
1)呼叫靜態方法
2)給靜態變數賦值獲取讀取一個靜態變數
3)反射 Class.forName
4)new 出一個物件
5)執行main方法的時候
6)初始化子類會初始化他的父類
&n