JVM從入門到上天之類加載子系統(tǒng)與SPI

類加載器

以實現(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)系以及功能

如下圖所示。

類加載器.png
注:類加載器之間存在著邏輯上的父子關(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

打印出來的類加載器居然不是咱們自己定義的類加載器,難道翻車了?

然而并沒有,此處需要引出類加載器之間一個很重要的機制——雙親委派。

雙親委派

類加載器的類存儲

首先先了解一下,類加載器加載的類是如何存儲的,廢話不說,先上圖。

類加載器的類存儲.png

值得一提的是ClassLoader本省對于java而言,也是一個分配在堆中的一個對象,它管理著自己在方法區(qū)的一個區(qū)域。對于對象來說,即使名稱相同,如果是不同類加載器加載的,那么它們就是不同的。

概念

如果某個類加載器收到了加載某個類的請求,這個類加載器并不會立即去加載這個類,而是把請求委派給父類加載器,每一個層次的類加載器都是如此,因此所有的類加載請求最終都會傳送到頂端的啟動類加載器,只有當(dāng)父類加載器在其搜索范圍內(nèi)無法找到所需的類,并將該結(jié)果反饋給子類加載器時,子類加載器才會嘗試自己去加載。

上圖

雙親委派.png

上面我們自己實現(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ū)動。

driver.png

很明顯,接口: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ū)動包里去看一下,如下圖

mysql驅(qū)動.png
線程上下文類加載器

在看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)逆向委派。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容