類加載器的簡單介紹

我們都知道JDBC規(guī)范定義在java的核心包rt.jar中,rt.jar是由啟動類加載器去加載的,jdbc的驅動管理器是由具體的數(shù)據(jù)庫廠商提供的,比如mysql和oracle的jar包。實際開發(fā)中一般是把驅動包放到classpath中,但是classpath路徑是由系統(tǒng)類加載器去加載的,正常來說啟動類加載器是無法加載classpath中的類文件的,那么jdbc是如何獲取到驅動的呢?想要弄清楚這個問題,需要先了解java類加載的機制。

一 類加載器的介紹

java代碼通過javac編譯成class文件,而類加載器就是把class文件裝進虛擬機。java中類加載器分為四類,分別是啟動類加載器、擴展類加載器、系統(tǒng)類加載器和自定義類加載器。為什么需要這么多類加載器呢?因為java虛擬機啟動的時候,并不會一次性加載所有的class文件,而是根據(jù)需要去動態(tài)加載,避免很大程度的內存浪費。

  1. 啟動類加載器
    顧名思義,是在虛擬機啟動時加載,是由C++編寫,主要掃描rt.jar這個包,但并不僅僅是這個包,可以通過系統(tǒng)參數(shù)System.getProperty("sun.boot.class.path")獲取掃描的路徑??梢钥吹剑瑔宇惣虞d器加載的是jre和jre/lib目錄下的核心庫。

    //啟動類加載器加載的路徑
    String bootstrapPaths = System.getProperty("sun.boot.class.path");
    Arrays.stream(bootstrapPaths.split(":")).forEach(path -> System.out.println(path));
    
    //打印結果如下
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/resources.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/sunrsasign.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jsse.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jce.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/charsets.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jfr.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/classes
    
  2. 擴展類加載器
    通過System.getProperty("java.ext.dirs")可以看到它主要負責加載Java的擴展類庫,默認加載JAVA_HOME/jre/lib/ext/目錄下的所有jar包或者由java.ext.dirs系統(tǒng)屬性指定的jar包。

    //擴展類加載器加載的路徑:
    String extPaths = System.getProperty("java.ext.dirs");
    Arrays.stream(extPaths.split(":")).forEach(path -> System.out.println(path));
    
    //打印結果如下
    /Users/liyefei6/Library/Java/Extensions
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext
    /Library/Java/Extensions
    /Network/Library/Java/Extensions
    /System/Library/Java/Extensions
    /usr/lib/java
    
  3. 系統(tǒng)類加載器
    負責在JVM啟動時,加載來自在命令java中的classpath或者java.class.path系統(tǒng)屬性或者CLASSPATH操作系統(tǒng)屬性所指定的JAR類包和類路徑.

    //系統(tǒng)類加載器加載的路徑:
    String appPaths = System.getProperty("java.class.path");
    Arrays.stream(appPaths.split(":")).forEach(path -> System.out.println(path));
    
    //打印結果如下
    系統(tǒng)類加載器加載的路徑:
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/charsets.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/deploy.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/dnsns.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/jaccess.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/localedata.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/nashorn.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/sunec.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/zipfs.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/javaws.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jce.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jfr.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jfxswt.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jsse.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/management-agent.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/plugin.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/resources.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/ant-javafx.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/dt.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/javafx-mx.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/jconsole.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/packager.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/sa-jdi.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/tools.jar
    /Users/liyefei6/code/demo-parent/demo-classloader/target/classes
    /Users/liyefei6/.m2/repository/org/apache/commons/commons-lang3/3.11/commons-lang3-3.11.jar
    ......
    
    public class Test {
        public static void main(String[] args) {
            //下面代碼輸出結果為:sun.misc.Launcher$AppClassLoader@18b4aac2
            System.out.println(Test.class.getClassLoader());
        }
    }
    //通過上面代碼打印的結果驗證了Test類使用的類加載器為AppClassLoader,即系統(tǒng)類加載器。
    
  4. 自定義類加載器
    如果系統(tǒng)提供的類加載器滿足不了需求,比如本地磁盤或者網絡下載的.class文件,那么就可以自定義一個類加載器。下面是官方doc上給出的實現(xiàn)自定義加載器的例子:

     *     class NetworkClassLoader extends ClassLoader {
     *         public Class findClass(String name) {
     *             byte[] b = loadClassData(name);
     *             return defineClass(name, b, 0, b.length);
     *         }
     *
     *         private byte[] loadClassData(String name) {
     *             // load the class data from the connection
     *         }
     *     }
    

    從代碼示例中可以看出,實現(xiàn)自定義類加載器只需要三步:寫一個loadClassData把class文件變?yōu)閎yte數(shù)組、重寫findClass方法、調用defineClass方法。下面是自定義類加載器的實現(xiàn):

    //自定義類加載器
    public class MyClassLoader extends ClassLoader {
    
        private String loadPath;
    
        public MyClassLoader(String loadPath) {
            super();
            this.loadPath = loadPath;
        }
    
        @Override
        public Class findClass(String name) {
            byte[] b = loadClassData(name);
            return defineClass(name, b, 0, b.length);
        }
    
        private byte[] loadClassData(String name) {
            String fullPath = loadPath + name.replaceAll("\\.", "/") + ".class";
            File file = new File(fullPath);
            if (file.exists()) {
                FileInputStream in = null;
                ByteArrayOutputStream out = null;
                try {
                    in = new FileInputStream(file);
                    out = new ByteArrayOutputStream();
                    byte[] buffer = new byte[1024];
                    int size = 0;
                    while ((size = in.read(buffer)) != -1) {
                        out.write(buffer, 0, size);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                return out.toByteArray();
            } else {
                return null;
            }
        }
    }
    
    //測試類
    public class Test {
        public static void main(String[] args) throws Exception {
            
             //使用默認的類加載器
            System.out.println(Test.class.getClassLoader());  
          
            //使用自定義的類加載器
            MyClassLoader myClassLoader = new MyClassLoader("/Users/liyefei6/classes/");
            Class<?> person1Class = myClassLoader.loadClass("com.demo.classloader.Person1");
            System.out.println(person1Class.getClassLoader());
        }
    }
    
    //打印結果:
    sun.misc.Launcher$AppClassLoader@18b4aac2//使用的是系統(tǒng)類加載器AppClassLoader
    com.demo.classloader.MyClassLoader@1fb3ebeb//使用的是自定義類加載器MyClassLoader
    

    從系統(tǒng)提供的類加載器掃描的路徑分析到/Users/liyefei6/classes/路徑是不會被掃描到的,就不會被類加載器所加載,所以打印出來的結果是自定義類加載器,而Test類是在classpath下,是會被系統(tǒng)類加載器所加載到的,所以打印的結果是AppClassLoader。有意思的是Person1類中有靜態(tài)代碼塊但并未執(zhí)行,java中嚴格規(guī)定了只有以下五種情況會觸發(fā)類的初始化:

    • 遇到new、getstatic、putstatic、invokestatic這四條字節(jié)碼指令
    • 使用Java.lang.refect包的方法對類進行反射調用
    • 當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行初始化,則需要先觸發(fā)其父類的初始化
    • 當虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類,虛擬機會先執(zhí)行該主類
    • JDK1.5中方法句柄所對應的類沒有進行過初始化,則需要先觸發(fā)其初始化

    簡單總結就是類加載不一定會初始化,初始化一定需要類加載,Person1類如下所示:

    public class Person1 {
        private String name;
        static {
            System.out.println("靜態(tài)代碼塊執(zhí)行 -> Person1");
        }
    
        public Person1() {
            System.out.println("構造方法執(zhí)行 -> Person1");
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    
    

二 類加載器執(zhí)行過程

雙親委派模型是一種很好的設計思想,它不僅可以避免重復加載,還可以避免核心類被篡改。比如自己重寫個java.lang.Object并放到Classpath中,如果沒有雙親委派的話直接自己執(zhí)行了,那不安全。雙親委派可以保證這個類只能被頂層Bootstrap Classloader類加載器加載,從而確保只有JVM中有且僅有一份正常的java核心類。如果有多個的話,那么就亂套了。比如相同的類instance of可能返回false,因為可能父類不是同一個類加載器加載的Object。借鑒比較經典的兩幅圖來介紹類加載的過程。

image

如果一個類加載器收到了類加載的請求,他首先會從自己緩存里查找是否之前加載過這個class,如果加載過直接返回,如果沒加載過的話他不會自己親自去加載,他會把這個請求委派給父類加載器去完成,每一層都是如此,類似遞歸,一直遞歸到頂層父類。也就是Bootstrap ClassLoader,只要加載完成就會返回結果,如果頂層父類加載器無法加載此class,則會返回去交給子類加載器去嘗試加載,若最底層的子類加載器也沒找到,則會拋出ClassNotFoundException
但是在有些場景下需要打破雙親委派的思想,比如Tomcat、熱部署等等。

Tomcat為什么要破壞雙親委派模型?
因為一個Tomcat可以部署N個web應用,但是每個web應用都需要有自己的classloader,才能互不干擾。比如web1里面有com.test.A.class,web2里面也有com.test.A.class,如果只有一套classloader,出現(xiàn)了兩個重復的類路徑,會相互影響和沖突,所以tomcat打破了,他是線程級別的,不同web應用是不同的classloader。
熱部署打破雙親委派是因為jvm會通過默認的loadClass()方法先找緩存,如果加載過就不會再次加載,你改了class字節(jié)碼也不會熱加載,所以自定義ClassLoader,去掉找緩存那部分,直接就去加載,也就是每次都重新加載。

三 SPI介紹

「SPI」 全稱為 (Service Provider Interface) ,是JDK內置的一種服務提供發(fā)現(xiàn)機制。 目前有不少框架用它來做服務的擴展實現(xiàn), 簡單來說,它就是一種動態(tài)替換發(fā)現(xiàn)的機制。

按照雙親委派模型的思想,JDBC規(guī)范中定義的類是由啟動類加載器去加載的,它是是無法使用mysql或者oracle驅動包里由系統(tǒng)類加載器加載出來的類的。為了解決這個問題,又引入了SPI的規(guī)范,使啟動類加載器可以調用系統(tǒng)類加載器加載出的類。

下面舉例說明SPI的使用,首先創(chuàng)建一個僅包含擴展接口的maven工程spi-interface,然后創(chuàng)建一個接口SPIService。

//SPIService.java
package com.liyefei.service;

public interface SPIService {
    void println(String str);
}

<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 
    <groupId>com.liyefei.interface</groupId>
    <artifactId>spi-interface</artifactId>
    <version>1.0.0</version>
</project>

接下來創(chuàng)建一個maven工程spi-impl作為擴展的實現(xiàn),工程的資源文件下需要創(chuàng)建META-INF/services目錄,目錄中需要創(chuàng)建一個用擴展接口全路徑名稱的文件,文件內容為接口實現(xiàn)類的全路徑名,代碼如下:

//OneServiceImpl.java
package com.liyefei.one;
import com.liyefei.service.SPIService;

public class OneServiceImpl implements SPIService {
    @Override
    public void println(String str) {
        System.out.println("impl service:" + str);
    }
}
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   
    <groupId>com.liyefei.impl</groupId>
    <artifactId>spi-impl</artifactId>
    <version>1.0.0</version>

    <dependencies>
        <dependency>
            <groupId>com.liyefei.interface</groupId>
            <artifactId>spi-interface</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
</project>
文件目錄如下:
├── spi-impl
        │       ├──src
        │       │       ├──main
        │       │               ├──java
        │       │                       └──OneServiceImpl
        │       │               ├──resources
        │       │                       ├──META-INF
        │       │                               ├──services
        │       │                                       ├──com.liyefei.service
        └──pom.xml

最后創(chuàng)建一個spi-test的maven工程,用于測試,工程依賴擴展的實現(xiàn),代碼如下:

//Test.java
package com.liyefei.test;
import com.liyefei.service.SPIService;

import java.util.Iterator;
import java.util.ServiceLoader;

public class Test {
    public static void main(String[] args) {
        ServiceLoader<SPIService> spiServices = ServiceLoader.load(SPIService.class);
        Iterator<SPIService> iterator = spiServices.iterator();
        while (iterator.hasNext()) {
            iterator.next().println("run");
        }
    }
}
//打印結果:impl service:run
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  
    <dependencies>
        <dependency>
            <groupId>com.liyefei.impl</groupId>
            <artifactId>spi-impl</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
</project>

從打印結果可以看出,接口SPIService的實現(xiàn)類ONEServiceImpl成功執(zhí)行,想要深究原因只能查看源碼,進入load方法發(fā)現(xiàn)使用了上下文類加載器,源碼如下:

//ServiceLoader.class
public static <S> ServiceLoader<S> load(Class<S> service) {
            //上下文類加載器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

通過源碼一路跟進可以看到,fullName就是由接口擴展實現(xiàn)工程中資源文件夾下"META-INF/services/" + 接口全路徑名稱組成,最后通過Class.forName使用上下文類加載器反射出所需要的類。

//ServiceLoader.class
private static final String PREFIX = "META-INF/services/";
String fullName = PREFIX + service.getName();
if (loader == null)
    configs = ClassLoader.getSystemResources(fullName);
else
    configs = loader.getResources(fullName);
//ServiceLoader.class
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
Class<?> c = null;
try {
  c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
  fail(service, "Provider " + cn + " not found");
}

那么上下文類加載器中存放的是那個類加載器呢?通過類加載器的主要類Launcher.class源碼中可以看到線程上下文類加載器中默認存放的類加載器為系統(tǒng)類加載器。通過Thread.currentThread().setContextClassLoader(this.loader)這行代碼打破了雙親委派模型。

//Launcher.class
public class Launcher {
    private static URLStreamHandlerFactory factory = new Launcher.Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    private ClassLoader loader;
    private static URLStreamHandler fileHandler;

    public static Launcher getLauncher() {
        return launcher;
    }

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            //擴展類加載器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            //系統(tǒng)類加載器
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        //此處即為jvm啟動時設置的上下文類加載器,即系統(tǒng)類加載器
        Thread.currentThread().setContextClassLoader(this.loader);
        .....

SPI打破了雙親委派模型,具體的底層實現(xiàn)可以分離出來,將每組實現(xiàn)打包成不同的jar,在具體使用時根據(jù)需要使用不同的jar即可,方便了應用的擴展,JDBC規(guī)范中加載各個供應商的驅動就是通過這種方式去實現(xiàn)的。


image

四 總結與展望

工作和生活中只要肯思考,就會有收獲。通過一個類加載器可以挖掘出很多的知識點,慢慢就會明白很多應用的實現(xiàn)原理,想要弄清楚原理必須要通過源碼去發(fā)掘、去實踐,也希望自己可以通過多發(fā)掘別人的源碼、學習別人的設計思想,有朝一日可以在技術的道路上越走越遠。

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

相關閱讀更多精彩內容

  • 類加載器負責的范圍,首先看張圖 通過代碼驗證如下結論 在idea里面運行上述程序,會的到如下結果 啟動類加載器/L...
    ZFH__ZJ閱讀 570評論 0 0
  • 一、類加載器基本原理 虛擬機提供了3種類加載器:Bootstrap類加載器、Ext類加載器、App類加載器。他們之...
    一天的閱讀 4,911評論 1 3
  • 內容概述 “類加載”介紹 “類加載器”介紹 深入“類加載器” 深入“父親委托機制” 一,“類加載”介紹 “加載”是...
    tomas家的小撥浪鼓閱讀 2,041評論 0 1
  • 所謂類加載機制,就是虛擬機把描述類的數(shù)據(jù)從Class文件加載到內存中,并對其進行校驗,轉換,分析以及初始化,并最終...
    登高且賦閱讀 1,227評論 0 15
  • 今天, 同事來找, 有個需求, 大概是這樣子, 想用crontab shell方式跑我們現(xiàn)在的SpringBoot...
    it_true閱讀 1,350評論 0 0

友情鏈接更多精彩內容