Java虛擬機學習筆記(五)——委派模型、ClassLoader

類加載器

JVM 中內置了三個重要的 ClassLoader,除了 BootstrapClassLoader 其他類加載器均由 Java 實現且全部繼承自java.lang.ClassLoader:

類加載器總結

  • BootstrapClassLoader(啓動類加載器) :最頂層的加載類,由C++實現,負責加載 %JAVA_HOME%/lib目錄下的jar包和類或者或被 -Xbootclasspath參數指定的路徑中的所有類。
  • ExtensionClassLoader(擴展類加載器) :主要負責加載目錄 %JRE_HOME%/lib/ext 目錄下的jar包和類,或被 java.ext.dirs 系統變量所指定的路徑下的jar包。
  • AppClassLoader(應用程序類加載器) :面向我們用戶的加載器,負責加載當前應用classpath下的所有jar包和類。

雙親委派模型

每一個類都有一個對應它的類加載器。系統中的 ClassLoder 在協同工作的時候會默認使用 雙親委派模型 。即在類加載的時候,系統會首先判斷當前類是否被加載過。已經被加載的類會直接返回,否則纔會嘗試加載。加載的時候,首先會把該請求委派該父類加載器的 loadClass() 處理,因此所有的請求最終都應該傳送到頂層的啓動類加載器 BootstrapClassLoader 中。當父類加載器無法處理時,才由自己來處理。當父類加載器爲null時,會使用啓動類加載器 BootstrapClassLoader 作爲父類加載器

在這裏插入圖片描述
每個類加載都有一個父類加載器 。

public class ClassLoaderDemo {
    public static void main(String[] args) {
        System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
        System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());
        System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());
    }
}
ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@1b6d3586
The GrandParent of ClassLodarDemo's ClassLoader is null

AppClassLoader的父類加載器爲ExtClassLoader ExtClassLoader的父類加載器爲null,null並不代表ExtClassLoader沒有父類加載器,而是 BootstrapClassLoader 。

雙親委派模型實現源碼分析

雙親委派模型的實現代碼非常簡單,邏輯非常清晰,都集中在 java.lang.ClassLoader 的 loadClass() 中,相關代碼如下所示。

private final ClassLoader parent; 
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
    	synchronized (getClassLoadingLock(name)) {
        // 首先,檢查請求的類是否已經被加載過
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {//父加載器不爲空,調用父加載器loadClass()方法處理
                    c = parent.loadClass(name, false);
                } else {//父加載器爲空,使用啓動類加載器 BootstrapClassLoader 加載
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
               //拋出異常說明父類加載器無法完成加載請求
            }
            
            if (c == null) {
                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程序的穩定運行,可以避免類的重複加載JVM 區分不同類的方式不僅僅根據類名,相同的類文件被不同的類加載器加載產生的是兩個不同的類),也保證了 Java 的核心 API 不被篡改如果沒有使用雙親委派模型,而是每個類加載器加載自己的話就會出現一些問題,比如我們編寫一個稱爲 java.lang.Object 類的話,那麼程序運行的時候,系統就會出現多個不同的 Object 類

自定義類加載器

除了 BootstrapClassLoader 其他類加載器均由 Java 實現且全部繼承自java.lang.ClassLoader。如果我們要自定義自己的類加載器,需要繼承 ClassLoader

public class ClassLoadDemo{

    public static void main(String[] args) throws Exception {

        ClassLoader clazzLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String clazzName = name.substring(name.lastIndexOf(".") + 1) + ".class";

                    InputStream is = getClass().getResourceAsStream(clazzName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };

        String currentClass = "com.classloader.ClassLoadDemo";
        Class<?> clazz = clazzLoader.loadClass(currentClass);
        Object obj = clazz.newInstance();

        System.out.println(obj.getClass());
        System.out.println(obj instanceof com.classloader.ClassLoadDemo);
		//class com.classloader.ClassLoadDemo
		//false 即使都是來自同一個Class文件,加載器不同,仍然是兩個不同的類,所以返回值是false
    }
}

補充:Android中的ClassLoader

Android 的 Dalvik/ART 虛擬機如同標準 Java 的 JVM 虛擬機一樣,也是同樣需要加載 class 文件到內存中來使用,但是在 ClassLoader 的加載細節上會有略微的差別

Android中的dex

Android 應用打包成 apk 文件時,class 文件會被打包成一個或者多個 dex 文件。將一個 apk 文件後綴改成 .zip 格式解壓後(也可以直接解壓,apk 文件本質是個 zip 文件),裏面就有 class.dex 文件,由於 Android 的 64K 問題,使用 MultiDex 就會生成多個 dex 文件

Android 中的 Dalvik/ART 無法像 JVM 那樣 直接 加載 class 文件和 jar 文件中的 class,需要通過 dx 工具來優化轉換成 Dalvik byte code 纔行,只能通過 dex 或者 包含 dex 的jar、apk 文件來加載 ,因此 Android 中的 ClassLoader 工作就交給了 BaseDexClassLoader 來處理

BaseDexClassLoader 及其子類

在 Android 開發者官網上的 ClassLoader 的文檔說明中我們可以看到,ClassLoader 是個抽象類,其具體實現的子類有 BaseDexClassLoader 和 SecureClassLoader

SecureClassLoader 的子類是 URLClassLoader ,其只能用來加載 jar 文件,這在 Android 的 Dalvik/ART 上沒法使用的。

BaseDexClassLoader 的子類是 PathClassLoader 和 DexClassLoader

PathClassLoader

PathClassLoader 在應用啓動時創建,從 data/app/… 安裝目錄下加載 apk 文件

public PathClassLoader(String dexPath, ClassLoader parent) {
    super(dexPath, null, null, parent);
}

public PathClassLoader(String dexPath, String libraryPath,
        ClassLoader parent) {
    super(dexPath, null, libraryPath, parent);
}

他有兩個構造方法。

  • dexPath : 包含 dex 的 jar 文件或 apk 文件的路徑集,多個以文件分隔符分隔,默認是“:”
  • libraryPath : 包含 C/C++ 庫的路徑集,多個同樣以文件分隔符分隔,可以爲空

PathClassLoader 裏面除了這 2 個構造方法以外就沒有其他的代碼了,具體的實現都是在 BaseDexClassLoader 裏面,其 dexPath 比較受限制,一般是已經安裝應用的 apk 文件路徑

在 Android 中,App 安裝到手機後,apk 裏面的 class.dex 中的 class 均是通過 PathClassLoader 來加載的

DexClassLoader

A class loader that loads classes from .jar and .apk filescontaining a classes.dex entry. This can be used to execute code notinstalled as part of an application.

對比 PathClassLoader 只能加載已經安裝應用的 dex 或 apk 文件,DexClassLoader 則沒有此限制,可以從 SD 卡上加載包含 class.dex 的 .jar 和 .apk 文件,這也是插件化和熱修復的基礎,在不需要安裝應用的情況下,完成需要使用的 dex 的加載。

構造方法

public DexClassLoader(String dexPath, String optimizedDirectory,
        String libraryPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
  • String dexPath : 包含 class.dex 的 apk、jar 文件路徑 ,多個用文件分隔符(默認是 :)分隔

  • String optimizedDirectory : 用來緩存優化的 dex 文件的路徑,即從 apk 或 jar 文件中提取出來的 dex 文件。該路徑不可以爲空,且應該是應用私有的,有讀寫權限的路徑(實際上也可以使用外部存儲空間,但是這樣的話就存在代碼注入的風險)。

  • String libraryPath : 存儲 C/C++ 庫文件的路徑集

  • ClassLoader parent : 父類加載器,遵從雙親委託模型

BaseDexClassLoader

PathClassLoader 和 DexClassLoader,這兩者都是對 BaseDexClassLoader 的一層簡單封裝,真正的實現都在 BaseDexClassLoader 內。

BaseDexClassLoader 有個很重要的字段private final DexPathList pathList他繼承ClassLoader,findClass()、findResource()均是基於pathList實現的。

 @Override
 protected Class<?> findClass(String name) throws ClassNotFoundException {
     List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
     Class c = pathList.findClass(name, suppressedExceptions);
     ...
     return c;
 }
 @Override
 protected URL findResource(String name) {
     return pathList.findResource(name);
 }
 @Override
 protected Enumeration<URL> findResources(String name) {
     return pathList.findResources(name);
 }
 @Override
 public String findLibrary(String name) {
     return pathList.findLibrary(name);
 }

DexPathList的構造方法和前面介紹的類似,接受之前傳進來的包含 dex 的 apk/jar/dex 的路徑集、native 庫的路徑集和緩存優化的 dex 文件的路徑

public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ...
}

然後調用 makePathElements() 方法生成一個 Element[] dexElements 數組,Element 是 DexPathList 的一個嵌套類,其有以下字段

static class Element {
	private final File dir;
	private final boolean isDirectory;
	private final File zip;
	private final DexFile dexFile;
	private ZipFile zipFile;
	private boolean initialized;
}

makePathElements() 生成 Element 數組的過程

private static Element[] makePathElements(List<File> files, File optimizedDirectory,
                                          List<IOException> suppressedExceptions) {
    List<Element> elements = new ArrayList<>();
    // 遍歷所有的包含 dex 的文件
    for (File file : files) {
        File zip = null;
        File dir = new File("");
        DexFile dex = null;
        String path = file.getPath();
        String name = file.getName();
        // 判斷是不是 zip 類型
        if (path.contains(zipSeparator)) {
            String split[] = path.split(zipSeparator, 2);
            zip = new File(split[0]);
            dir = new File(split[1]);
        } else if (file.isDirectory()) {
            // 如果是文件夾,則直接添加 Element,這個一般是用來處理 native 庫和資源文件
            elements.add(new Element(file, true, null, null));
        } else if (file.isFile()) {
            // 直接是 .dex 文件,而不是 zip/jar 文件(apk 歸爲 zip),則直接加載 dex 文件
            if (name.endsWith(DEX_SUFFIX)) {
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else {
                // 如果是 zip/jar 文件(apk 歸爲 zip),則將 file 值賦給 zip 字段,再加載 dex 文件
                zip = file;
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                    suppressedExceptions.add(suppressed);
                }
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + file);
        }
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(dir, false, zip, dex));
        }
    }
    // list 轉爲數組
    return elements.toArray(new Element[elements.size()]);
}

loadDexFile() 方法最終會調用 JNI 層的方法來讀取 dex 文件。

DexPathList 的 findClass()方法,傳入的完整的類名來加載對應的 class:

public Class findClass(String name, List<Throwable> suppressed) {
	// 遍歷 dexElements 數組,依次尋找對應的 class,一旦找到就終止遍歷
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    // 拋出異常
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
} 

這裏有關於熱修復實現的一個點,就是將補丁 dex 文件放到 dexElements 數組前面,這樣在加載 class 時,優先找到補丁包中的 dex 文件,加載到 class 之後就不再尋找,從而原來的 apk 文件中同名的類就不會再使用,從而達到修復的目的。

BaseDexClassLader 尋找 class 的路線

  • 當傳入一個完整的類名,調用 BaseDexClassLader 的 findClass(String name)方法
  • BaseDexClassLader 的 findClass 方法會交給 DexPathList 的findClass(String name, List<Throwable> suppressed方法處理
  • 在 DexPathList 方法的內部,會遍歷 dexFile ,通過 DexFile 的 dex.loadClassBinaryName(name, definingContext, suppressed)來完成類的加載
    實際使用

需要注意的是,在項目中使用 BaseDexClassLoader 或者 DexClassLoader 去加載某個 dex 或者 apk 中的 class 時,是無法調用 findClass() 方法的,因爲該方法是包訪問權限,你需要調用 loadClass(String className) ,該方法其實是 BaseDexClassLoader 的父類 ClassLoader 內實現的:

public Class<?> loadClass(String className) throws ClassNotFoundException {
    return loadClass(className, false);
}

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    Class<?> clazz = findLoadedClass(className);
    if (clazz == null) {
        ClassNotFoundException suppressed = null;
        try {
            clazz = parent.loadClass(className, false);
        } catch (ClassNotFoundException e) {
            suppressed = e;
        }
        if (clazz == null) {
            try {
                clazz = findClass(className);
            } catch (ClassNotFoundException e) {
                e.addSuppressed(suppressed);
                throw e;
            }
        }
    }
    return clazz;
}

上面這段代碼結合之前提到的雙親委託模型就很好理解了,先查找當前的 ClassLoader 是否已經加載過,如果沒有就交給父 ClassLoader 去加載,如果父 ClassLoader 沒有找到,才調用當前 ClassLoader 來加載,此時就是調用上面分析的 findClass() 方法了。

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