Java類加載器與雙親委派

前言

JVM對於java程序員來講既是高級也是基礎,剛入行的同窗沒必要知道jvm的內存劃分、不須要知道類的加載過程、GC的回收過程也能夠舒舒服服的寫代碼,可是這種知其然不知其因此然的態度確定會限制咱們的上升空間,今天這篇文章開始走進jvm,咱們從第一步開始,先搞清楚類是怎麼被加載的,這就是今天要分享的內容!java

JVM的組成結構

進入正題以前要先說一下JVM,JVM的組成結構主要是由 類裝載子系統、運行時數據區、執行引擎、本地方法接口這4部分組成,而今天的文章主要圍繞類狀態子系統展開描述!c++

類加載器

  • 啓動類加載器(BootstrapClassLoader):由c++語言提供,主要負責加載%{JAVA_HOME}\jdk1.8.0_261\jre\lib目錄下的類;
  • 擴展類加載器(ExtClassLOader):sun.misc.Launcher.ExtClassLoader,主要負責加載%{JAVA_HOME}\jdk1.8.0_261\jre\lib\ext目錄下的類;
  • 應用程序類加載器:sun.misc.Launcher.AppClassLoader,負責加載咱們配置的環境變量classpath目錄下的類;
  • 自定義類加載器:經過繼承ClassLoader類能夠實現本身定製的類加載方式,這種方式能夠用來打破雙親委派模型(雙親委派模型下面會講到);

咱們能夠經過一段代碼很清晰的看到每一個類加載器的樣子:程序員

public class TestClassLoader {
    public static void main(String[] args) {
        System.out.println(Object.class.getClassLoader());
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());
        System.out.println(TestClassLoader.class.getClassLoader());
    }
}
複製代碼

輸出結果:bootstrap

null
sun.misc.Launcher$ExtClassLoader@77459877
sun.misc.Launcher$AppClassLoader@18b4aac2
複製代碼

Object類對應的加載器爲何是null嘞? 上面已經說過了,jvm內部會建立一個c++編寫的啓動類加載器負責去加載%{JAVA_HOME}\jdk1.8.0_261\jre\lib目錄下的類,這個加載器在java裏面是獲取不到的,因此是null; 下面兩個,一個是ExtClassLoader,一個是AppClassLoader,是sun.misc.Launcher類的靜態內部類;而這個Launcher類是由C++調用sun.misc.Launcher#getLauncher方法得到的;api

各個類加載之間的關係

能夠經過如下代碼看一下類加載器之間有什麼聯繫安全

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);
複製代碼

輸出結果:markdown

the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@77459877
the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2
複製代碼

能夠看到,默認的類加載器是AppClassLoader,往上走是ExtClassLoader,最上層是BootStrapClassLoader; 先來看一下類加載器的類圖結構:app

image.png

類加載器都是繼承的ClassLoader,爲每一個子類都維護了一個parent屬性:jvm

image.png

下面咱們就重點看parent屬性作了什麼事情,在實例化Launcher的時候 ,構造器裏面對app和ext這兩個對象作了初始化:ide

image.png

  • 上面的代碼作了兩件事情:
  1. 建立了一個ExtClassLoader,獲得var1;
  2. 建立AppClassLoader,把var1傳進去;

那繼續點進去看看AppClassLoader是怎麼建立的,中間套娃的代碼我就跳過了,他是直接調用super(parent)這個構造器,直接看他就行了:

image.png

在這個地方維護了類加載器之間的父子關係,因此Ext也是App的父加載器,那麼這麼作他到底要幹什麼呢?這裏涉及到了一個概念:雙親委派(下面會細說);上面的代碼還反映了一個問題:默認的類加載器是AppClassLoader?在JVM內部會默認調用Launcher類的getClassLoader()方法來獲取一個默認類加載器進行加載,而這個classLoader恰好就是在實例化Launcher類的時候生成的AppClassLoader:

image.png

image.png

雙親委派模型

image.png

一個類在被類加載器加載的時候,該類的加載器不會當即去加載,而是經過parent屬性找到其父加載器進行加載,一直遞歸往上找,一直到頂層的BootStrap都沒有被加載,就會返回到本類的加載器進行加載:

ClassLoader裏面除了維護parent屬性外,還維護了一個公共的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) {
                        // 這裏會一直往上調,app的parent是ext,ext的parent是bootstrap
                        c = parent.loadClass(name, false);
                    } else {
                        // 這個方法最終會調用到一個native方法,加載不到類的時候會返回null
                        // return null if not found
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                // 父加載器沒有加載到類,返回null
                if (c == null) {
                    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;
        }
    }
複製代碼

看完上面的代碼,咱們的思路就很清晰了,其實就是遞歸向上調用若是沒有加載到就再向下返回;

  • 爲何要設計雙親委派模型來加載類呢?
  1. 保證一個類在內存裏只被加載一次;
  2. 保護了jdk內部的類的安全性,防止外部進行破壞;

第一點你們應該改都明白,第二點是什麼意思嘞?咱們跑一段代碼演示一下:

// 覆蓋原有的java.lang包
package java.lang;

// 覆蓋原有的Object類
public class Object {
    public static void main(String[] args) {
        System.out.println("object ....");
    }
}
複製代碼

上面的代碼執行完以後,會是什麼結果呢?

錯誤: 在類 java.lang.Object 中找不到 main 方法, 請將 main 方法定義爲:
   public static void main(String[] args) 不然 JavaFX 應用程序類必須擴展javafx.application.Application 複製代碼

因此,由於有雙親委派的存在,咱們這種惡意破壞原有api的行爲就行不通了;

自定義類加載器

咱們能夠經過自定義類加載器,來指定咱們本身要去加載的類;讀完以上源碼咱們不難發現,雙親委派的邏輯在loadClass方法裏,而加載類的邏輯是在findClass方法裏,我想要本身實現一個類加載器就應該去繼承ClassLoader,重寫findClass方法,在findClass方法裏面去加載咱們本身指定的類:

public 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
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            byte[] bytes = loadByte(name);
            return defineClass(name, bytes, 0, bytes.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");
        // 在這個路徑下面放一個User.class文件,由咱們本身的類加載器去加載
        Class clazz = classLoader.loadClass("com.maolin.User");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

複製代碼

運行,輸出結果:

com.maolin.MyClassLoader
複製代碼

User.class的字節碼是被MyClassLoader加載器加載的; 經過這種方式還能夠打破雙親委派的機制,在重寫findClass的基礎上,再重寫loadClass:

@Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c = findLoadedClass(name);
            /* 咱們把ClassLoader類的loadClass方法裏的代碼複製出來, 把雙親委派的那段代碼去掉,讓當前的類加載器直接加載,不向上委託 */
            if (c == null) {
                long t1 = System.nanoTime();
                c = findClass(name);

                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
複製代碼
  • 什麼狀況下會打破雙親委派呢
  1. JDBC
  2. Tomcat

打破雙親委派機制篇幅太長,後續會單獨寫一篇文章來描述,想知道結果的能夠參考這兩篇文章:

  1. 聊聊JDBC是如何破壞雙親委派機制的
  2. 深刻理解 Tomcat(四)Tomcat 類加載器之爲什麼違背雙親委派模型

類加載器說完了,咱們再來瞅瞅一個類被加載的時候會經歷哪些過程:

image.png

  • 一個完整的類加載過程會經歷:加載-->驗證-->準備-->解析-->初始化
  1. 加載:從磁盤上讀取xx.class文件並生成其對應的Class對象;
  2. 校驗字節碼文件的格式(字節碼也屬於JVM系統的一種機器碼,也是有必定的格式的)
  3. 準備:給類的靜態變量分配內存並賦予默認值。boolean=false,int=0等;
  4. 解析:把符號引用替換爲直接引用(也被稱爲靜態連接),在加載階段會對方法,靜態變量生成的符號引用替換爲內存中的真實引用地址;
  5. 初始化:給類的靜態變量設置初始值,就是咱們本身設置的值,並執行靜態代碼塊;

結語

感謝各位讀者朋友耐心看完個人文章,文章中如有錯誤之處,還請留言指正,或者文章中有哪一個細節描述的不夠清晰,也能夠在評論區留言,我看到後必定會回覆並改正;

不求作的最好,但求作的更好。

相關文章
相關標籤/搜索