一篇文章搞定JVM如何加載.class文件

一、Java虛擬機

JVM是一個內存中的虛擬機,主要運用內存存儲,所有類、類型、方法,都是在內存中,這決定着我們的程序運行是否健壯、高效。

二、.class文件加載執行流程圖

在這裏插入圖片描述

  • Class Loader(類加載器):依據特定格式,加載class文件到內存
  • Execution Engine:(執行引擎)對命令進行解析,解析完畢之後就可以提交到操作系統裏面執行了
  • Native Interface:融合不同的開發語言的原生庫爲Java所用,性能並不如c/c++高,主流的JVM也是由c/c++實現的
  • Runtime Data Area:JVM內存空間結構模型,所寫的程序都會被加載到這裏

三、一個類從編譯到加載再到執行的流程

這裏我們用一個例子來進行解釋

package com.mtli.reflect;

/**
 * @Description:
 * @Author: Mt.Li
 * @Create: 2020-04-25 13:55
 */
public class Robot {
    private String name;
    public void sayHi(String helloSentence){
        System.out.println(helloSentence + " " + name);
    }

    private String throwHello(String tag){
        return "Hello " + tag;
    }
}
  • 編譯器將Robot.java源文件編譯爲Robot.class字節碼文件
  • ClassLoader將字節碼轉換爲JVM中的Class對象
  • JVM利用Class對象實例化爲Robot對象

3.1、ClassLoader(類加載器)

  • ClassLoader 在Java中有着非常重要的作用,主要工作在Class裝載的加載階段,其主要作用是從系統外部獲得Class二進制數據流。它是Java的核心組件,所有的Class都是由ClassLoader進行加載的,ClassLoader負責通過將Class文件裏的二進制數據流加載進系統,然後交給Java虛擬機進行連接、初始化等操作。

  • 它的種類如下
    在這裏插入圖片描述

  • 還有自定義ClassLoader:Java編寫,定製化加載(加載的可以不是class,java,可以是其他類型的)

  • 想要實現自定義ClassLoader,我們需要重寫兩個關鍵函數

    • findClass(根據名稱或者位置加載.class字節碼,然後使用defindClass)和defineClass(生成.class)
  • 可能有人說咋還要生成.class啊,因爲findClass引入進來的可能是java語言編譯成的.class也有可能是其他語言編譯成的,甚至可能是自定義的,我們需要用defineClass將字節數組流解密之後,將該字節流數組生成JVM能夠識別的字節碼文件。

3.2、自定義CLassLoader

package com.mtli.reflect;

import java.io.*;

/**
 * @Description:自定義ClassLoader
 * @Author: Mt.Li
 * @Create: 2020-04-25 15:37
 */


public class MyClassLoader extends ClassLoader{
    private String path;
    private String classLoaderName;

    public MyClassLoader(String path, String classLoaderName){
        this.path = path;
        this.classLoaderName = classLoaderName;
    }

    // 重寫findClass用於尋找類文件
    // defineClass還是用Java自己的
    @Override
    public Class findClass(String name){
        // 以二進制字節流傳遞進來,然後交給defind
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }

    // 用於加載類文件
    private byte[] loadClassData(String name) {
        name = path + "/" + name + ".class";
        InputStream in = null;
        ByteArrayOutputStream out = null;
        try{
            in = new FileInputStream(new File(name));
            // 通過ByteArrayOutputStream解密
            out = new ByteArrayOutputStream();
            int i = 0;
            // 沒讀完繼續讀
            while((i = in.read()) != -1){
                out.write(i);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 返回字節碼
        return out.toByteArray();
    }
}

我們自定義一個Java類

public class Wali{
	static{
		System.out.println("Hello Wali");
	}
}

用javac對其進行編譯生成Wali.class文件

寫一個測試類:

package com.mtli.reflect;

/**
 * @Description:
 * @Author: Mt.Li
 * @Create: 2020-04-25 17:08
 */
public class ClassLoaderChecker {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoader m = new MyClassLoader("C:/Users/Taogege/Desktop", "myClassLoader");
        Class c = m.loadClass("Wali");
        // 創建實例
        c.newInstance();
    }
}

執行結果:
Hello Wali

3.3、類加載器的雙親委派機制

上面我們在自定義ClassLoader中重寫了它的findClass方法,對於實際應用中,我們也有必要了解findClass和loadClass的執行。

jdk1.8中的loadClass()源碼:

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先我們檢查要加載的類是否加載過
            Class<?> c = findLoadedClass(name);
            // c爲null,則遞歸該方法,到當前加載器的父級繼續查詢
            if (c == null) {
                // native方法
                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;
        }
    }
先要說的

上面我們說了ClassLoader有多種,不同的ClassLoader加載類的方式和路徑有所不同,爲了實現分工,各自負責各自的區塊,使得邏輯更加明確,加載類的時候各司其職,當然存在一個機制,讓他們相互協作,形成一個整體,這個機制就是雙親委派機制
在這裏插入圖片描述
由上圖,我們看到左邊是由下而上檢測是否曾經加載過這個類,比如先檢測用戶自定義類加載器
是否加載過這個類,有則提交給JVM使用,節省時間,節省內存空間,沒有則繼續向它的父級查詢,重複步驟,如果沒有繼續向上直到根加載器,如果還是沒有,則會像如圖右邊,當前的類加載器會自己去加載當前的類,如果不能加載,則向下,看子加載器能否加載,直到最下邊,如果還是不能加載,則拋出ClassNotFoundException 異常。這跟我們上邊放出的源碼流程是差不多的。

爲什麼要用雙親委派機制去加載類呢

內存空間是寶貴的,我們沒有必要保存多份同樣的類,所以每次加載類進行查詢是否曾經加載過,也就是避免多個同樣字節碼的加載。

四、類的加載方式

  • 隱式加載:new
    • 這個 方式大家都知道,直接創建類
  • 顯式加載:loadClass,forName

4.1、loadClass和forName的區別

  • Class.forName也是可以對類進行加載的,內部實際調用的方法是 Class.forName(className,true,classloader);
    注意它的第二個參數爲true,這個參數表示是否進行初始化,默認爲true,它會讓jvm對指定的類執行加載、連接、初始化操作。
  • ClassLoader.loadClass:從上邊的加載分析可以知道,它只負責連接,不會進行初始化等操作,所以static這樣的靜態代碼是不會進行初始化的

文章的最後附上一張類的裝載過程便於自己查看:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章