JVM必備基礎知識(二)-- 類載入器和雙親委派模型

ClericYi發表於2020-02-21

JVM必備基礎知識(二)-- 類載入器和雙親委派模型

本章內容是對《深入理解Java虛擬機器:JVM高階特性和最佳實踐》的理解和概括。

前言

在上文中我們已經講了類的載入機制,這一章的主角就是類載入器和雙親委派模型了。

類載入器

在Java虛擬機器中,類載入器十分重要。每一個類的載入,都需要通過一個類的載入器。但是如果我們建立一個屬於自己的類載入器,這個時候會出現一個什麼樣的情況呢? 接下來,我們用程式碼來進行驗證測試。

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    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);
                }
            }
        };
        // 由自己的類載入器建立物件
        Object obj = myLoader.loadClass("XuNiJi.ClassLoaderTest").newInstance();
        // 由系統提供的類載入器載入建立物件
        Object obj1 = ClassLoader.getSystemClassLoader().loadClass("XuNiJi.ClassLoaderTest").newInstance();
        System.out.println(obj instanceof ClassLoaderTest);
        System.out.println(obj1 instanceof ClassLoaderTest);
    }
}
複製程式碼

JVM必備基礎知識(二)-- 類載入器和雙親委派模型
從這裡想來已經能夠看出了,由一個類載入器統一建立的類,才存在可比性。因為類載入器是擁有獨立的類名稱空間的。更簡單的說,就像上面的例子,如果不使用Java虛擬機器提供的類載入器,你就會失去一大部分功能,比如equals()isAssignableFrom()isInstance()instanceof。如果要相同,除非你直接在java原始碼上動手腳。

雙親委派模型

第一個問題:為什麼需要這個模型? 其實這個模型的提出,就是為了解決類載入器可能不出現不同的問題。因為即便是相同的class,由不同的類載入器載入時,結果就是不同的。

工作原理

雙親委派的工作流程非常簡單,這就跟之前文章裡的Android的事件分發機制一樣,向上傳遞,由上一層的載入器先行嘗試消費,如果上一層無法完成這個任務,那麼子載入器就要由自己動手完成。

JVM必備基礎知識(二)-- 類載入器和雙親委派模型

  1. 啟動類載入器:負責載入/lib下的類。
  2. 擴充套件類載入器:負責載入/lib/ext下的類。
  3. 系統類載入器/應用程式類載入器:ClassLoader.getSystemClassLoader返回的就是它。

通過上圖我們可以知道,子載入器不斷的給上一層載入器傳遞載入請求,那麼這個時候啟動類載入器勢必是接受到過全部的載入請求的。如果不信,我們就用原始碼來證明。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 判斷Class是否被載入過
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果父類丟擲ClassNotFoundException
                    // 說明父類無法完成載入
                }
                // 這個時候c依舊為null,說明父類載入不了
                // 那沒有辦法,只能子載入器自己效勞了
                if (c == null) {
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
}
複製程式碼

講完了他的工作原理,自然就要知道,他能夠如何被破壞的了。

破壞雙親委派模型

第二個問題,為什麼要破壞雙親委派? 拿最簡單的例子,在上文中我們,提到過各個資源的載入範圍,但是Driver作為後來才加入的一個介面,他的很多api是由第三方服務商開發的。那麼這個時候,破壞雙親委派就有了他的用武之地了,當然這只是他的用處之一。

下面來介紹,他是如何破壞雙親委派的。 先看看我們平時都是怎麼用的。(當然這是很基礎的寫法了,因為現在池的概念加深,所以很多事情都已經被封裝了。)

String url = "jdbc:mysql://localhost:3306/db";
Connection conn = DriverManager.getConnection(url, "root", "root"); 
複製程式碼

上面很明顯就能看出這件事情就是關於DriverManager展開的了。

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}
複製程式碼

這裡根據前一章的內容先要對DriverManager進行初始化,也就是呼叫了一個loadInitialDrivers()函式。

private static void loadInitialDrivers() {
        .....
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);// 1
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
        .....
}
複製程式碼

從這一小段中,我們關注註釋1能夠知道他專門去訪問了一個ServiceLoader的類,點進去之後我們能夠發現這麼三段程式碼。

// 1
public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
}

// 2
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
        return new ServiceLoader<>(service, loader);
    }

// 3
private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
}
複製程式碼

由1 --> 2 --> 3的順序循序漸進,你是否已經和我關注到一個問題了!! ClassLoader.getSystemClassLoader(),看到這個函式了嗎,我在上文提到過,這個函式我們獲得的類載入器將會是應用程式類載入器。也就是說我們的任務不會再向上傳遞了,到頭就是到了應用程式類載入器這個位置,那麼雙親委派模型也就破壞了。

以上就是打破雙親委派的方法之一的介紹了。

溯源ClassLoader.getSystemClassLoader()

為什麼說我們呼叫的是應用程式類載入器呢? 接下來直接從原始碼來解析了。 首先就是呼叫getSystemClassLoader()這個函式了

JVM必備基礎知識(二)-- 類載入器和雙親委派模型

這張圖裡我們只用關注圈紅的函式。

JVM必備基礎知識(二)-- 類載入器和雙親委派模型

然後在initSystemClassLoader()函式中呼叫了一個Launcher的類。

JVM必備基礎知識(二)-- 類載入器和雙親委派模型

Launcher整個類的建立,想來讀者也已經看到loader這個變數了,通過getAppClassLoader()這個函式所建立的loader也就是我們口中所說的應用程式類載入器了。

以上就是我的學習成果,如果有什麼我沒有思考到的地方或是文章記憶體在錯誤,歡迎與我分享。


相關文章推薦:

JVM必備基礎知識(一)-- 類的載入機制

JVM必備基礎知識(三)-- GC垃圾回收機制

相關文章