JVM類加載機制深度解析

準備:

  • Markdown編寫工具
  • 有道雲筆記
  • Idea開發工具
  • GItHub項目地址

分析:

類加載運行全過程:

當我們用java命令運行某個類的main函數啓動程序時,首先需要通過類加載器把主類加載到JVM。

public class Math {
    public static final int initData = 666;
    public static User user = new User();

    public int compute() {  //一個方法對應一塊棧幀內存區域
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
    }
}

  • 其中loadClass的類加載過程有如下幾步:
  • 加載 >> 驗證 >> 準備 >> 解析 >> 初始化 >> 使用 >> 卸載
    • 加載:在硬盤上查找並通過IO讀入字節碼文件,使用到類時纔會加載,例如調用類的main()方法,new對象等等,在加載階段會在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口
    • 驗證:校驗字節碼文件的正確性
    • 準備:給類的靜態變量分配內存,並賦予默認值
    • 解析:將符號引用替換爲直接引用,該階段會把一些靜態方法(符號引用,比如main()方法)替換爲指向數據所存內存的指針或句柄等(直接引用),這是所謂的靜態鏈接過程(類加載期間完成),動態鏈接是在程序運行期間完成的將符號引用替換爲直接引用.
    • 初始化:對類的靜態變量初始化爲指定的值,執行靜態代碼塊

  • 類被加載到方法區中後主要包含 運行時常量池、類型信息、字段信息、方法信息、類加載器的引用、對應class實例的引用等信息。
  • 類加載器的引用:這個類到類加載器實例的引用
  • 對應class實例的引用:類加載器在加載類信息放到方法區中後,會創建一個對應的Class 類型的對象實例放到堆(Heap)中, 作爲開發人員訪問方法區中類定義的入口和切入點。
  • *注意: * 主類在運行過程中如果使用到其它類,會逐步加載這些類。 jar包或war包裏的類不是一次性全部加載的,是使用到時才加載。
public class TestDynamicLoader {
    static {
        System.out.println("*************load TestDynamicLoad************");
    }
    public static void main(String[] args) {
        new A();
        System.out.println("*************load test************");
//        B b = null;  //B不會加載,除非這裏執行 new B()
        B b = new B();
    }
}

class A {
    static {
        System.out.println("*************load A************");
    }
    public A() {
        System.out.println("*************initial A************");
    }
}

class B {
    static {
        System.out.println("*************load B************");
    }
    public B() {
        System.out.println("*************initial B************");
    }
}
# 運行結果:
*************load TestDynamicLoad************
*************load A************
*************initial A************
*************load test************
*************load B************
*************initial B************

類加載器和雙親委派機制:

上面的類加載過程主要是通過類加載器來實現的,Java裏有如下幾種類加載器:

  • 引導類加載器:負責加載支撐JVM運行的位於JRE的lib目錄下的核心類庫,比如rt.jar、charsets.jar等
  • 擴展類加載器:負責加載支撐JVM運行的位於JRE的lib目錄下的ext擴展目錄中的JAR類包
  • 應用程序類加載器:負責加載ClassPath路徑下的類包,主要就是加載你自己寫的那些類
  • 自定義加載器:負責加載用戶自定義路徑下的類包
public class TestJDKClassLoader {
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
        System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());

        System.out.println();
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        ClassLoader extClassloader = appClassLoader.getParent();
        ClassLoader bootstrapLoader = extClassloader.getParent();
        System.out.println("the bootstrapLoader : " + bootstrapLoader);
        System.out.println("the extClassloader : " + extClassloader);
        System.out.println("the appClassLoader : " + 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
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader

the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@2d209079
the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2

bootstrapLoader加載以下文件:
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/classes

extClassloader加載以下文件:
/Users/fwh/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java

appClassLoader加載以下文件:
/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/tools.jar:/Users/fwh/A_FWH/GItHub/fwh-JVM/target/classes:/Users/fwh/.m2/repository/org/projectlombok/lombok/1.18.12/lombok-1.18.12.jar:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar:/Users/fwh/Library/Caches/JetBrains/IntelliJIdea2020.1/captureAgent/debugger-agent.jar
Disconnected from the target VM, address: '127.0.0.1:50876', transport: 'socket'

類加載器初始化過程:

  • 參見類運行加載全過程圖可知其中會創建JVM啓動器實例sun.misc.Launcher。

  • sun.misc.Launcher初始化使用了單例模式設計,保證一個JVM虛擬機內只有一個sun.misc.Launcher實例。 在Launcher構造方法內部,其創建了兩個類加載器,分別是sun.misc.Launcher.ExtClassLoader(擴展類加載器)和sun.misc.Launcher.AppClassLoader(應用類加載器)。

  • JVM默認使用Launcher的getClassLoader()方法返回的類加載器AppClassLoader的實例加載我們的應用程序。

  • 以下來自Java8版本:

//Launcher的構造方法
public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        //構造擴展類加載器,在構造的過程中將其父加載器設置爲null
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        //構造應用類加載器,在構造的過程中將其父加載器設置爲ExtClassLoader,
        //Launcher的loader屬性值是AppClassLoader,我們一般都是用這個類加載器來加載我們自己寫的應用程序
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    Thread.currentThread().setContextClassLoader(this.loader);
    String var2 = System.getProperty("java.security.manager");
    。。。 。。。 //省略一些不需關注代碼

雙親委派機制:

  • JVM類加載器是有親子層級結構的,如下圖:

  • 這裏類加載其實就有一個雙親委派機制,加載某個類時會先委託父加載器尋找目標類,找不到再委託上層父加載器加載,如果所有父加載器在自己的加載類路徑下都找不到目標類,則在自己的類加載路徑中查找並載入目標類。

  • 比如我們的Math類,最先會找應用程序類加載器加載,應用程序類加載器會先委託擴展類加載器加載,擴展類加載器再委託引導類加載器,頂層引導類加載器在自己的類加載路徑裏找了半天沒找到Math類,則向下退回加載Math類的請求,擴展類加載器收到回覆就自己加載,在自己的類加載路徑裏找了半天也沒找到Math類,又向下退回Math類的加載請求給應用程序類加載器,應用程序類加載器於是在自己的類加載路徑裏找Math類,結果找到了就自己加載了。。

  • 雙親委派機制說簡單點就是,先找父親加載,不行再由兒子自己加載

  • 我們來看下應用程序類加載器AppClassLoader加載類的雙親委派機制源碼,AppClassLoader的loadClass方法最終會調用其父類ClassLoader的loadClass方法,該方法的大體邏輯如下:

    • 首先,檢查一下指定名稱的類是否已經加載過,如果加載過了,就不需要再加載,直接返回。
    • 如果此類沒有加載過,那麼,再判斷一下是否有父加載器; 如果有父加載器,則由父加載器加載(即調用parent.loadClass(name, false);).或者是調用bootstrap類加載器來加載。
    • 如果父加載器及bootstrap類加載器都沒有找到指定的類,那麼調用當前類加載器的findClass方法來完成類加載。
//ClassLoader的loadClass方法,裏面實現了雙親委派機制
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);
                }
            } 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();
                //都會調用URLClassLoader的findClass方法在加載器的類路徑裏查找並加載該類
                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;
    }
}

爲什麼要設計雙親委派機制?

  • 沙箱安全機制:自己寫的java.lang.String.class類不會被加載,這樣便可以防止核心API庫被隨意篡改
  • 避免類的重複加載:當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次,保證被加載類的唯一性 看一個類加載示例:
package java.lang;

public class String {
    public static void main(String[] args) {
        System.out.println("**************My String Class**************");
    }
}

運行結果:
錯誤: 在類 java.lang.String 中找不到 main 方法, 請將 main 方法定義爲:
   public static void main(String[] args)
否則 JavaFX 應用程序類必須擴展javafx.application.Application

全盤負責委託機制

  • “全盤負責”是指當一個ClassLoder裝載一個類時,除非顯示的使用另外一個ClassLoder,該類所依賴及引用的類也由這個ClassLoder載入。

自定義類加載器示例:

  • 自定義類加載器只需要繼承 java.lang.ClassLoader 類,該類有兩個核心方法,
    • 一個是loadClass(String, boolean),實現了雙親委派機制,
    • 還有一個方法是findClass,默認實現是空方法,所以我們自定義類加載器主要是重寫findClass方法。
public class MyClassLoaderTest {
    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;
        }

        @Override
        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();
            }
        }

    }

    public static void main(String args[]) throws Exception {
        //初始化自定義類加載器,會先初始化父類ClassLoader,其中會把自定義類加載器的父加載器設置爲應用程序類加載器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        //D盤創建 test/com/tuling/jvm 幾級目錄,將User類的複製類User1.class丟入該目錄
        Class clazz = classLoader.loadClass("com.tuling.jvm.User1");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}
運行結果:
=======自己的加載器加載類調用方法=======
com.tuling.jvm.MyClassLoaderTest$MyClassLoader
  • 此處注意:需要加載是主類,需要在指定目錄下有Object.class。
    • 但由於沙箱安全機制,肯定是會異常的,需要手動邏輯判斷下代碼,重新賦值。

      打破雙親委派機制:

  • 再來一個沙箱安全機制示例,嘗試打破雙親委派機制,用自定義類加載器加載我們自己實現的 java.lang.String.class
public class MyClassLoaderTest2 {
    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);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

        /**
         * 重寫類加載方法,實現自己的加載邏輯,不委派給雙親加載
         *
         * @param name
         * @param resolve
         * @return
         * @throws ClassNotFoundException
         */
        @Override
        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();
                    c = findClass(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 {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        //嘗試用自己改寫類加載機制去加載自己寫的java.lang.String.class
        Class clazz = classLoader.loadClass("java.lang.String");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

運行結果:
java.lang.SecurityException: Prohibited package name: java.lang
    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:758)

Tomcat打破雙親委派機制:

  • 以Tomcat類加載爲例,Tomcat 如果使用默認的雙親委派類加載機制行不行?

  • 我們思考一下:Tomcat是個web容器, 那麼它要解決什麼問題:

    1. 一個web容器可能需要部署兩個應用程序,不同的應用程序可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個服務器只有一份,因此要保證每個應用程序的類庫都是獨立的,保證相互隔離。
    2. 部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果服務器有10個應用程序,那麼要有10份相同的類庫加載進虛擬機。
    3. web容器也有自己依賴的類庫,不能與應用程序的類庫混淆。基於安全考慮,應該讓容器的類庫和程序的類庫隔離開來。
    4. web容器要支持jsp的修改,我們知道,jsp 文件最終也是要編譯成class文件才能在虛擬機中運行,但程序運行後修改jsp已經是司空見慣的事情, web容器需要支持 jsp 修改後不用重啓。
  • 再看看我們的問題:Tomcat 如果使用默認的雙親委派類加載機制行不行?

  • 答案是不行的。爲什麼?

    • 第一個問題,如果使用默認的類加載器機制,那麼是無法加載兩個相同類庫的不同版本的,默認的類加器是不管你是什麼版本的,只在乎你的全限定類名,並且只有一份。
    • 第二個問題,默認的類加載器是能夠實現的,因爲他的職責就是保證唯一性。
    • 第三個問題和第一個問題一樣。
    • 我們再看第四個問題,我們想我們要怎麼實現jsp文件的熱加載,jsp 文件其實也就是class文件,那麼如果修改了,但類名還是一樣,類加載器會直接取方法區中已經存在的,修改後的jsp是不會重新加載的。那麼怎麼辦呢?我們可以直接卸載掉這jsp文件的類加載器,所以你應該想到了,每個jsp文件對應一個唯一的類加載器,當一個jsp文件修改了,就直接卸載這個jsp類加載器。重新創建類加載器,重新加載jsp文件。

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版本;
  • 從圖中的委派關係中可以看出:

    • CommonClassLoader能加載的類都可以被CatalinaClassLoader和SharedClassLoader使用,從而實現了公有類庫的共用,而CatalinaClassLoader和SharedClassLoader自己能加載的類則與對方相互隔離。 WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。
    • 而JasperLoader的加載範圍僅僅是這個JSP文件所編譯出來的那一個.Class文件,它出現的目的就是爲了被丟棄:當Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並通過再建立一個新的Jsp類加載器來實現JSP文件的熱加載功能。
  • tomcat 這種類加載機制違背了java 推薦的雙親委派模型了嗎?答案是:違背了。

    • 很顯然,tomcat 不是這樣實現,tomcat 爲了實現隔離性,沒有遵守這個約定,每個webappClassLoader加載自己的目錄下的class文件,不會傳遞給父類加載器,打破了雙親委派機制。
  • 模擬實現Tomcat的webappClassLoader加載自己war包應用內不同版本類實現相互共存與隔離

public class MyClassLoaderTest3 {
    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;

        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

        /**
         * 重寫類加載方法,實現自己的加載邏輯,不委派給雙親加載
         * @param name
         * @param resolve
         * @return
         * @throws ClassNotFoundException
         */
        @Override
        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("com.tuling.jvm")){
                        c = this.getParent().loadClass(name);
                    }else{
                        c = findClass(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 {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class clazz = classLoader.loadClass("com.tuling.jvm.User1");
        Object obj = clazz.newInstance();
        Method method= clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader());

        System.out.println();
        MyClassLoader classLoader1 = new MyClassLoader("D:/test1");
        Class clazz1 = classLoader1.loadClass("com.tuling.jvm.User1");
        Object obj1 = clazz1.newInstance();
        Method method1= clazz1.getDeclaredMethod("sout", null);
        method1.invoke(obj1, null);
        System.out.println(clazz1.getClassLoader());
    }
}

*注意:同一個JVM內,兩個相同包名和類名的類對象可以共存,因爲他們的類加載器可以不一樣,所以看兩個類對象是否是同一個,除了看類的包名和類名是否都相同之外,還需要他們的類加載器也是同一個才能認爲他們是同一個。 *

相關知識點:

  • javap -private Person.class
    • 反編譯輸出查看其字節碼文件的內容;
  • 主要加載是ClassLoad的loadClass
  • JVM默認使用Launcher的getClassLoader()方法返回的類加載器AppClassLoader的實例加載我們的應用程序。

總結:

  • JVM內容是Java開發者必備知識點,很多底層性能調優時候都需要具備這方面的知識儲備,才能更快速的定位問題點找到方法。
  • 示例代碼-GitHub
  • Blog鏈接:文浩的博客
  • 微信公衆號:
    似水似流年
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章