插件化開發基礎篇—類加載

1. 什麼是類加載?

每個編寫的".java"拓展名類文件都存儲着需要執行的程序邏輯,這些".java"文件經過Java編譯器編譯成拓展名爲".class"的文件,".class"文件中保存着Java代碼經轉換後的虛擬機指令,當需要使用某個類時,虛擬機將會加載它的".class"文件,將class文件加載到虛擬機內存的方法區,生成class對象,這個過程稱爲類加載

2. 類加載的方式?

2.1 顯式加載

顯式加載指的是在代碼中通過調用ClassLoader加載字節碼並返回class對象,如直接使用Class.forName(name)this.getClass().getClassLoader().loadClass()加載class對象。

2.2 隱式加載

隱式加載則是不直接在代碼中調用ClassLoader的方法加載class對象,而是通過虛擬機自動加載到內存中,如在加載某個類的class文件時,該類的class文件中引用了另外一個類,此時當前類的類加載器嘗試加載被引用的類到JVM內存中。在日常開發以上兩種方式一般會混合使用。

2.2.1 延遲加載

延遲加載又叫按需加載,當程序創建第一個對類的靜態成員(靜態成員屬性、靜態成員方法)的引用時,就會加載這個類。這也證明了構造器也是類的靜態方法,即使在構造器之前並沒有 static 關鍵字。因此,使用 new 操作符創建類的對象也會被當作對類的靜態成員的引用。

反過來,類加載的時候,又會去進行static變量的賦值和執行static靜態代碼塊。”類加載“和”靜態成員的初始化和靜態代碼塊的執行“這兩個過程是相輔相成的。

3. 類加載的過程

加載:就是指通過類的全限定名將class文件讀入內存,併爲之創建一個Class對象

鏈接:

  1. 驗證:確保被加載類的正確性
  2. 準備:負責爲類的靜態成員分配內存,並設置默認初始化值爲0
  3. 解析:將類中的符號引用替換爲直接引用(參考jvm內存模型中的常量池)

初始化: 執行靜態語句,包括靜態變量的賦值還有靜態代碼塊
1

圖一:類的生命週期

4. 雙親委派

雙親委派模型發佈於JDK1.2,其要求除頂層的啓動類加載器外,其餘的類加載器都應該有自己的父類加載器。這裏的父子關係不是以繼承的方式體現,而是通過組合的方式,即父加載器以parent成員屬性的方式保存在子加載器中。

JDK自帶的類加載器有三種:

  1. 啓動類加載器,Bootstrap ClassLoader,由C++實現,是虛擬機自身的一部分。加載%JAVA_HOME%/lib目錄和-Xbootclasspath指定目錄下的,並且必須是虛擬機能識別(如rt.jar,名字不符合的放在目錄下也不會被加載)的類
  2. 擴展類加載器,sun.misc.Launcher$ExtClassLoader,加載%JAVA_HOME%/lib/ext目錄和-Djava.ext.dir系統變量指定的目錄下的所有類庫。開發者可以直接使用該類加載器
  3. 系統類加載器/應用程序類加載器,sun.misc.Launcher$AppClassLoader, ClassLoader.getSystemClassLoader方法的返回值,負責加載classpath下所有類庫
    2
圖二:Parents Delegation Model

4.1 確立父子關係

JVM啓動時,會去調用Launcher.java類的構造方法,其中定義了三種類加載器的父子關係:

public Launcher() {
        ClassLoader extcl;
        try {
        // 首先創建擴展類加載器
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader");
        }

        // Now create the class loader to use to launch the application
        try {
	        //再創建AppClassLoader並把extcl作爲父加載器傳遞給AppClassLoader
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader");
        }

        //設置線程上下文類加載器,稍後分析
        Thread.currentThread().setContextClassLoader(loader);
        //省略其他沒必要的代碼......
        }
    }

Launcher初始化時首先會創建ExtClassLoader類加載器,然後再創建AppClassLoader並把ExtClassLoader傳遞給它作爲父類加載器,這裏還把AppClassLoader默認設置爲線程上下文類加載器,關於線程上下文類加載器稍後會分析。那ExtClassLoader類加載器的父類加載器被設置爲null,因爲其父類加載器BootstraClassLoaders是JVM自身需要的類,這個類加載器使用C++語言實現的。

//Launcher中創建ExtClassLoader
extcl = ExtClassLoader.getExtClassLoader();

//getExtClassLoader()方法
public static ExtClassLoader getExtClassLoader() throws IOException{

  //........省略其他代碼 
  return new ExtClassLoader(dirs);                     
  // .........
}

//構造方法
public ExtClassLoader(File[] dirs) throws IOException {
   //調用父類構造URLClassLoader傳遞null作爲parent
   super(getExtURLs(dirs), null, factory);
}

//URLClassLoader構造
public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
}

ClassLoader是一個抽象類,很多方法沒有實現,比如 findClass()等。而URLClassLoader這個實現類爲這些方法提供了具體的實現,並新增了URLClassPath類協助取得Class字節碼流等功能,在編寫自定義類加載器時,如果沒有太過於複雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findClass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔
在這裏插入圖片描述

圖三:ClassLoader extends relationship

4.2 委派過程

  1. loadclass():定義了雙親委派流程
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 查JVM中的緩存,看是否當前ClassLoader已經作爲定義類加載器加載過該class
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                		//嘗試讓父類加載器加載
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    		//如果沒有父類加載器,嘗試讓BootstrapClassLoader加載
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
								//如果父類加載器無法返回class對象,則調用findClass方法,讀取字節流
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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;
        }
    }
  1. findClass():讀取.class字節碼文件的字節流

JDK1.2之後已不再建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中,這樣就可以保證自定義的類加載器也符合雙親委託模式。

   import java.io.*;
   
   public class MyClassLoader extends ClassLoader {
   
       /**
        * 重寫findClass方法
        * @param name 是我們這個類的全限定名
        * @return
        * @throws ClassNotFoundException
        */
       @Override
       protected Class<?> findClass(String name) throws ClassNotFoundException {
   
           try {
               String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
             	 //到classpath下尋找.class文件並讀取其字節流
               InputStream is = this.getClass().getResourceAsStream(fileName);
               byte[] b = new byte[is.available()];
               is.read(b);
               return defineClass(name, b, 0, b.length);
           }catch (IOException e){
               throw new ClassNotFoundException(name);
           }
       }
   }

自定義類加載器如果沒有定義構造函數,則會默認調用父類ClassLoader的構造函數,並會默認將AppClassLoader作爲自己的父類加載器:

public abstract class ClassLoader {
  	//省略無關代碼...
    protected ClassLoader() {
      	//getSystemClassLoader()返回AppClassLoader
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

    private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                    Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }
}
  1. defineClass():用來將byte字節流解析成JVM能夠識別的Class對象

4.3優缺點

  1. 優點:

首先,通過這種層級關可以避免類的重複加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次;

其次,確保核心類的安全,防止核心類型被隨意替換。假設通過網絡傳遞一個名爲java.lang.Integer的類,通過雙親委託模式傳遞到啓動類加載器,而啓動類加載器在覈心Java API發現這個名字的類,發現該類已被加載,並不會重新加載網絡傳遞的過來的java.lang.Integer,而是直接返回已加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改

  1. 缺點:

雙親委託機制中存在一個隱含的關係是一個類是由某個類加載器加載的,那麼它所引用的其他類都是由該類加載器來加載,一些SPI接口屬於 Java 核心庫如java.sql.DriverManager,由BootstrapClassLoader加載,當SPI接口想要引用第三方實現類的具體方法時,BootstrapClassLoader無法加載位於classpath下的第三方實現類

5. 打破雙親委派

由上節可知,一些SPI接口屬於 Java 核心庫,由BootstrapClassLoader加載,當SPI接口想要引用第三方實現類的具體方法時,BootstrapClassLoader無法加載classpath下的第三方實現類,這時就需要使用線程上下文類加載器Thread.currentThread().getContextClassLoader()來解決。藉助這種機制可以打破雙親委託機制限制:

//DriverManager是Java核心包rt.jar的類
public class DriverManager {
	//省略不必要的代碼
    static {
        loadInitialDrivers();//執行該方法
        println("JDBC DriverManager initialized");
    }

//loadInitialDrivers方法
 private static void loadInitialDrivers() {
     sun.misc.Providers()
     AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
				//加載外部的Driver的實現類
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
              //省略不必要的代碼......
            }
        });
    }

//SPI核心類ServiceLoader
public final class ServiceLoader<S> implements Iterable<S> {
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 線程上下文類加載器,在Launcher類的構造器中被賦值爲AppClassLoader,它可以讀到ClassPath下的自定義類
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            //cn表示第三方具體實現類的全限定名,如果該實現類的class在loader類加載器命名空間沒有找到,則通過loader進行加載
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service,
                    "Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service,
                    "Provider " + cn + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service,
                    "Provider " + cn + " could not be instantiated", x);
        }
        throw new Error();          // This cannot happen
    }
}

在這裏插入圖片描述

圖四:ThreadContextClassLoader

6. 命名空間

在JVM中,即使兩個class對象來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的ClassLoader實例對象不同,那麼這兩個class對象也是不相等的。那麼一個進程中有諸多不同類加載器加載的class,這些class對象是完全隔離的嗎?滿足怎樣的條件才能互相引用?請看下篇《利用SPI進行插件化開發》

參考:
https://blog.csdn.net/javazejian/article/details/73413292
https://juejin.im/post/5e3cd9cee51d4527214ba232
https://www.runoob.com/w3cnote/java-class-forname.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章