天天看點

JVM - 類加載子系統

學習清單:

深入探讨 Java 類加載器

老大難的 Java ClassLoader 再不了解就老了

好怕怕的類加載器

1. 類加載子系統的作用

類加載過程即是指

JVM

虛拟機把

.class

檔案中類資訊加載進記憶體,并進行解析生成對應的

class

對象的過程,

class

對象就是一份描述

Class

結構的元資訊對象(類模版對象),通過該元資訊對象可以獲知

Class

的結構資訊:如構造函數,屬性和方法等,

Java

允許使用者借由這個

Class

相關的元資訊對象間接調用

Class

對象的功能,這裡就是我們經常能見到的

Class

JVM - 類加載子系統
  • 類加載器子系統負責從檔案系統或者網絡中加載

    class

    檔案,

    class

    檔案在檔案頭有特定的辨別(

    cafe baby

    )
  • ClassLoader

    隻負責class檔案的加載,至于是否能運作,則有

    Execution Engine

    決定(這裡的是否能運作指的是會不會出錯)
  • 加載的類資訊存放在一塊稱為方法區的記憶體空間,除了類的資訊外,方法區中還會存放運作時常量池資訊,可能還包括字元串字面量和數字常量

2. 類的加載過程

JVM - 類加載子系統

2.1 加載

① 加載指的是根據一個類的全限定名将 (class檔案) 定義此類的二進制位元組流讀入記憶體

虛拟機規範并沒有指明二進制檔案是從哪裡擷取,也就是說并不一定是從

class

檔案中擷取,還可以通過以下方式擷取

  • 運作時計算生成

    我們經常使用的動态代理技術就是這樣,在

    java.lang.reflect.Proxy

    中使用

    ProxyGenerator.generateProxyClass

    來為特定接口生成形式為

    *$Proxy

    的代理類的二進制位元組流
  • 由其他檔案生成

    我們用到的JSP檔案也可以生成對應的

    Class

  • ZIP

    包中讀取

    常見的就是

    JAR

    WAR

    格式的包的使用

② 将位元組流所代表的靜态存儲結構轉換為方法區的運作時資料結構(

instanceKlass

位元組碼被加載到方法區,内部采用c++的

instancKlass

來描述java類,他的主要field有

- _java_mirror 即 java 的類鏡像,指向Class對象,例如對 String 來說,就是 String.class,作用是把 klass 暴露給 java 使用
- _super 即父類
- _fields 即成員變量 
- _methods 即方法 
- _constants 即常量池 
- _class_loader 即類加載器 
- _vtable 虛方法表 
- _itable 接口方法表
           
  • jdk1.8是放在了元空間,構成了

    instanceKlass

    的資料結構,這個

    instanceKlass

    是用來描述類的資料結構
  • jdk1.7是放在了堆的永久代

③ 在記憶體中生成一個代表此類的

java.lang.Class

對象,作為通路方法區這些運作時資料結構的入口(Class對象)

Class

對象有指向

Klass

的指針,java并不能直接通路

instanceKlass

,而需要使用該

Class

對象來使用(他就是上面提到的

java_mirror

),想要通路

Klass

對象要先找到

Class

對象,再通過

Class

指向

Klass

的指針通路

instanceKlass

加載完成之後,虛拟機外部的二進制位元組流就按照虛拟機所需要的格式存儲在方法區之中,然後在記憶體中執行個體化一個

java.lang.Class

類型的對象(并沒有明确要在堆中)

hotspot

選擇将

Class

對象存儲在方法區中(這點比較特殊,他雖然是對象,但是存儲在方法區中)持有

instanceKlass

的記憶體位址(

instanceKlass

也持有

_java_mirror

對象的記憶體位址),Java虛拟機規範并沒有明确要求一定要存儲在方法區或堆區中

JVM - 類加載子系統
JVM - 類加載子系統

關于這裡的

instanceKlass

再提一嘴【了解HotSpot虛拟機】對象在jvm中的表示:OOP-Klass模型

HotSpot

是基于

c++

實作,而

c++

是一門面向對象的語言,本身具備面向對象基本特征,是以

Java

中的對象表示,最簡單的做法是為每個

Java

類生成一個

c++

類與之對應。

HotSpot JVM

并沒有這麼做,而是設計了一個

OOP-Klass Model

;這裡的

OOP

指的是

Ordinary Object Pointer

(普通對象指針),它用來表示對象的執行個體資訊,看起來像個指針實際上是藏在指針裡的對象。而

Klass

則包含中繼資料和方法資訊,用來描述Java類。

之是以采用這個模型是因為

HotSopt JVM

的設計者不想讓每個對象中都含有一個

vtable

(虛函數表),是以就把對象模型拆成

klass

oop

,其中

oop

中不含有任何虛函數,而

Klass

就含有虛函數表,可以進行

method dispatch

Klass

簡單的說是

Java

類在

HotSpot

中的

c++

對等體,用來描述

Java

類,一般

jvm

在加載

class

檔案時,會在方法區建立

instanceKlass

,表示其中繼資料,包括常量池、字段、方法等

OOP

則是在Java程式運作過程中new對象時建立的

類的加載由類加載器完成,JVM提供的類加載器叫做系統類加載器,此外還可以通過繼承

ClassLoader

基類來自定義類加載器

相對于類生命周期的其他階段而言,加載階段(準确地說,是加載階段擷取類的二進制位元組流的動作)是可控性最強的階段,因為開發人員既可以使用系統提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載

關于數組類型的加載

數組類本身并不通過類加載器建立,它是由jvm直接建立的,但是的數組的元素類型還是要由類加載器去建立

  • 如果數組的類型是引用類型,就會遞歸加載這個元件類型
  • 如果數組的類型不是引用類型,會把數組标記為和引用類加載器相關聯

2.2 連結

連接配接階段負責把類的二進制資料合并到

JRE

中,其又可分為如下三個階段:

① 校驗

此階段主要確定

Class

檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機的自身安全

為什麼不在剛讀取二進制位元組流後就進行驗證,而是在

Class

對象生成完成了以後再驗證?

深入了解JVM虛拟機這本書說的是加載階段和連接配接階段的部分内容是交叉進行的(比如一部分位元組碼檔案格式驗證工作)

② 準備

為類變量配置設定記憶體,并将其初始化為預設值,這些記憶體都将在方法區中配置設定(此時為預設值,在初始化的時候才會給變量指派)

此時在準備階段過後的初始值為0而不是123;将

value

指派為123的

putstatic

指令是程式被編譯後,存放于類構造器

<client>

方法之中

注意: 這裡不會為執行個體變量配置設定初始化,類變量會配置設定在方法區中,而執行個體變量會随着對象配置設定到堆中,這裡還沒對象,自然不會為執行個體變量配置設定初始化即在方法區中配置設定這些變量所使用的記憶體空間。

注意:

static final

的常量在編譯的使時候就已經配置設定值了,準備階段會顯示初始化

此時

value

的值在準備階段過後就是123

③ 解析

把常量池内的符号引用轉換為直接引用

符号引用: 符号引用是以一組符号來描述所引用的目标,符号可以是任何的字面形式的字面量,隻要不會出現沖突能夠定位到就行,布局和記憶體無關

直接引用: 可以是指向目标的指針,相對偏移量或是一個能間接定位到目标的句柄。如果有了直接引用,那引用的目标必定已經在記憶體中存在

主要有以下四種:

  • 類或接口的解析
  • 字段解析
  • 類方法解析
  • 接口方法解析

假設:一個類有一個靜态變量,該靜态變量是一個自定義的類型,那麼經過解析後,該靜态變量将是一個指針,指向該類在方法區的記憶體位址

2.3 初始化

在前面的類加載過程中,除了在加載階段使用者應用程式可以通過自定義類加載器參與之外,其餘動作都是由虛拟機主導和控制的,到了初始化階段,才開始真正執行Java程式代碼

初始化階段是執行類構造器

<clinit>

方法的過程(注意這不是我們平時自己定義的構造器,構造器在JVM角度是

<init>

方法)

  • <clinit>

    方法是由編譯器自動收集類中的類變量的指派操作和靜态語句塊中的語句合并而成的,收集的順序是由語句在源檔案中出現的順序決定的;
  • 虛拟機會保證

    <clinit>

    方法執行之前,父類的

    <clinit>

    方法已經執行完畢;和執行個體構造器

    <init>()

    不同,不需要去顯示的調用父類的構造方法
  • JVM必須確定一個類在初始化的過程中,如果是多線程需要同時初始化它,僅僅隻能允許其中一個線程對其執行初始化操作,其餘線程必須等待,隻有在活動線程執行完對類的初始化操作之後,才會通知正在等待的其他線程。(是以可以利用靜态内部類實作線程安全的單例模式)
  • 如果一個類中沒有對靜态變量指派也沒有靜态語句塊,那麼編譯器可以不為這個類生成

    <clinit>

    ()方法
  • 靜态語句塊中隻能通路到定義在靜态語句塊之前的變量,定義在它之後的不能。前面的靜态語句塊可以指派,但不能通路
public class ClassInitTest{
	private static int num = 1;
	static{
		num = 2;
		number = 20;
	}
	private static int number = 10;
	public static void main(String[] args){
		System.out.print(number);//10
	}
}
           

之前一直搞不懂在上面代碼中

number = 20;

的指派操作為什麼可以放到

private static int number = 10;

聲明語句的上面,針對上面的這些步驟再看

① 首先在連結的準備階段就會為類變量配置設定記憶體,并将其初始化為預設值,是以這個時候記憶體中就已經有了

number

,且初值為預設值0

② 靜态代碼塊中

number = 20;

的指派操作是在初始化階段進行的,是以合情合理

③ 最後列印結果是10,因為

<clinit>

方法中指令按照語句在源檔案中出現的順序執行

但是下面的代碼是錯的,雖然可以在它聲明之前指派,但是不能在它聲明之前調用

public class ClassInitTest{
	private static int num = 1;
	static{
		num = 2;
		number = 20;
		System.out.print(number);//錯誤,非法的前向引用
	}
	private static int number = 10;
	public static void main(String[] args){
		System.out.print(number);//10
	}
}
           
  • 接口中不能使用靜态語句塊,但仍有變量初始化的指派操作,但是和類不同的是,執行接口的< clinit>()方法不需要先執行父類的,隻有當父類接口中定義的變量被使用的時候才會初始化
2.3.1 java中,對于初始化階段,有且隻有以下六種情況才會對要求類立刻“初始化”(加載,驗證,準備,自然需要在此之前開始)
  1. 遇到

    new

    getstatic

    putstatic

    invokestatic

    指令的時候
  • 使用

    new

    關鍵字執行個體化對象(比如new、反射、序列化)
  • 調用一個類型或接口的靜态字段,或者對這些靜态字段執行指派操作時(即在位元組碼中,執行

    getstatic

    或者

    putstatic

    指令),(被

    final

    修飾的靜态字段除外、編譯器優化時已經放入常量池)
  • 調用一個類型的靜态方法時(即在位元組碼中執行

    invokestatic

    指令)
  1. 初始化一個類的派生類時,先觸發父類的初始化(Java虛拟機規範明确要求初始化一個類時,它的超類必須提前完成初始化操作,接口例外)

    需要注意,這一點對于接口來說,初始化接口不要求其父接口都被初始化,注意在真正使用到父接口(如引用父接口的常量)才會初始化

  2. 使用

    java.lang.reflect

    包的方法進行反射調用的時候,如果類沒有被初始化,則要先初始化。
  3. 虛拟機啟動時,使用者會先初始化要執行的主類(含有

    main

上面稱為對一個類的主動引用,除此之外所有引用類的方法都屬于被動引用,不會觸發初始化

2.3.2 不會引發初始化的幾個場景
  1. 通過子類引用父類的靜态字段,隻會觸發父類的初始化,而不會觸發子類的初始化
public class SuperClass{
	public static int value = 123;
	static{
		System.out.printlin("Super init");
	}
}

public class SubClass extends SuperClass{
	static{
		System.out.printlin("Sub init");
	}
}

public class NotInitialization{
	public static void main(String[] args){
		System.out.printlin(SubClass.val);
		//Super init
		//123
	}
}
           

但是是否會觸發子類的加載和驗證,在虛拟機規範中并未明确規定,這取決于虛拟機的具體實作

  1. 定義對象數組和集合,不會觸發該類的初始化
package org.fenixsoft.classloading;
public class NotInitialization{
	public static void main(String[] args){
		SuperClass[] sups = new SuperClass[10];
	}
}
           

但是會觸發

[Lorg.fenixsoft.classloading.SuperClass

的類的初始化,這個類代表一個元素類型為

org.fenixsoft.classloading.SuperClass

的一位數組,建立動作由位元組碼指令

newArray

觸發,數組中的屬性和方法(

length

clone()

)都實作在這個類裡面;

Java語言對數組的通路比c/c++安全是因為這個類封裝了數組元素的通路方法,而c/c++翻譯為對數組指針的移動

  1. 類A引用類B的

    static final

    常量不會導緻類B初始化(注意靜态常量必須是字面值常量,否則還是會觸發B的初始化)
public class ConstClass{
	static{
		System.out.printlin("ConstClass init");
	}
	public static final String HELLO = "hello";
}
public class NotInitialization{
	public static void main(String[] args){
		System.out.printlin("ConstClass.HELLO");
		//hello
	}
}
           

上面的代碼并沒有輸出

ConstClass init

,因為雖然引用了

ConstClass

類的

HELLO

常量,但是在編譯階段通過常量的傳播優化,以及将此常量的

hello

值存儲到了

NotInitialization

類的常量池中,以後對

ConstClass.Hello

的引用實際都是轉換為對自身常量池的引用

  1. 通過類名擷取Class對象,不會觸發類的初始化。如

    System.out.println(Person.class);

  2. 通過

    Class.forName

    加載指定類時,如果指定參數

    initialize

    false

    時,也不會觸發類初始化
  3. 通過

    ClassLoader

    預設的

    loadClass

    方法,也不會觸發初始化動作

不會導緻類初始化,不代表類不會經曆加載、驗證、準備階段

3. 類加載器

類加載器是負責讀取

Java

位元組代碼,并轉換成

java.lang.Class

類的一個執行個體

把類加載階段的“通過一個類的全限定名來擷取描述此類的二進制位元組流”這個動作交給虛拟機之外的類加載器來完成。這樣的好處在于,我們可以自行實作類加載器來加載其他格式的類,隻要是二進制位元組流就行,這就大大增強了加載器靈活性

3.1 類的唯一性

類加載器除了用于加載類外,還可用于确定類在Java虛拟機中的唯一性

正如一個對象有一個唯一的辨別一樣,一個載入

JVM

的類也有一個唯一的辨別。在Java中,一個類用其全限定類名(包括包名和類名)作為辨別;但在

JVM

中,一個類用其全限定類名和其類加載器作為其唯一辨別。例如,如果在pg的包中有一個名為

Person

的類,被類加載器

ClassLoader

的執行個體

kl

負責加載,則該

Person

類對應的

Class

對象在

JVM

中表示為

(Person.pg.kl)

。這意味着兩個類加載器加載的同名類:

(Person.pg.kl)

(Person.pg.kl2)

是不同的、它們所加載的類也是完全不同、互不相容的

這裡的相同包括Class對象的的

equals

方法,

isInstance

方法的傳回結果,還包括使用

instanceof

關鍵字做對象所屬關系判斷等情況

通俗一點來講,要判斷兩個類是否“相同”,前提是這兩個類必須被同一個類加載器加載,否則這個兩個類不“相同”

3.2 類加載器的分類

JVM規範中是這樣定義類加載器類型的,JVM支援兩種類型的類加載器,分别為

  • 引導類加載器(

    bootstrap classloader

    )
  • 自定義類加載器(

    User-Defined ClassLoader

    )

這樣分類的原因是

bootstrap classloader

是由C++語言編寫的,而其他都是派生于抽象類

classLoader

的java層面實作的類加載器

而我們也可以按照虛拟機自帶的和使用者自定義的為标準來分類

JVM - 類加載子系統

這裡的類加載器之間的父子關系一般不會以繼承的關系來實作,而是以組合的方式來複用父加載器代碼

都含有一個

ClassLoader parent

成員變量,該變量指向其父加載器,類似單向連結清單

① 啟動類加載器 (bootstrap classloader)
  • 這個類加載器使用C/C++語言實作,嵌套在JVM内部
  • 它用來加載 Java 的核心類(

    $JAVA_HOME

    jre/lib/rt.jar

    裡所有的

    class

    )
  • 并不繼承自

    java.lang.ClassLoader

    (C++實作的當然不能繼承自Java的體系結構)
  • 負責裝載

    <Java_Home>/lib

    下面的核心類庫或

    -Xbootclasspath

    選項指定的jar包,隻加載包名為

    java

    javax

    sun

    開頭的類
  • 由于引導類加載器涉及到虛拟機本地實作細節,開發者無法直接擷取到啟動類加載器的引用,是以不允許直接通過引用進行操作

下面程式可以獲得根類加載器所加載的核心類庫,并會看到本機安裝的Java環境變量指定的jdk中提供的核心jar包路徑:

public class ClassLoaderTest {
	public static void main(String[] args) {	
		URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
		for(URL url : urls){
			System.out.println(url.toExternalForm());
		}
	}
}
           
② 擴充類加載器 (ExtClassLoader)
  • 拓展類類加載器,它用來加載

    <JAVA_HOME>/jre/lib/ext

    路徑以及

    java.ext.dirs

    系統變量指定的類路徑下的類
  • 派生自

    ClassLoader

③ 應用程式類加載器 (AppClassLoader)
  • 應用程式類類加載器,它主要加載應用程式

    ClassPath

    下的類(包含jar包中的類)。它是java應用程式預設的類加載器,一般來說,java應用的類都是由他來加載

除了系統提供的類加載器以外,開發人員可以通過繼承

java.lang.ClassLoader

類的方式實作自己的類加載器,以滿足一些特殊的需求

⑤ 自定義類加載器

為什麼要自定義類加載器

把類加載階段的“通過一個類的全限定名來擷取描述此類的二進制位元組流”這個動作交給虛拟機之外的類加載器來完成。這樣的好處在于,我們可以自行實作類加載器來加載其他格式的類,隻要是二進制位元組流就行,這就大大增強了加載器靈活性

  1. 隔離加載類

    如果引入了不同的中間件,他們定義的一些類的名字一樣,路徑一樣,就會出現類的沖突的;這個時候讓他們使用各自的類加載器加載就會實作不同的中間件的隔離

  2. 修改類加載的方式

    可以在需要的時候再動态加載

  3. 拓展加載源

    從其他地方加載二進制檔案

    我們需要的類不一定存放在已經設定好的

    classPath

    下(有系統類加載器

    AppClassLoader

    加載的路徑),對于自定義路徑中的

    class

    類檔案的加載,我們需要自己的

    ClassLoader

  4. 防止源碼洩漏

    有時我們不一定是從類檔案中讀取類,可能是從網絡的輸入流中讀取類,這就需要做一些加密和解密操作,這就需要自己實作加載類的邏輯,當然其他的特殊處理也同樣适用

  5. 可以定義類的實作機制,實作類的熱部署,如

    OSGi

    中的

    bundle

    子產品就是通過實作自己的

    ClassLoader

    實作的
怎麼自定義加載器

ClassLoader

裡面有三個重要的方法

loadClass()

findClass()

defineClass()

loadClass()

方法是加載目标類的入口,它首先會查找目前

ClassLoader

以及它的雙親裡面是否已經加載了目标類,如果沒有找到就會讓雙親嘗試加載,如果雙親都加載不了,就會調用

findClass()

讓自定義加載器自己來加載目标類。

ClassLoader

findClass()

方法是需要子類來覆寫的,不同的加載器将使用不同的邏輯來擷取目标類的位元組碼。拿到這個位元組碼之後再調用

defineClass()

方法将位元組碼轉換成

Class

對象,關于為什麼是讓子類重寫

findClass()

方法而不是直接重寫

loadClass

方法,是因為在

loadClass

方法中有雙親委派機制的邏輯,最後如果雙親都無法加載,在去調用自身的

findClass()

方法

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) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父類加載器無法完成加載請求
            }

            if (c == null) {
                // 父類無法加載再調用自身的findClass方法加載
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
           

findClass

方法中我們實際上需要做的就是讀取以下需要加載的二進制檔案,再交給

defineClass

方法把二進制流位元組組成的檔案轉換為一個

java.lang.Class

public class MyClassLoader extends ClassLoader{
    public MyClassLoader(){
        
    }
    
    public MyClassLoader(ClassLoader parent){
        super(parent);
    }
    
    protected Class<?> findClass(String name) throws ClassNotFoundException{
        File file = getClassFile(name);
        try{
            byte[] bytes = getClassBytes(file);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        } 
        catch (Exception e){
            e.printStackTrace();
        }
        
        return super.findClass(name);
    }
    
    private File getClassFile(String name){
        File file = new File("D:/Person.class");
        return file;
    }
    
    private byte[] getClassBytes(File file) throws Exception{
        // 這裡要讀入.class的位元組,是以要使用位元組流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        
        while (true){
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        
        fis.close();
        
        return baos.toByteArray();
    }
}
           

自定義類加載器不要破壞雙親委派規則,不要輕易覆寫

loadClass

方法。否則可能會導緻自定義加載器無法加載内置的核心類庫。在使用自定義加載器時,要明确好它的父加載器是誰,将父加載器通過子類的構造器傳入。如果父類加載器是

null

,那就表示父加載器是「根加載器」

3.3 雙親委派機制

學習老大難的 Java ClassLoader 再不了解就老了

Java虛拟機對class檔案采用的是按需加載的方式,也就是說當需要使用該類的時候才會将他的Class檔案加載到記憶體中生成class對象;而在加載某個類的class檔案的時候,Java虛拟機采用的是雙親委派機制,即把請求交給父類處理,他是一種任務委派模式

上面提到了雙親委派機制,其實就是,當一個類加載器去加載類時先嘗試讓父類加載器去加載,如果父類加載器加載不了再嘗試自身加載。這也是我們在自定義

ClassLoader

java

官方建議遵守的約定。

JVM - 類加載子系統

AppClassLoader

在加載一個未知的類名時,它并不是立即去搜尋 Classpath,它會首先将這個類名稱交給

ExtensionClassLoader

來加載,如果

ExtensionClassLoader

可以加載,那麼

AppClassLoader

就不用麻煩了。否則它就會搜尋

Classpath

ExtensionClassLoader

在加載一個未知的類名時,它也并不是立即搜尋 ext 路徑,它會首先将類名稱交給

BootstrapClassLoader

來加載,如果

BootstrapClassLoader

可以加載,那麼

ExtensionClassLoader

也就不用麻煩了。否則它就會搜尋 ext 路徑下的 jar 包。這三個

ClassLoader

之間形成了級聯的父子關系,每個

ClassLoader

都很懶,盡量把工作交給父親做,父親幹不了了自己才會幹。每個

ClassLoader

對象内部都會有一個

parent

屬性指向它的父加載器

class ClassLoader {
  ...
  private final ClassLoader parent;
  ...
}
           

值得注意的是圖中的

ExtensionClassLoader

parent

指針畫了虛線,這是因為它的

parent

的值是

null

,當

parent

字段是

null

時就表示它的父加載器是「根加載器」。如果某個

Class

對象的

classLoader

屬性值是

null

,那麼就表示這個類也是「根加載器」加載的

這種機制有以下好處:

① 避免類的重複加載

② 保護程式安全,防止核心API被随用篡改(沙箱安全機制)

對于第一點,假設有以下的場景,兩個類A和類B都要加載

System

類:

  • 如果不用委托而是自己加載自己的,那麼類A就會加載一份

    System

    位元組碼,然後類B又會加載一份

    System

    位元組碼,這樣記憶體中就出現了兩份

    System

    位元組碼
  • 如果使用委托機制,會遞歸的向父類查找,也就是首選用

    Bootstrap

    嘗試加載,如果找不到再向下。這裡的

    System

    就能在

    Bootstrap

    中找到然後加載,類加載器在成功加載某個類之後,會把得到的

    java.lang.Class

    類的執行個體緩存起來。下次再請求加載該類的時候,類加載器會直接使用緩存的類的執行個體,而不會嘗試再次加載。也就是說,對于一個類加載器執行個體來說,相同全名的類隻加載一次,即

    loadClass

    方法不會被重複調用,如果此時類B也要加載

    System

    ,也從

    Bootstrap

    開始,此時

    Bootstrap

    發現已經加載過了

    System

    那麼直接傳回記憶體中的

    System

    即可而不需要重新加載,這樣記憶體中就隻有一份

    System

    的位元組碼了

對于第二點,假設有以下的場景,我們自己寫個類叫

java.lang.System

類加載采用委托機制,這樣可以保證爸爸們優先,爸爸們能找到的類,兒子就沒有機會加載。而

System

類是

Bootstrap

加載器加載的,就算自己重寫,也總是使用

Java

系統提供的

System

,自己寫的

System

類根本沒有機會得到加載,保護程式安全,防止核心API被随用篡改

3.4 雙親委派機制的缺陷及打破

淺談雙親委派機制的缺陷及打破雙親委派機制

以JDBC為例談雙親委派模型的破壞

3.4.1 缺陷

由于

BootstrapClassloader

是頂級類加載器,

BootstrapClassloader

無法委派

AppClassLoader

來加載類,也就是說

BootstrapClassloader

中加載的類中無法使用由

AppClassLoader

加載的類。可能絕大部分情況這個不算是問題,因為

BootstrapClassloader

加載的都是基礎類,供

AppClassLoader

加載的類調用的類。但是萬事萬物都不是絕對的,比如經典的

JAVA SPI

機制

3.4.2 雙親委派機制的打破

雙親委派模型主要出現過三次大規模被破壞的情況

這裡大緻說一下

JAVA SPI

機制為什麼要打破,并且是如何使用線程上下文類加載器打破雙親委派機制的

下面這段話引自真正了解線程上下文類加載器

Java 提供了很多服務提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實作。常見的SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

這些 SPI 的接口由 Java 核心庫來提供,而這些 SPI 的實作代碼則是作為 Java 應用所依賴的 jar包被包含進類路徑(

CLASSPATH

)裡。SPI接口中的代碼經常需要加載具體的實作類。那麼問題來了,SPI的接口是Java核心庫的一部分,是由**啟動類加載器(

BootstrapClassloader

)來加載的;SPI的實作類是由系統類加載器(

SystemClassLoader

)**來加載的。引導類加載器是無法找到 SPI的實作類的,因為依照雙親委派模型,

BootstrapClassloader

無法委派

AppClassLoader

來加載類。

而線程上下文類加載器破壞了“雙親委派模型”,可以在執行線程中抛棄雙親委派加載鍊模式,使程式可以逆向使用類加載器。

具體看這篇JDBC詳解

3.5 分工與合作

這裡我們重新了解一下

ClassLoader

的意義,它相當于類的命名空間,起到了類隔離的作用。位于同一個

ClassLoader

裡面的類名是唯一的,不同的

ClassLoader

可以持有同名的類。

ClassLoader

是類名稱的容器,是類的沙箱

不同的

ClassLoader

之間也會有合作,它們之間的合作是通過

parent

屬性和雙親委派機制來完成的。

parent

具有更高的加載優先級。除此之外,

parent

還表達了一種共享關系,當多個子

ClassLoader

共享同一個

parent

時,那麼這個

parent

裡面包含的類可以認為是所有子

ClassLoader

共享的。這也是為什麼

BootstrapClassLoader

被所有的類加載器視為祖先加載器,JVM 核心類庫自然應該被共享

3.6

Class.forName

這是手動加載類的常見方式

但是他是使用哪個類加載器來加載的呢?看一下方法的具體實作

public static Class<?> forName(String className)
            throws ClassNotFoundException {
    // 使用native方法擷取調用類的Class對象
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
           

其中

getClassLoader(caller)

設定了所使用的類加載器,繼續看其實作:

static ClassLoader getClassLoader(Class<?> caller) {
     if (caller == null) {
         return null;
     }
     return caller.getClassLoader0();
 }
}
           

這段代碼的官方注解是“傳回caller的類加載器”,即

native

方法

getClassLoader0()

傳回調用者的類加載器。也就是說假設在A類裡執行

forName(String className)

,那麼所使用的

ClassLoader

就是加載A的

ClassLoader

forName

還提供了多參數版本,可以指定使用哪個 ClassLoader 來加載

通過這種形式的

forName

方法可以突破内置加載器的限制,通過使用自定類加載器允許我們自由加載其它任意來源的類庫。根據

ClassLoader

的傳遞性,目标類庫傳遞引用到的其它類庫也将會使用自定義加載器加載

3.7 鑽石依賴

項目管理上有一個著名的概念叫着「鑽石依賴」,是指軟體依賴導緻同一個軟體包的兩個版本需要共存而不能沖突

我們平時使用的

maven

是這樣解決鑽石依賴的,它會從多個沖突的版本中選擇一個來使用,如果不同的版本之間相容性很糟糕,那麼程式将無法正常編譯運作。Maven 這種形式叫「扁平化」依賴管理

使用

ClassLoader

可以解決鑽石依賴問題。不同版本的軟體包使用不同的

ClassLoader

來加載,位于不同

ClassLoader

中名稱一樣的類實際上是不同的類,上面提到過

ClassLoader

固然可以解決依賴沖突問題,不過它也限制了不同軟體包的操作界面必須使用反射或接口的方式進行動态調用。

Maven

沒有這種限制,它依賴于虛拟機的預設懶惰加載政策,運作過程中如果沒有顯示使用定制的

ClassLoader

,那麼從頭到尾都是在使用

AppClassLoader

,而不同版本的同名類必須使用不同的

ClassLoader

加載,是以

Maven

不能完美解決鑽石依賴

繼續閱讀