JVM系列(五) - JVM類加載機制詳解

前言

本文將由淺及深,介紹Java類加載的過程和原理,進一步對類加載器的進行源碼分析,完成一個自定義的類加載器。java

正文

(一). 類加載器是什麼

類加載器簡言之,就是用於把.class文件中的字節碼信息轉化爲具體的java.lang.Class對象的過程的工具。編程

具體過程:後端

  1. 在實際類加載過程當中,JVM會將全部的.class字節碼文件中的二進制數據讀入內存中,導入運行時數據區的方法區中。
  2. 當一個類首次被主動加載被動加載時,類加載器會對此類執行類加載的流程 – 加載鏈接驗證準備解析)、初始化
  3. 若是類加載成功,堆內存中會產生一個新的Class對象,Class對象封裝了類在方法區內的數據結構

Class對象的建立過程描述:數組

(二). 類加載的過程

類加載的過程分爲三個步驟(五個階段) :加載 -> 鏈接驗證準備解析)-> 初始化緩存

加載驗證準備初始化這四個階段發生的順序是肯定的,而解析階段能夠在初始化階段以後發生,也稱爲動態綁定晚期綁定數據結構

類加載的過程描述:
多線程

1. 加載

加載:查找並加載類的二進制數據的過程。架構

加載的過程描述:

  1. 經過類的全限定名定位.class文件,並獲取其二進制字節流
  2. 把字節流所表明的靜態存儲結構轉換爲方法區運行時數據結構
  3. Java中生成一個此類的java.lang.Class對象,做爲方法區中這些數據的訪問入口

2. 鏈接

鏈接:包括驗證準備解析三步。框架

a). 驗證

驗證:確保被加載的類的正確性。驗證是鏈接階段的第一步,用於確保Class字節流中的信息是否符合虛擬機的要求。異步

具體驗證形式:

  1. 文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。
  2. 元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object以外。
  3. 字節碼驗證:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。
  4. 符號引用驗證:確保解析動做能正確執行。

b). 準備

準備:爲類的靜態變量分配內存,並將其初始化爲默認值。準備過程一般分配一個結構用來存儲類信息,這個結構中包含了類中定義的成員變量方法接口信息等。

具體行爲:

  1. 這時候進行內存分配的僅包括類變量(static),而不包括實例變量實例變量會在對象實例化時隨着對象一塊分配在Java中。
  2. 這裏所設置的初始值一般狀況下是數據類型默認的零值(如00Lnullfalse等),而不是被在Java代碼中被顯式賦值

c). 解析

解析:把類中對常量池內的符號引用轉換爲直接引用

解析動做主要針對類或接口字段類方法接口方法方法類型方法句柄調用點限定符等7類符號引用進行。

3. 初始化

初始化:對類靜態變量賦予正確的初始值 (注意和鏈接時的解析過程區分開)。

初始化的目標

  1. 實現對聲明類靜態變量時指定的初始值的初始化;
  2. 實現對使用靜態代碼塊設置的初始值的初始化。

初始化的步驟

  1. 若是此類沒被加載鏈接,則先加載鏈接此類;
  2. 若是此類的直接父類還未被初始化,則先初始化其直接父類;
  3. 若是類中有初始化語句,則按照順序依次執行初始化語句。

初始化的時機

  1. 建立類的實例(new關鍵字);
  2. java.lang.reflect包中的方法(如:Class.forName(「xxx」));
  3. 對類的靜態變量進行訪問或賦值;
  4. 訪問調用類的靜態方法
  5. 初始化一個類的子類父類自己也會被初始化;
  6. 做爲程序的啓動入口,包含main方法(如:SpringBoot入口類)。

(三). 類的主動引用和被動引用

主動引用

主動引用:在類加載階段,只執行加載鏈接操做,不執行初始化操做。

主動引用的幾種形式

  1. 建立類的實例(new關鍵字);
  2. java.lang.reflect包中的方法(如:Class.forName(「xxx」));
  3. 對類的靜態變量進行訪問或賦值;
  4. 訪問調用類的靜態方法
  5. 初始化一個類的子類父類自己也會被初始化;
  6. 做爲程序的啓動入口,包含main方法(如:SpringBoot入口類)。

主動引用1 - main方法在初始類中

代碼示例:

1
2
3
4
5
6
7
8
9
public class OptimisticReference0 {
static {
System.out.println(OptimisticReference0.class.getSimpleName() + " is referred!");
}

public static void main(String[] args) {
System.out.println();
}
}

 

運行結果:

OptimisticReference0 is referred!

主動引用2 – 建立子類會觸發父類的初始化

代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class OptimisticReference1 {
public static class Parent {
static {
System.out.println(Parent.class.getSimpleName() + " is referred!");
}
}

public static class Child extends Parent {
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}

public static void main(String[] args) {
new Child();
}
}

 

運行結果:

Parent is referred!
Child is referred!

主動引用3 – 訪問一個類靜態變量

代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class OptimisticReference2 {
public static class Child {
protected static String name;
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
name = "Child";
}
}

public static void main(String[] args) {
System.out.println(Child.name);
}
}

 

運行結果:

Child is referred!
Child

主動引用4 – 對類的靜態變量進行賦值

代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
public class OptimisticReference3 {
public static class Child {
protected static String name;
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}

public static void main(String[] args) {
Child.name = "Child";
}
}

 

運行結果:

Child is referred!

主動引用5 – 使用java.lang.reflect包提供的反射機制

代碼示例:

1
2
3
4
5
public class OptimisticReference4 {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("org.ostenant.jdk8.learning.examples.reference.optimistic.Child");
}
}

運行結果:

Child is referred!

被動引用

被動引用: 在類加載階段,會執行加載鏈接初始化操做。

被動引用的幾種形式:

  1. 經過子類引用父類的的靜態字段,不會致使子類初始化;
  2. 定義類的數組引用不賦值,不會觸發此類的初始化;
  3. 訪問類定義的常量,不會觸發此類的初始化。

被動引用1 – 子類引用父類的的靜態字段,不會致使子類初始化

代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class NegativeReference0 {
public static class Parent {
public static String name = "Parent";
static {
System.out.println(Parent.class.getSimpleName() + " is referred!");
}
}

public static class Child extends Parent {
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}

public static void main(String[] args) {
System.out.println(Child.name);
}
}

 

運行結果:

Parent is referred!
Parent

被動引用2 – 定義類的數組引用而不賦值,不會觸發此類的初始化

代碼示例:

1
2
3
4
5
6
7
8
9
10
11
public class NegativeReference1 {
public static class Child {
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}

public static void main(String[] args) {
Child[] childs = new Child[10];
}
}

 

運行結果:

無輸出

被動引用3 – 訪問類定義的常量,不會觸發此類的初始化

示例代碼:

1
2
3
4
5
6
7
8
9
10
11
12
public class NegativeReference2 {
public static class Child {
public static final String name = "Child";
static {
System.out.println(Child.class.getSimpleName() + " is referred!");
}
}

public static void main(String[] args) {
System.out.println(Child.name);
}
}

 

運行結果:

Child

(四). 三種類加載器

類加載器:類加載器負責加載程序中的類型(類和接口),並賦予惟一的名字予以標識。

類加載器的組織結構

類加載器的關係

  1. Bootstrap Classloader 是在Java虛擬機啓動後初始化的。
  2. Bootstrap Classloader 負責加載 ExtClassLoader,而且將 ExtClassLoader的父加載器設置爲 Bootstrap Classloader
  3. Bootstrap Classloader 加載完 ExtClassLoader 後,就會加載 AppClassLoader,而且將 AppClassLoader 的父加載器指定爲 ExtClassLoader

類加載器的做用

Class Loader 實現方式 具體實現類 負責加載的目標
Bootstrap Loader C++ 由C++實現 %JAVA_HOME%/jre/lib/rt.jar以及-Xbootclasspath參數指定的路徑以及中的類庫
Extension ClassLoader Java sun.misc.Launcher$ExtClassLoader %JAVA_HOME%/jre/lib/ext路徑下以及java.ext.dirs系統變量指定的路徑中類庫
Application ClassLoader Java sun.misc.Launcher$AppClassLoader Classpath以及-classpath-cp指定目錄所指定的位置的類或者是jar文檔,它也是Java程序默認的類加載器

類加載器的特色

  • 層級結構:Java裏的類裝載器被組織成了有父子關係的層級結構。Bootstrap類裝載器是全部裝載器的父親。
  • 代理模式: 基於層級結構,類的代理能夠在裝載器之間進行代理。當裝載器裝載一個類時,首先會檢查它在父裝載器中是否進行了裝載。若是上層裝載器已經裝載了這個類,這個類會被直接使用。反之,類裝載器會請求裝載這個類
  • 可見性限制:一個子裝載器能夠查找父裝載器中的類,可是一個父裝載器不能查找子裝載器裏的類。
  • 不容許卸載:類裝載器能夠裝載一個類可是不能夠卸載它,不過能夠刪除當前的類裝載器,而後建立一個新的類裝載器裝載。

類加載器的隔離問題

每一個類裝載器都有一個本身的命名空間用來保存已裝載的類。當一個類裝載器裝載一個類時,它會經過保存在命名空間裏的類全侷限定名(Fully Qualified Class Name) 進行搜索來檢測這個類是否已經被加載了。

JVM 及 Dalvik 對類惟一的識別是 ClassLoader id + PackageName + ClassName,因此一個運行程序中是有可能存在兩個包名類名徹底一致的類的。而且若是這兩個不是由一個 ClassLoader 加載,是沒法將一個類的實例強轉爲另一個類的,這就是 ClassLoader 隔離性。

爲了解決類加載器的隔離問題JVM引入了雙親委託機制

(五). 雙親委託機制

核心思想:其一,自底向上檢查類是否已加載;其二,自頂向下嘗試加載類

具體加載過程

  1. AppClassLoader加載一個class時,它首先不會本身去嘗試加載這個類,而是把類加載請求委派父類加載器ExtClassLoader去完成。
  2. ExtClassLoader加載一個class時,它首先也不會本身去嘗試加載這個類,而是把類加載請求委派BootStrapClassLoader去完成。
  3. 若是BootStrapClassLoader加載失敗(例如在%JAVA_HOME%/jre/lib裏未查找到該class),會使用ExtClassLoader來嘗試加載;
  4. 若是ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,若是AppClassLoader也加載失敗,則會報出異常ClassNotFoundException

源碼分析

ClassLoader.class

  1. loadClass():經過指定類的全限定名稱,由類加載器檢測裝載建立並返回該類的java.lang.Class對象。

    ClassLoader經過loadClass()方法實現了雙親委託機制,用於類的動態加載

loadClass()自己是一個遞歸向上調用的過程。

  • 自底向上檢查類是否已加載

    1. 先經過findLoadedClass()方法從最底端類加載器開始檢查類是否已經加載。
    2. 若是已經加載,則根據resolve參數決定是否要執行鏈接過程,並返回Class對象。
    3. 若是沒有加載,則經過parent.loadClass()委託其父類加載器執行相同的檢查操做(默認不作鏈接處理)。
    4. 直到頂級類加載器,即parent爲空時,由findBootstrapClassOrNull()方法嘗試到Bootstrap ClassLoader中檢查目標類。
  • 自頂向下嘗試加載類

    1. 若是仍然沒有找到目標類,則從Bootstrap ClassLoader開始,經過findClass()方法嘗試到對應的類目錄下去加載目標類。
    2. 若是加載成功,則根據resolve參數決定是否要執行鏈接過程,並返回Class對象。
    3. 若是加載失敗,則由其子類加載器嘗試加載,直到最底端類加載器也加載失敗,最終拋出ClassNotFoundException
  1. findLoadedClass()

    查找當前類加載器的緩存中是否已經加載目標類。findLoadedClass()實際調用了底層的native方法findLoadedClass0()

  1. findBootstrapClassOrNull()

    查找最頂端Bootstrap類加載器的是否已經加載目標類。一樣,findBootstrapClassOrNull()實際調用了底層的native方法findBootstrapClass()

  1. findClass()

    ClassLoaderjava.lang包下的抽象類,也是全部類加載器(除了Bootstrap)的基類,findClass()ClassLoader對子類提供的加載目標類的抽象方法。

    注意Bootstrap ClassLoader並不屬於JVM的層次,它不遵照ClassLoader的加載規則,Bootstrap classLoader並無子類。

  1. defineClass()

    defineClass()ClassLoader向子類提供的方法,它能夠將.class文件的二進制數據轉換爲合法的java.lang.Class對象。

(六). 類的動態加載

類的幾種加載方式

  • 經過命令行啓動時由JVM初始化加載;
  • 經過Class.forName()方法動態加載;
  • 經過ClassLoader.loadClass()方法動態加載。

Class.forName()和ClassLoader.loadClass()

  • Class.forName():把類的.class文件加載到JVM中,對類進行解釋的同時執行類中的static靜態代碼塊
  • ClassLoader.loadClass():只是把.class文件加載到JVM中,不會執行static代碼塊中的內容,只有在newInstance纔會去執行。

(七). 對象的初始化

對象的初始化順序

靜態變量/靜態代碼塊 -> 普通代碼塊 -> 構造函數

  1. 父類靜態變量靜態代碼塊(先聲明的先執行);
  2. 子類靜態變量靜態代碼塊(先聲明的先執行);
  3. 父類普通成員變量普通代碼塊(先聲明的先執行);
  4. 父類的構造函數
  5. 子類普通成員變量普通代碼塊(先聲明的先執行);
  6. 子類的構造函數

對象的初始化示例

Parent.java

Children.java

Tester.java

測試結果:

測試結果代表:JVM在建立對象時,遵照以上對象的初始化順序。

(八). 自定義類加載器

編寫本身的類加載器

在源碼分析階段,咱們已經解讀了如何實現自定義類加載器,如今咱們開始本身的類加載器。

Step 1:定義待加載的目標類Parent.javaChildren.java

Parent.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package org.ostenant.jdk8.learning.examples.classloader.custom;

public class Parent {
protected static String CLASS_NAME;
protected static String CLASS_LOADER_NAME;
protected String instanceID;

// 1.先執行靜態變量和靜態代碼塊(只在類加載期間執行一次)
static {
CLASS_NAME = Parent.class.getName();
CLASS_LOADER_NAME = Parent.class.getClassLoader().toString();
System.out.println("Step a: " + CLASS_NAME + " is loaded by " + CLASS_LOADER_NAME);
}

// 2.而後執行變量和普通代碼塊(每次建立實例都會執行)
{
instanceID = this.toString();
System.out.println("Step c: Parent instance is created: " + CLASS_LOADER_NAME + " -> " + instanceID);
}

// 3.而後執行構造方法
public Parent() {
System.out.println("Step d: Parent instance:" + instanceID + ", constructor is invoked");
}

public void say() {
System.out.println("My first class loader...");
}
}

 

Children.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.ostenant.jdk8.learning.examples.classloader.custom;

public class Children extends Parent {
static {
CLASS_NAME = Children.class.getName();
CLASS_LOADER_NAME = Children.class.getClassLoader().toString();
System.out.println("Step b: " + CLASS_NAME + " is loaded by " + CLASS_LOADER_NAME);
}

{
instanceID = this.toString();
System.out.println("Step e: Children instance is created: " + CLASS_LOADER_NAME + " -> " + instanceID);
}

public Children() {
System.out.println("Step f: Children instance:" + instanceID + ", constructor is invoked");
}

public void say() {
System.out.println("My first class loader...");
}
}

 

Step 2:實現自定義類加載器CustomClassLoader

CustomClassLoader.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class CustomClassLoader extends ClassLoader {
private String classPath;

public CustomClassLoader(String classPath) {
this.classPath = classPath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name); // 可省略
if (c == null) {
byte[] data = loadClassData(name);
if (data == null) {
throw new ClassNotFoundException();
}
return defineClass(name, data, 0, data.length);
}
return null;
}

protected byte[] loadClassData(String name) {
try {
// package -> file folder
name = name.replace(".", "//");
FileInputStream fis = new FileInputStream(new File(classPath + "//" + name + ".class"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len = -1;
byte[] b = new byte[2048];
while ((len = fis.read(b)) != -1) {
baos.write(b, 0, len);
}
fis.close();
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}

 

Step 3:測試類加載器的加載過程

CustomerClassLoaderTester.java

  1. 測試程序啓動時,逐一拷貝加載待加載的目標類源文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    private static final String CHILDREN_SOURCE_CODE_NAME = SOURCE_CODE_LOCATION + "Children.java";
    private static final String PARENT_SOURCE_CODE_NAME = SOURCE_CODE_LOCATION + "Parent.java";
    private static final List<String> SOURCE_CODE = Arrays.asList(CHILDREN_SOURCE_CODE_NAME, PARENT_SOURCE_CODE_NAME);

    static {
    SOURCE_CODE.stream().map(path -> new File(path))
    // 路徑轉文件對象
    .filter(f -> !f.isDirectory())
    // 文件遍歷
    .forEach(f -> {
    // 拷貝後源代碼
    File targetFile = copySourceFile(f);
    // 編譯源代碼
    compileSourceFile(targetFile);
    });
    }
  2. 拷貝單一源文件到自定義類加載器的類加載目錄

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    protected static File copySourceFile(File f) {
    BufferedReader reader = null;
    BufferedWriter writer = null;
    try {
    reader = new BufferedReader(new FileReader(f));
    // package ...;
    String firstLine = reader.readLine();

    StringTokenizer tokenizer = new StringTokenizer(firstLine, " ");
    String packageName = "";
    while (tokenizer.hasMoreElements()) {
    String e = tokenizer.nextToken();
    if (e.contains("package")) {
    continue;
    } else {
    packageName = e.trim().substring(0, e.trim().length() - 1);
    }
    }

    // package -> path
    String packagePath = packageName.replace(".", "//");
    // java file path
    String targetFileLocation = TARGET_CODE_LOCALTION + "//" + packagePath + "//";

    String sourceFilePath = f.getPath();
    String fileName = sourceFilePath.substring(sourceFilePath.lastIndexOf("\\") + 1);

    File targetFile = new File(targetFileLocation, fileName);
    File targetFileLocationDir = new File(targetFileLocation);
    if (!targetFileLocationDir.exists()) {
    targetFileLocationDir.mkdirs();
    }
    // writer
    writer = new BufferedWriter(new FileWriter(targetFile));
    // 寫入第一行
    writer.write(firstLine);
    writer.newLine();
    writer.newLine();

    String input = "";
    while ((input = reader.readLine()) != null) {
    writer.write(input);
    writer.newLine();
    }

    return targetFile;
    } catch (FileNotFoundException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    } finally {
    try {
    reader.close();
    writer.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    return null;
    }
  3. 對拷貝後的.java源文件執行手動編譯,在同級目錄下生成.class文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    protected static void compileSourceFile(File f) {
    try {
    JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
    StandardJavaFileManager standardFileManager = javaCompiler.getStandardFileManager(null, null, null);
    Iterable<? extends JavaFileObject> javaFileObjects = standardFileManager.getJavaFileObjects(f);

    // 執行編譯任務
    CompilationTask task = javaCompiler.getTask(null, standardFileManager, null, null, null, javaFileObjects);
    task.call();
    standardFileManager.close();

    } catch (Exception e) {
    e.printStackTrace();
    }
    }
  4. 經過自定義類加載器加載Childrenjava.lang.Class<?>對象,而後用反射機制建立Children的實例對象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Test
    public void test() throws Exception {
    // 建立自定義類加載器
    CustomClassLoader classLoader = new CustomClassLoader(TARGET_CODE_LOCALTION); // E://myclassloader//classpath
    // 動態加載class文件到內存中(無鏈接)
    Class<?> c = classLoader.loadClass("org.ostenant.jdk8.learning.examples.classloader.custom.Children");
    // 經過反射拿到全部的方法
    Method[] declaredMethods = c.getDeclaredMethods();
    for (Method method : declaredMethods) {
    if ("say".equals(method.getName())) {
    // 經過反射拿到children對象
    Object children = c.newInstance();
    // 調用children的say()方法
    method.invoke(children);
    break;
    }
    }
    }

測試編寫的類加載器

(一). 測試場景一

  1. 保留static代碼塊,把目標類Children.javaParent.java拷貝到類加載的目錄,而後進行手動編譯
  2. 保留測試項目目錄中的目標類Children.javaParent.java

測試結果輸出:

測試結果分析:

咱們成功建立了Children對象,並經過反射調用了它的say()方法。
然而查看控制檯日誌,能夠發現類加載使用的仍然是AppClassLoaderCustomClassLoader並無生效。

查看CustomClassLoader的類加載目錄:

類目錄下有咱們拷貝編譯ParentChidren文件。

分析緣由:

因爲項目空間中的Parent.javaChildren.java,在拷貝後並無移除。致使AppClassLoader優先在其Classpath下面找到併成功加載了目標類。

(二). 測試場景二

  1. 註釋掉static代碼塊(類目錄下有已編譯的目標類.class文件)。
  2. 移除測試項目目錄中的目標類Children.javaParent.java

測試結果輸出:

測試結果分析:

咱們成功經過自定義類加載器加載了目標類。建立了Children對象,並經過反射調用了它的say()方法。

至此,咱們本身的一個簡單的類加載器就完成了!

參考書籍

周志明,深刻理解Java虛擬機:JVM高級特性與最佳實踐,機械工業出版社


歡迎關注技術公衆號: 零壹技術棧

零壹技術棧零壹技術棧

本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。

相關文章
相關標籤/搜索