Java類加載機制源碼分析

Java代碼首先要編譯成class文件字節碼,在運行時通過JIT(即時編譯器)編譯成本地機器碼,最後由ClassLoader將其加載解析成Class對象到內存中。通過ClassLoader的loadClass方法的源碼加深對Java類加載機制的理解。


1. ClassLoader加載機制簡述

Java的類加載遵循父類優先的原則,也就是說ClassLoader是一個有層級的樹形組合體系,並且一個ClassLoader要加載一個類,首先逐層向上檢查是否有加載器加載過該類,如果有,將結果逐層返回到下級。如果沒有,繼續檢查直到有一層ClassLoader返回沒有加載並且它不應該加載,那麼該層的下一層就可以加載該類。


PS:父類優先的方式也不是萬能的,在JavaEE Web應用程序中,也會使用子女優先加載的方式;


1.1 JVM提供3層基本的類加載平臺

BootstrapClassLoader:加載JVM自身需要的類,注意在Hotspot JVM中它嚴格來說不是JVM類加載體系中的,它並不遵循上述機制,也不是下面ExtClassLoader的父類加載器;

ExtClassLoader:加載特定的類:System.getProperty("java.ext.dirs");也就是JRE/LIB/EXT目錄下的類,加載的是sun公司的一些擴展包,它是AppClassLoader的父類加載器;

AppClassLoader:加載System.getProperty("java.class.path");就是classpath,看到這個你可能已經知道eclipse項目下.classpath的作用了,就是告訴AppClassLoader這些類由它加載;

繼承自URLClassLoader的自定義類加載器,通過調用getSystemClassLoader獲取自己的父加載器(AppClassLoader);


ClassLoader的類層次結構:



圖中的AppClassLoader和ExtClassLoader是Launcher的內部類;

到現在,我們可以也可以看出Java的ClassLoader使用了職責鏈設計模式,父優先加載,一定程度上保證了程序安全(防止惡意代碼替換JSE核心類)。


1.2 JVM加載Class文件到內存的方式:

一是隱式加載:繼承或引用某個類時,有JVM負責加載;

二是顯式加載:在代碼中調用loadClass(),Class.forName,ClassLoader的findClass方法等,顯式加載中也可能包含隱式加載;


2. ClassLoader的重要方法:

findClass:主要由URLClassLoader實現,根據URLClassPath去指定地方查找class文件;取得要加載class文件的字節流;

defineCLass:可以將字節流解析成Class對象,該Class對象並未進行resolve;

resolveClass:對Class對象進行Link,載入引用類(超類,接口字段,方法簽名,方法中的本地變量);

loadClass:採用默認的加載邏輯根據類名加載一個類,返回Class對象,調用前面3個方法實現;


3. 加載class文件的過程:

3.1 加載字節碼到內存:findClass和defineClass方法

首先來看看URLClassLoader中的findClass方法,因爲加載的第一步,找到並獲取指定class文件的字節流;
protected Class<?> findClass(final String name)
            throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            //獲取特權,確保有權限可以讀取到資源,這裏
            result = AccessController.doPrivileged(
                    new PrivilegedExceptionAction<Class<?>>() {
                        public Class<?> run() throws ClassNotFoundException {
                            //將完整的類名轉換成文件路徑格式
                            String path = name.replace('.', '/').concat(".class");
                            //在指定的URLPath中獲取對應的class文件資源
                            Resource res = ucp.getResource(path, false);
                            if (res != null) {
                                try {
                                    //獲取成功將資源傳入,最終獲取未解析的Class對象
                                    return defineClass(name, res);
                                } catch (IOException e) {
                                    //不能成功讀取文件內容
                                    throw new ClassNotFoundException(name, e);
                                }
                            } else {
                                //不能獲取資源
                                return null;
                            }
                        }
                    }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            //因爲要在特權操作中拋出ClassNotFoundException,使用了PrivilegedExceptionAction回調
            //它會將異常包裝,這裏要解除包裝
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

findClass方法的作用就是找到並藉助defineClass方法返回Class對象(未解析),它獲取特權權限去讀取資源(Java安全模型對不同的代碼是區分Domain的,不同的域(比如不同的項目的代碼)可能對其他域的代碼限制了訪問自身資源的權限,具體可以參看http://www.ibm.com/developerworks/cn/java/j-lo-javasecurity/),並保證了在不同情況下可以正確的拋出ClassNotFoundException;

接下來看看findClass中使用的defineClass方法:
private Class<?> defineClass(String name, Resource res) throws IOException {
        long t0 = System.nanoTime();
        int i = name.lastIndexOf('.');
        URL url = res.getCodeSourceURL();
        //首先加載包
        if (i != -1) {
            String pkgname = name.substring(0, i);
            // Check if package already loaded.
            Manifest man = res.getManifest();
            definePackageInternal(pkgname, man, url);
        }
        // Now read the class bytes and define the class
        //使用nio,獲取字節緩衝區
        java.nio.ByteBuffer bb = res.getByteBuffer();
        if (bb != null) {
            // Use (direct) ByteBuffer:
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, bb, cs);
        } else {
            //獲取不到緩衝區,直接InputStream獲取字節數組
            byte[] b = res.getBytes();
            // must read certificates AFTER reading bytes.
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, b, 0, b.length, cs);
        }
    }

在這個方法中,我們可以看到對於class文件的讀取策略,其中CodeSigner和CodeSource分別是代碼簽名和代碼源,它們組合使用與前面提及的保護域機制(ProtectionDomain)當中,可見Java中類加載機制和安全模型是密不可分的。

3.2 驗證和解析:defineClass方法和resovleClass方法:

defineClass首先進行:
(1)字節碼驗證;
(2)類準備:準備類中每個字段、方法和實現接口所需的數據結構
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
            throws ClassFormatError
    {
        //首先檢查類名;阻止加載“java.”開頭包內的類(應有BootStrapLoader加載);
        //確保同一個包內的Class擁有相同的證書
        protectionDomain = preDefineClass(name, protectionDomain);
        //根據CodeSource獲取一個URL的字符串表示
        String source = defineClassSourceLocation(protectionDomain);
        //字節碼驗證;類準備(準備字段,方法,實現接口所必需的數據結構);
        Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
        //通過證書爲該類設置簽名
        postDefineClass(c, protectionDomain);
        return c;
    }

(3)解析:resolveClass方法,直接通過一個native方法實現
進行LINK,裝入引用類,如超類,接口,字段,方法中使用的本地變量。

3.3 顯式加載時的過程:

調用this.getClass().getClassLoader().loadClass("className");時的過程:

首先看看URLClassLoader中的loadClass方法:

protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        //該getClassLoadingLock獲取同步鎖,該鎖用併發Map保存(享元模式)
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 這裏體現了類加載機制中的父優先的查找機制
                    // 通過上濾直到parent爲null
                    // 這時再去BootstrapClassLoader中查找,在運行用戶程序時,這一步一般都是null
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                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;
        }
    }

反映了JVM類加載機制的過程爲:加載一個新類,逐層上濾檢查父加載器,直到頂層(parent==null),再去BootStrapClassLoader中查找,如果返回null,就要調用findClass,resolveCass(需要的話);

3.4 初始化Class對象:

類的靜態字段,靜態初始化器會按順序賦值/執行;

上面的過程概括一下:(1)加載class文件字節碼到內存——>(2)將字節碼轉換成未Link的Class對象(安全驗證/字節碼驗證/類準備)——>(3)resolveClass方法Link
——>(4)類初始化;

4. 小結

本篇總結了一下Java類加載機制的機制和過程,並從源碼角度加以分析。在分析源碼的過程也看到了Java中的安全模型的應用,以及併發控制(findClass中的lock併發Map)







發佈了40 篇原創文章 · 獲贊 10 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章