文章目錄
一、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這樣的靜態代碼是不會進行初始化的
文章的最後附上一張類的裝載過程便於自己查看: