詳細講解!從JVM直到類加載器

思惟導圖

1、JVM介紹

在介紹JVM以前,先看一下.java文件從編碼到執行的過程:java

整個過程是,x.java文件須要編譯成x.class文件,經過類加載器加載到內存中,而後經過解釋器或者即時編譯器進行解釋和編譯,最後交給執行引擎執行,執行引擎操做OS硬件。mysql

類加載器到執行引擎這塊內容就是JVMgit

JVM是一個跨語言的平臺。從上面的圖中能夠看到,實際上JVM上運行的不是.java文件,而是.class文件。這就引出一個觀點,JVM是一個跨語言的平臺,他不只僅能跑java程序,只要這種編程語言能編譯成JVM可識別的.class文件均可以在上面運行。程序員

因此除了java之外,能在JVM上運行的語言有不少,好比JRuby、Groovy、Scala、Kotlin等等。github

從本質上講JVM就是一臺經過軟件虛擬的計算機,它有它自身的指令集,有它自身的操做系統。web

因此Oracle給JVM定了一套JVM規範,Oracle公司也給出了他的實現。基本上是目前最多人使用的java虛擬機實現,叫作Hotspot。使用java -version能夠查看:面試

一些體量較大,有必定規模的公司,也會開發本身的JVM虛擬機,好比淘寶的TaobaoVM、IBM公司的J9-IBM、微軟的MicrosoftVM等等。sql

2、JDK、JRE、JVM

JVM應該很清楚了,是運行.class文件的虛擬機。JRE則是運行時環境,包括JVM和java核心類庫,沒有核心的類庫是跑不起來的。數據庫

JDK則包括JRE和一些開發使用的工具集。編程

因此總的關係是JDK > JRE > JVM

3、Class加載過程

類加載是JVM工做的一個很重要的過程,咱們知道.class是存在在硬盤上的一個文件,如何加載到內存工做的呢,面試中也常常問這個問題。因此你要和其餘程序員拉開差距,體現差別化,這個問題要搞懂。

類加載的過程實際上分爲三大步:Loading(加載)、Linking(鏈接)、Initlalizing(初始化)

其中第二步Linking又分爲三小步:Verification(驗證)、Preparation(準備)、Resolution(解析)

3.1 Loading

Loading是把.class字節碼文件加載到內存中,並將這些數據轉換成方法區中的運行時數據,在堆中生成一個java.lang.Class類對象表明這個類,做爲方法區這些類型數據的訪問入口

3.2 Linking

Linking簡單來講,就是把原始的類定義的信息合併到JVM運行狀態之中。分爲三小步進行。

3.2.1 Verification

驗證加載的類信息是否符合class文件的標準,防止惡意信息或者不符合規範的字節信息。是JVM虛擬機運行安全的重要保障。

3.2.2 Preparation

建立類或者接口中的靜態變量,並初始化靜態變量賦默認值。賦默認值不是賦初始值,好比static int i = 5,這一步只是把i賦值爲0,而不是賦值爲5。賦值爲5是在後面的步驟。

3.2.3 Resolution

把class文件常量池裏面用到的符號引用轉換成直接內存地址,直接能夠訪問到的內容。

3.3 Initlalizing

這一步真正去執行類初始化clinit()(類構造器)的代碼邏輯,包括靜態字段賦值的動做,以及執行類定義中的靜態代碼塊內(static{})的邏輯。當初始化一個類時,發現父類尚未進行過初始化,則先初始化父類。虛擬機會保證一個類的clinit()方法在多線程環境中被正確加鎖和同步。

4、類加載器

上面就是類加載的整個過程。而最後一步Initlalizing是經過類加載器加載類。類加載器這裏我單獨講一下,由於這是一個重點。

Java中的類加載器由上到下分爲:

  • Bootstrap ClassLoader(啓動類加載器)
  • ExtClassLoader(擴展類加載器)
  • AppClassLoader(應用程序類加載器)

從類圖,能夠看到ExtClassLoader和AppClassLoader都是ClassLoader的子類

因此若是要自定義一個類加載器,能夠繼承ClassLoader抽象類,重寫裏面的方法。重寫什麼方法後面再講。

5、雙親委派機制

講完類加載器,這些類加載器是怎麼工做的呢。對於雙親委派機制可能多多少少有聽過,沒聽過也不要緊,我正要講。

上面說過有Bootstrap,ExtClassLoader,AppClassLoader三個類加載器。工做機制以下:

加載類的邏輯是怎麼樣的呢,核心代碼是能夠在JDK源碼中找到的,在抽象類ClassLoader類的loadClass(),有興趣能夠源碼看看:

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) {
            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
            }
   //若是上層的都找不到相應的class
            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.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

其實整個邏輯已經很清晰了,爲了更好理解,我這裏畫張圖給給你們,更好理解一點:

看到這裏,應該都清楚了雙親委派機制的流程了。重點來了,爲何要使用雙親委派機制呢?

若是面試官問這個問題,必定要答出關鍵字:安全性

反證法來辯證。假設不採用雙親委派機制,那我能夠自定義一個類加載器,而後我寫一個java.lang.String類用自定義的類加載器加載進去,原來java自己又有一個java.lang.String類,那麼類的惟一性就無法保證,就不就給虛擬機的安全帶來的隱患了嗎。因此要保證一個類只能由同一個類加載器加載,才能保證系統類的的安全

6、自定義類加載器

自定義類加載器,上面講過能夠有樣學樣,自定義一個類繼承ClassLoader抽象類。重寫哪一個方法呢?loadClass()方法是加載類的方法,重寫這個不就好了?

若是重寫loadClass()那證實有思考過,可是不太對,由於重寫loadClass()會破壞了雙親委派機制的邏輯。應該重寫loadClass()方法裏的findClass()方法。

findClass()方法纔是自定義類加載器加載類的方法。

那findClass()方法源碼是怎麼樣的呢?

明顯這個方法是給子類重寫用的,權限修飾符也是protected,若是不重寫,那就會拋出找不到類的異常。若是學過設計模式的同窗,應該看得出來這裏用了模板模式的設計模式。因此咱們自定義類加載器重寫此方法便可。開始動手!

建立CustomerClassLoader類,繼承ClassLoader抽象類的findClass()方法。

public class CustomerClassLoader extends ClassLoader {
 //class文件在磁盤中的路徑
    private String path;
 //經過構造器初始化class文件的路徑
    public CustomerClassLoader(String path) {
        this.path = path;
    }

    /**
     * 加載類
     *
     * @param name 類的全路徑
     * @return Class<?>
     * @author Ye hongzhi
     */

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        //獲取class文件,轉成字節碼數組
        byte[] data = getData();
        if (data != null) {
            //將class的字節碼數組轉換成Class類的實例
            clazz = defineClass(name, data, 0, data.length);
        }
        //返回Class對象
        return clazz;
    }

    private byte[] getData() {
        File file = new File(path);
        if (file.exists()) {
            try (FileInputStream in = new FileInputStream(file);
                 ByteArrayOutputStream out = new ByteArrayOutputStream();) {
                byte[] buffer = new byte[1024];
                int size;
                while ((size = in.read(buffer)) != -1) {
                    out.write(buffer, 0, size);
                }
                return out.toByteArray();
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        } else {
            return null;
        }
    }
}

這樣就完成了,接下來測試一下,定義一個Hello類。

public class Hello {
    public void say() {
        System.out.println("hello.......java");
    }
}

使用javac命令編譯成class文件,以下圖:

最後寫個main方法運行測試一把:

public class Main {
    public static void main(String[] args) throws Exception {
        String path = "D:\\mall\\core\\src\\main\\java\\io\\github\\yehongzhi\\classloader\\Hello.class";
        CustomerClassLoader classLoader = new CustomerClassLoader(path);
        Class<?> clazz = classLoader.findClass("io.github.yehongzhi.classloader.Hello");
        System.out.println("使用類加載器:" + clazz.getClassLoader());
        Method method = clazz.getDeclaredMethod("say");
        Object obj = clazz.newInstance();
        method.invoke(obj);
    }
}

運行結果:

7、破壞雙親委派機制

看到這裏,你確定會很疑惑。上面不是纔講過雙親委派機制爲了保證系統的安全性嗎,爲何又要破壞雙親委派機制呢?

重溫一下雙親委派機制,應該還記得,就是底層的類加載器一直委託上層的類加載器,若是上層的已經加載了,就無需加載,上層的類加載器沒有加載則本身加載。這就突出了雙親委派機制的一個缺陷,就是隻能子的類加載器委託父的類加載器,不能反過來用父的類加載器委託子的類加載器

那你會問,什麼狀況會出現父的類加載器委託子的類加載器呢?

還真有這個場景,就是加載JDBC的數據庫驅動。在JDK中有一個全部 JDBC 驅動程序須要實現的接口Java.sql.Driver。而Driver接口的實現類則是由各大數據庫廠商提供。那問題就出現了,DriverManager(JDK的rt.jar包中)要加載各個實現了Driver接口的實現類,而後進行統一管理,可是DriverManager是由Bootstrap類加載器加載的,只能加載JAVA_HOME下lib目錄下的文件(能夠看回上面雙親委派機制的第一張圖),可是實現類是服務商提供的,由AppClassLoader加載,這就須要Bootstrap(上層類加載器)委託AppClassLoader(下層類加載器),也就破壞了雙親委派機制。這只是其中一種場景,破壞雙親委派機制的例子還有不少。

那麼怎麼實現破壞雙親委派機制呢?

  • 最簡單就是自定義類加載器,前面講過爲了避免破壞雙親委派機制重寫findClass()方法,因此若是我要破壞雙親委派機制,那就重寫loadClass()方法,直接把雙親委派機制的邏輯給改了。在JDK1.2後不提倡重寫此方法。因此提供下面這種方式。
  • 使用線程上下文件類加載器(Thread Context ClassLoader)。 這個類加載器能夠經過java.lang.Thread類的setContextClassLoader()方法進行設置,若是建立線程時還未設置,它將會從父線程中繼承一個;若是在應用程序的全局範圍內都沒有設置過,那麼這個類加載器默認就是AppClassLoader類加載器

那麼剛剛說的JDBC又是採用什麼方式破壞雙親委派機制的呢?

固然是採用上下文文件類加載器,還有使用了SPI機制,下面一步一步分解。

第一步,Bootstrap加載DriverManager類,在DriverManager類的靜態代碼塊調用初始化方法。

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
}

第二步,加載Driver接口的全部實現類,獲得Driver實現類的集合,獲取一個迭代器。

第三步,看ServiceLoader.load()方法。

第四步,看迭代器driversIterator。

接着一直找下去,就會看到一個很神奇的地方。

而這個常量值PREFIX則是:

private static final String PREFIX = "META-INF/services/";

因此咱們能夠在mysql驅動包中找到這個文件:

經過文件名找接口的實現類,這是java的SPI機制。到此爲止,破案了大人!

做爲暖男的我,就畫張圖,總結一下整個過程吧:

總結

這篇文章主要介紹了JVM,而後講到JVM的類加載機制的三大步驟,接着講自定義類加載器以及雙親委派機制。最後再深刻探討了爲何要使用雙親委派機制,又爲何要破壞雙親委派機制的問題。可能講得有點長,不過我相信應該都看懂了,由於我講得比較通俗,並且圖文並茂。

上面全部例子的代碼都上傳Github了:

https://github.com/yehongzhi/mall

以爲有用就點個贊吧,你的點贊是我創做的最大動力~

拒絕作一條鹹魚,我是一個努力讓你們記住的程序員。咱們下期再見!!!

能力有限,若是有什麼錯誤或者不當之處,請你們批評指正,一塊兒學習交流!

本文分享自微信公衆號 - java技術愛好者(yehongzhi_java)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索