類加載器
以實現(xiàn)方式分類
1、c++實現(xiàn)
2、java實現(xiàn)
以功能分類
1、啟動類加載器
2、擴展類加載器
3、應(yīng)用程序類加載器
其中啟動類加載器由c++實現(xiàn),其它的的類加載器均又java實現(xiàn),由java實現(xiàn)的類加載器都繼承自類java.lang.ClassLoader
類加載器之間的聯(lián)系以及功能
如下圖所示。

注:類加載器之間存在著邏輯上的父子關(guān)系,但不是真正意義上的父子關(guān)系,因為它們沒有真正的從屬關(guān)系,即不是繼承關(guān)系
啟動類加載器
JVM將C++處理類加載的一套邏輯定義為啟動類加載器,所以它不像其它類加載器是有實體的,因此無法被java程序調(diào)用。
查看啟動類加載器的加載路徑
代碼
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL url:urLs) {
System.out.println(url);
}
打印結(jié)果
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/classes
可以通過-Xbootclasspath指定
擴展類加載器
查看擴展類加載器加載的路徑
ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent();
URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
URL[] urLs = urlClassLoader.getURLs();
for (URL url:urLs) {
System.out.println(url);
}
打印結(jié)果
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/jfxrt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/nashorn.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/zipfs.jar
可以通過java.ext.dirs指定
應(yīng)用類加載器
默認加載用戶程序的類加載器
查看應(yīng)用類加載器的路徑
String[] urls = System.getProperty("java.class.path").split(":");
for (String url:urls) {
System.out.println(url);
}
System.out.println("=======================================");
URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
URL[] urLs = classLoader.getURLs();
for (URL url:urLs) {
System.out.println(url);
}
打印結(jié)果
上面演示的是兩種方法,打印結(jié)果是一樣的
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/deploy.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/jfxrt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/nashorn.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/zipfs.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/javaws.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/jfxswt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/management-agent.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/plugin.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/rt.jar
file:/F:/study/lb/%e7%ac%ac%e4%b8%80%e9%98%b6%e6%ae%b5%20jvm/2%20%e7%b1%bb%e5%8a%a0%e8%bd%bd%e5%ad%90%e7%b3%bb%e7%bb%9f%e4%b8%8eSPI/luban-jvm-research/target/classes/
file:/F:/java_dev_env/maven-repository/org/openjdk/jol/jol-core/0.10/jol-core-0.10.jar
file:/F:/java_dev_env/maven-repository/cglib/cglib/3.3.0/cglib-3.3.0.jar
file:/F:/java_dev_env/maven-repository/org/ow2/asm/asm/7.1/asm-7.1.jar
file:/F:/Program%20Files%20(x86)/IntelliJ%20IDEA%202019.3.4/lib/idea_rt.jar
自定義類加載器
當(dāng)我們寫一個類繼承自java.lang.ClassLoader之后,這就是我們的自定義加載器了,自定義加載器的的父類加載器默認使用系統(tǒng)類加載器(AppClassLoader)。
對應(yīng)的無參構(gòu)造函數(shù)如下
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
代碼實現(xiàn)
//繼承ClassLoader
public class MyClassloader extends ClassLoader {
?
public static void main(String[] args) {
MyClassloader classloader = new MyClassloader();
try {
Class<?> clazz = classloader.loadClass(ClassloaderToLoad.class.getName());
System.out.println(clazz);
System.out.println(clazz.getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
?
public static final String SUFFIX = ".class";
//重寫findClass方法
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
System.out.println("Classloader_1 findClass");
//將包名轉(zhuǎn)換為路徑名獲取文件字節(jié)數(shù)組
byte[] data = getData(className.replace('.', '/'));
return defineClass(className, data, 0, data.length);
}
//通過路徑名獲取字節(jié)數(shù)組
private byte[] getData(String name) {
InputStream inputStream = null;
ByteArrayOutputStream outputStream = null;
?
File file = new File(name + SUFFIX);
if (!file.exists()) return null;
?
try {
inputStream = new FileInputStream(file);
outputStream = new ByteArrayOutputStream();
?
int size = 0;
byte[] buffer = new byte[1024];
?
while ((size = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, size);
}
?
return outputStream.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
outputStream.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
return null;
}
}
//要去加載的類
class ClassloaderToLoad {
?
}
打印結(jié)果
class com.cloud.classloader.Classloader_1_A
sun.misc.Launcher$AppClassLoader@18b4aac2
打印出來的類加載器居然不是咱們自己定義的類加載器,難道翻車了?
然而并沒有,此處需要引出類加載器之間一個很重要的機制——雙親委派。
雙親委派
類加載器的類存儲
首先先了解一下,類加載器加載的類是如何存儲的,廢話不說,先上圖。

值得一提的是ClassLoader本省對于java而言,也是一個分配在堆中的一個對象,它管理著自己在方法區(qū)的一個區(qū)域。對于對象來說,即使名稱相同,如果是不同類加載器加載的,那么它們就是不同的。
概念
如果某個類加載器收到了加載某個類的請求,這個類加載器并不會立即去加載這個類,而是把請求委派給父類加載器,每一個層次的類加載器都是如此,因此所有的類加載請求最終都會傳送到頂端的啟動類加載器,只有當(dāng)父類加載器在其搜索范圍內(nèi)無法找到所需的類,并將該結(jié)果反饋給子類加載器時,子類加載器才會嘗試自己去加載。
上圖

上面我們自己實現(xiàn)的自定義類加載器之所以沒有加載咱們想加載的類是因為應(yīng)用程序類加載器已經(jīng)幫我們加載了,如果想要用我們自己定義的類加載器加載,需要去加載三個預(yù)定義類加載器加載范圍之外的類。
優(yōu)勢
避免重復(fù)加載 + 避免核心類篡改
采用雙親委派模式的是好處是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系,通過這種層級關(guān)可以避免類的重復(fù)加載,當(dāng)父親已經(jīng)加載了該類時,就沒有必要子ClassLoader再加載一次。
其次是考慮到安全因素,java核心api中定義類型不會被隨意替換,假設(shè)我們自己定義了一個java.lang.Integer的類,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發(fā)現(xiàn)這個名字的類,發(fā)現(xiàn)該類已被加載,并不會重新加載我們自己定義的java.lang.Integer,而直接返回已加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改。
劣勢
通過雙親委派機制的原理可以得出一下結(jié)論:由于BootstrapClassloader是頂級類加載器,BootstrapClassloader無法委派AppClassLoader來加載類,也就是說BootstrapClassloader中加載的類中無法使用由AppClassLoader加載的類??赡芙^大部分情況這個不算是問題,因為BootstrapClassloader加載的都是基礎(chǔ)類,供AppClassLoader加載的類調(diào)用的類。但是萬事萬物都不是絕對的,比如最經(jīng)典的例子—— jdbc加載數(shù)據(jù)庫驅(qū)動。

很明顯,接口:java.sql.Driver,定義在java.sql包中,包所在的位置是:jdk\jre\lib\rt.jar中,java.sql包中還提供了其它相應(yīng)的類和接口比如管理驅(qū)動的類:DriverManager類,很明顯java.sql包是由BootstrapClassloader加載器加載的;而接口的實現(xiàn)類com.mysql.jdbc.Driver是由第三方實現(xiàn)的類庫,由AppClassLoader加載器進行加載的,我們的問題是DriverManager再獲取鏈接的時候必然要加載到com.mysql.jdbc.Driver類,這就是由BootstrapClassloader加載的類使用了由AppClassLoader加載的類,很明顯和雙親委托機制的原理相悖,那它是怎么解決這個問題的?這就引申了我們第二個問題:如何打破雙親委派機制?
打破雙親委派
思路很簡單,取反唄,雙親委派是向上委派,打破雙親委派,那咱們就是不委派,或向下委派。
自定義類加載器
public class TestClassLoader extends ClassLoader {
?
private String name;
?
public TestClassLoaderN(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
?
@Override
public String toString() {
return this.name;
}
?
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> clazz = null;
if(name.startsWith("com.cloud")){
clazz = findClass(name);
} else{
ClassLoader system = getSystemClassLoader();
try {
clazz = system.loadClass(name);
} catch (Exception e) {
// ignore
}
if (clazz != null)
return clazz;
}
return clazz;
}
?
@Override
public Class<?> findClass(String name) {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
is = new FileInputStream(new File("F:/test/Test.class"));
int c = 0;
while (-1 != (c = is.read())) {
baos.write(c);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return this.defineClass(name, data, 0, data.length);
}
?
public static void main(String[] args) {
TestClassLoaderN loader = new TestClassLoaderN(
TestClassLoaderN.class.getClassLoader(), "TestLoader");
Class clazz;
try {
clazz = loader.loadClass("com.cloud.Test");
Object object = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
自定義類加載器,重寫loadClass方法,讓其不去父類加載器中查找,用我們自己的findClass方法查找即可。
SPI機制
SPI ,全稱為 Service Provider Interface,是一種服務(wù)發(fā)現(xiàn)機制。它通過在ClassPath路徑下的META-INF/services文件夾查找文件,自動加載文件里所定義的類。
這一機制為很多框架擴展提供了可能,比如在Dubbo、JDBC中都使用到了SPI機制。
上面已經(jīng)提到DriverManager是啟動類加載器加載的,根據(jù)雙親委派,它不可能調(diào)用到由應(yīng)用程序類加載器加載的驅(qū)動程序,而事實上他卻可以調(diào)用,實現(xiàn)了向下委派,典型破壞了雙親委派,而這里就應(yīng)用了SPI機制,咱們來看下源碼。
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
?
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//ServiceLoader.load 獲取驅(qū)動的關(guān)鍵代碼,點進ServiceLoader看看
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
public final class ServiceLoader<S> implements Iterable<S>
{
//很明顯這個類就是去"META-INF/services/" 查找對應(yīng)的驅(qū)動類,這不就是前面解釋的SPI機制嗎?
private static final String PREFIX = "META-INF/services/";
?
// The class or interface representing the service being loaded
private final Class<S> service;
?
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
?
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
為了驗證一下我們?nèi)ysql驅(qū)動包里去看一下,如下圖

線程上下文類加載器
在看jdbc的源碼時,我們會看到它是如下獲取類加載器,其中ContextClassLoader就是上下文類加載器。
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
獲取
Thread.currentThread().getContextClassLoader()
設(shè)置
Thread.currentThread().setContextClassLoader(new Classloader());
如果不做任何設(shè)置,Java 應(yīng)用的線程的上下文類加載器默認就是系統(tǒng)上下文類加載器,它的存在就是為了打破雙親委派,實現(xiàn)逆向委派。