1、類加載器
1.1 類加載的概念
要了解雙親委派模型,首先我們需要知道java的類加載器。所謂類加載器就是通過一個類的全限定名來擷取描述此類的二進制位元組流,然後把這個位元組流加載到虛拟機中,擷取響應的java.lang.Class類的一個執行個體。我們把實作這個動作的代碼子產品稱為“類加載器”。
1.2 類與類加載器
對于任意的一個類,都需要由加載它的類加載器和這個類本身一同建立其在Java虛拟機中的唯一性,每個類加載器,都擁有一個獨立的類名稱空間,即:比較兩個類是否“相等”,隻有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類源于同一個Class檔案,被同一個虛拟機加載,隻要加載它們的類加載器不同,那這兩個類就必定不想等。
package com.demo.test;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* @author lxc
* @createTime 2023-02-09 10:20
* @description
*/
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
ClassLoader myloader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
try {
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
Object obj = myloader.loadClass("com.demo.test.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof ClassLoaderTest);
Class<?> clazz = Class.forName("com.demo.test.ClassLoaderTest");
Constructor<?> constructor = clazz.getConstructor();
Object obj1 = constructor.newInstance();
System.out.println(obj1 instanceof ClassLoaderTest);
}
}
運作結果如下:
obj對象的Class:class com.demo.test.ClassLoaderTest
obj1對象的Class:class com.demo.test.ClassLoaderTest
false
true
從運作結果來看,第一句和第二句來看,兩個對象都是由類class com.demo.test.ClassLoaderTest執行個體化出來的對象;
從第三句可以看出,這個對象與類class com.demo.test.ClassLoaderTest做所屬類型檢查時卻傳回了false,這是因為虛拟機中存在了兩個ClassLoaderTest,一個是由我們自定義的類加載器加載的,
另一個是由系統應用程式類加載器加載的,雖然都來自同一個Class檔案,但依然是兩個獨立的類,做對象所屬類型檢查時結果為false。
2、雙親委派模型
2.1 類加載器的分類
從虛拟機的角度來看,存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實作,是虛拟機自身的一部分;另一個就是所有的其他類加載器,這些類加載器都是由Java語言實作,獨立于虛拟機外部,并且全都繼承字抽象類java.lang.ClassLoader。
從開發者角度來看,類加載器可以分為以下四類:啟動類加載器(Bootstrap ClassLoader)、擴充類加載器(Extension ClassLoader)、應用程式類加載(Application ClassLoader)和自定義類加載器。
- 啟動類加載器(Bootstrap ClassLoader):這個類加載器是加載核心java庫,負責将<JAVA_HOME>/jre/lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,并且是虛拟機識别的(僅僅按照檔案名識别,如rt.jar,名字不符合的類庫即使放在lib目錄下也不會被加載)類庫加載到虛拟機記憶體中。開發者不能直接使用啟動類加載器。
- 擴充類加載器(Extension ClassLoader):這個類加載器是由sun.misc.Launcher$ExtClassLoader實作,它負責加載<JAVA_HOME>/jre/lib/ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用該類加載器。
-
應用程式類加載器(Application ClassLoader):這個類加載器是由sun.misc.Launcher$AppClassLoader實作。這個類加載器是ClassLoader中的getSystemClassLoader()方法的傳回值,是以一般也稱之為系統類加載器。它負責加載使用者類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程式中沒有定義過自己的類加載器,一般情況下這就是程式中預設的類加載器。
通過上面的分類我們可以看到,這三種類加載器隻能加載各自所負責的目錄下的類庫,而不能加載超過其目錄範圍的類庫,這也就是我們常常說的雙親委派模型中的可見性原則。
我們平時所寫的應用程式都是由這三種類加載器互相配合進行加載的,如果有必要,還可以加上自己定的類加載器。
2.2 雙親委派模型中各類加載器之間的層次關系
類加載器之間這種層次關系,我們稱之為類加載的雙親委派模型。雙親委派模型中,除了頂層的啟動類加載器外,其餘的類加載器都應當有自己的父-類加載。這裡的父子關系不是以繼承關系來實作的,而都是使用組合的關系來複用父-類加載的代碼。
2.3 雙親委派模型中類加載的工作過程
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父-類加載去完成,每一個層次的類加載器(啟動類加載器除外)都是如此,是以所有的加載請求最終都應該傳送到頂層的啟動類加載器中,隻有當父-類加載器回報自己無法完成這個加載請求時,子-類加載器才會嘗試自己去加載。
下面我們看段源碼,從代碼角度看一下這個工作過程:
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
//如果父-類加載器抛出異常,說明父-類加載無法完成加載工作
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//在父-類加載器無法完成加載的時候,再調用本身的findClass方法來進行類加載
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的穩定運作很重要,但從上面的源碼來看,實作還是比較簡單的,雙親委派模型的核心代碼主要都在java.lang.ClassLoader的loadClass()方法中,大體邏輯如下:
先檢查是否已經被加載過,若沒有加載則調用父-類加載的loadClass()方法,若父類加載為空則預設使用啟動類加載器做父-類加載器加載。如果父-類加載器加載失敗,抛出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。
2.4 雙親委派模型的目的
那麼我們思考一下,java為什麼采用雙親委派模型呢?從上面雙親委派模型的工作過程,我們看出,java類随着它的類加載器一起具備了帶有優先級的層次關系。例如類java.lang.Integer,它存放在核心包rt.jar中,那麼無論哪一個類加載器要加載這個類,最終都
是要委派給處于最頂端的啟動類加載器進行加載,進而是的Integer類在程式的各中類加載器環境中都是同一個類;相反,如果沒有使用雙親委派模型,而是由各個類加載器自行去加載的話,當使用者自己編寫了一個名為java.lang.Integer類并放到ClassPath中,那麼系統将會出現多個不同的Integer類,這樣就會造成java體系中最基礎的行為都無法保證(連最基本的類型都不唯一),程式将變得一片混亂。你可能會說,我自定義一個類加載去加載java.lang.Integer,直接重寫loadClass方法,進而破壞掉雙親委派模型不就行了。
我們寫個簡單的例子試下。
public class MyClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String className = null;
if (name.startsWith("java.lang")) {
className = "/" + name.replace(".", "/") + ".class";
} else {
className = name.substring(name.lastIndexOf(".") + 1) + ".class";
}
InputStream is = getClass().getResourceAsStream(className);
if (is == null) {
return super.loadClass(name);
}
try {
byte[] bytes = new byte[is.available()];
is.read(bytes);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoader myLoader = new MyClassLoader();
Object obj = myLoader.loadClass("java.lang.Integer").newInstance();
System.out.println(obj);
}
}
結果輸出:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.demo.test.MyClassLoader.loadClass(MyClassLoader.java:28)
at com.demo.test.MyClassLoader.main(MyClassLoader.java:36)
從源碼分析來看,主要是defineClass方法調用的preDefineClass方法異常,在preDefineClass這個方法中我們看到:
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
即如果是以java.開頭的包下的類,都隻能用啟動類加載器來加載。
2.5 雙親委派模型的三個原則
雙親委派模型有三個基本原則:委托性、可見性和唯一性原則。
- 委托性原則:當子類加載器收到類加載請求時,會将加載請求向上委托給父類加載器;
- 可見性原則:每種類加載器都有自己可加載類庫的範圍,超出這個範圍是不可見的,即無法加載的;
- 唯一性原則:這是雙親委派模型的核心,也是最重要的目的。
2.6 為什麼要打破雙親委派模型
我們這裡主要說一下JDBC為什麼要打破雙親委派模型,其他的方面我後續再分析。
我們以mysql資料庫驅動為例來說明。最早我們使用mysql資料庫驅動的時候,一般是這樣寫代碼:
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://host:port/dbname?useUnicode=true&characterEncoding=utf-8&useSSL=false", "username", "password");
其中com.mysql.jdbc.Driver下的Driver.class的源碼如下:
package com.mysql.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
從com.mysql.jdbc的Driver.java源碼中看到,在Driver類中向DriverManager注冊了對應的驅動實作類。
而從JDBC4.0以後,開始支援使用SPI的方式來注冊這個Driver,這樣當我們使用不同jdbc驅動時,就不用手動修改Class.forName加載的驅動類,隻需要加入相關的jar包就行了。是以上面的資料庫連接配接代碼可以簡寫成如下:
這就不需要Class.forName(“com.mysql.jdbc.Driver”)了。
了解SPI的同學都知道,在DriverManager中,這時候對應的驅動類大體是這麼加載的:
1.通過從META-INF/services/java.sql.Driver檔案中擷取具體的實作類”com.mysql.jdbc.Driver“;
2.通過Class.forName(“com.mysql.jdbc.Driver”)将這個類加載進來。
但是DriverManager是在java.sql中,在rt.jar包中,這個包中的類隻能使用啟動類加載器進行加載,那麼根據類加載的機制,當被裝載的類引用了另外一個類的時候,虛拟機就會使用裝載第一個類的類裝載器裝載被引用的類。即:啟動類加載器還要去加載mysql驅動jar中的類(com.mysql.jdbc.Driver),這顯然是不可能的,根據雙親委派模型的可見性原則,啟動類加載器找不到這個mysql類庫,是以無法加載。
這個問題更加有适用性的說法應該是:JAVA核心包中的類去調用開發者實作的類的方法,這時候就會出現啟動類加載器無法加載到具體實作類的問題。是以想讓啟動類加載器(頂層類加載器)加載可見範圍之外的類庫,隻能破壞雙親委派模型中的可見性原則,讓啟動類加載器可以”加載“到可見範圍之外的類庫。主要這裡我加了個引号,因為這個地方并不是真的是由啟動類加載器加載了com.mysql.jdbc.Driver這個類庫,其實還是由Application ClassLoader系統類加載器加載完成的,隻不過從表面上看起來是破壞了可見行原則,實質上并沒有破壞雙親委派原則。
下面我們看DriverManager是怎麼實作的。
DriverManager加載時,會執行靜态代碼塊,在靜态代碼塊中,會執行loadInitialDrivers方法。而這個方法中會加載對應的驅動類。
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 根據配置檔案加載驅動實作類,下面這個方法中說明了所使用的類加載器
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
}
public static <S> ServiceLoader<S> load(Class<S> service) {
//使用了一個線程上下文類加載器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
ExtClassLoader和AppClassLoader都是通過Launcher類來建立的,在Launcher類的構造函數中我們可以看到線程上下文類加載器預設是AppClassLoader。Launcher類中無參構造方法:
public Launcher() {
ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//設定目前線程的上下文類加載器就是AppClassLoader
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
2.7 如何打破雙親委派模型?
在ClassLoader中有幾個核心方法,上面我們已經展示了loadClass的基本源碼,下面我們再簡略看一下(去掉了一些代碼細節):
package java.lang;
public abstract class ClassLoader {
protected Class defineClass(byte[] b);
protected Class<?> findClass(String name);
protected Class<?> loadClass(String name, boolean resolve) {
synchronized (getClassLoadingLock(name)) {
// 1. 檢查類是否已經被加載過
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//2. 委托給父類加載
c = parent.loadClass(name, false);
} else {
//3. 父類不存在的,交給啟動類加載器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) { }
if (c == null) {
//4. 父類加載器無法完成類加載請求時,調用自身的findClass方法來完成類加載
c = findClass(name);
}
}
return c;
}
}
- defineClass 方法:調用 native 方法将 位元組數組解析成一個 Class 對象;
- findClass 方法:抽象類ClassLoader中預設抛出ClassNotFoundException,需要繼承類自己去實作,目的是通過檔案系統或者網絡查找類;
-
loadClass 方法: 首先根據類的全限定名檢查該類是否已經被加載過,如果沒有被加載,那麼當子加載器持有父加載器的引用時,那麼委托給父加載器去嘗試加載,如果父類加載器無法完成加載,再交給子類加載器進行加載。loadClass方法 就是實作了雙親委派機制。
ClassLoader 的三個重要方法,那麼如果需要自定義一個類加載器的話,直接繼承 ClassLoader類,一般情況隻需要重寫 findClass 方法即可,自己定義加載類的路徑,可以從檔案系統或者網絡環境。但是,如果想打破雙親委派機制,那麼還要重寫 loadClass 方法。