1. 程式人生 > >玩命學JVM(二)—類載入機制

玩命學JVM(二)—類載入機制

## 前言 Java程式執行圖: ![Alt](https://myblog-1258060977.cos.ap-beijing.myqcloud.com/cnblog/JVM/JVM%E6%B5%81%E7%A8%8B.png) 上一篇[玩命學JVM(一)—認識JVM和位元組碼檔案](https://www.cnblogs.com/cleverziv/p/13751488.html)我們簡單認識了 JVM 和位元組碼檔案。那JVM是如何使用位元組碼檔案的呢?從上圖清晰地可以看到,JVM 通過類載入器完成了這一過程。 以下是類載入機制的知識框架: ![Alt](https://myblog-1258060977.cos.ap-beijing.myqcloud.com/cnblog/JVM/JVM%E7%9A%84%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B62.png) 接下來我們對思維導圖中重難點部分做補充。 ### 1. 是什麼? 類的載入就是將 .class 檔案的二進位制資料讀入到記憶體中,將其放在 JVM 的執行時資料區的方法區內。然後在堆區內建立一個 java.lang.Class 物件,用於封裝類在方法區內的資料結構。 ### 5. 雙親委派模型 雙親委派模型圖如下: ![Alt](https://myblog-1258060977.cos.ap-beijing.myqcloud.com/cnblog/JVM/%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%A8%A1%E5%9E%8B.png) 對於“雙親委派模型”,首先需要糾正一點,“雙親”並不是說它有“兩個親”。實際上行“雙親委派模型”和“雙”毫無關係,只和“親”有關係。 其實“雙親”是翻譯的一個錯誤,原文出處是“parent”,被翻譯成了“雙親”,在計算機領域更常見的說法是“父節點”。所以如果將“雙親委派模型”改為“父委派模型”,應該更好理解。 結合實際的類載入器來說,就是: 1. 每個類載入器都會向上找自己父類載入器嘗試完成類載入; 2. 父載入器載入失敗會向下找載入器嘗試載入。 如圖: ![Alt](https://myblog-1258060977.cos.ap-beijing.myqcloud.com/cnblog/JVM/%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE.png) 接下來我們從原始碼上來分析下 雙親委派模型 除`Bootstrap ClassLoader`外,其它的類載入器都是`ClassLoader`的子類。載入類的方法為`loadClass`,檢視原始碼可發現,`loadClass`在`ClassLoader`中有具體的實現,且在各個子類中都沒有被覆蓋。 先介紹三個重要的函式,對後續的原始碼閱讀有幫助: `loadClass`:呼叫父類載入器的loadClass,載入失敗則呼叫自己的findClass方法。 `findClass`:根據名稱讀取檔案存入位元組陣列。 `defineClass`:把一個位元組陣列轉為Class物件。 ```java protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded // 在JVM中檢視類是否已經被載入 Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { // 呼叫父類載入器的 loadClass方法,parent是該類載入器的父類,parent的值可能為 Application ClassLoader、Extension ClassLoader,當想要繼續往上找 Extension ClassLoader時,由於Bootstrap ClassLoader是C/C++實現的,所以在java中是Null c = parent.loadClass(name, false); } else { // 尋找 Bootstrap ClassLoader 載入 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(); // 父載入器開始嘗試載入.class檔案,載入成功就返回一個java.lang.Class,載入不成功就丟擲一個ClassNotFoundException,給子載入器去載入 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) { // 如果要解析這個.class檔案的話,就解析一下,解析的作用主要就是將符號引用替換為直接引用的過程 resolveClass(c); } return c; } } ``` 所謂的雙親委派模型,就是利用了`loadClass`只在父類中實現了這一點。 #### 自定義類載入器 自定義類載入主要有兩種方式: 1. 遵守雙親委派模型:繼承ClassLoader,重寫findClass()方法。 2. 破壞雙親委派模型:繼承ClassLoader,重寫loadClass()方法。 通常我們推薦採用第一種方法自定義類載入器,最大程度上的遵守雙親委派模型。 我們看一下實現步驟 (1)建立一個類繼承ClassLoader抽象類 (2)重寫findClass()方法 (3)在findClass()方法中呼叫defineClass() 第一步,自定義一個實體類Person.java,我把它編譯後的Person.class放在D盤根目錄下: ```java package com.xrq.classloader; public class Person { private String name; public Person() { } public Person(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String toString() { return "I am a person, my name is " + name; } } ``` 第二步,自定義一個類載入器,裡面主要是一些IO和NIO的內容,另外注意一下 defineClass方法可以把二進位制流位元組組成的檔案轉換為一個java.lang.Class----只要二進位制位元組流的內容符合Class檔案規 範。我們自定義的MyClassLoader繼承自java.lang.ClassLoader,就像上面說的,只實現findClass方法: ```java public class MyClassLoader extends ClassLoader { public MyClassLoader() { } public MyClassLoader(ClassLoader parent) { super(parent); } protected Class findClass(String name) throws ClassNotFoundException { File file = getClassFile(name); try { byte[] bytes = getClassBytes(file); Class c = this.defineClass(name, bytes, 0, bytes.length); return c; } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); } private File getClassFile(String name) { File file = new File("D:/Person.class"); return file; } private byte[] getClassBytes(File file) throws Exception { // 這裡要讀入.class的位元組,因此要使用位元組流 FileInputStream fis = new FileInputStream(file); FileChannel fc = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel wbc = Channels.newChannel(baos); ByteBuffer by = ByteBuffer.allocate(1024); while (true) { int i = fc.read(by); if (i == 0 || i == -1) break; by.flip(); wbc.write(by); by.clear(); } fis.close(); return baos.toByteArray(); } } ``` 第三步,Class.forName有一個三個引數的過載方法,可以指定類載入器,平時我們使用的Class.forName("XX.XX.XXX")都是使用的系統類載入器Application ClassLoader。寫一個測試類: ```java public class TestMyClassLoader { public static void main(String[] args) throws Exception { MyClassLoader mcl = new MyClassLoader(); Class c1 = Class.forName("com.xrq.classloader.Person", true, mcl); Object obj = c1.newInstance(); System.out.println(obj); System.out.println(obj.getClass().getClassLoader()); } } ``` 執行結果: > I am a person, my name is null com.xrq.classloader.MyClassLoader@5d888759 ## 參考文獻 https://baijiahao.baidu.com/s?id=1636309817155065432&wfr=spider&for=pc https://blog.csdn.net/qq_44836294/article/details/10