一篇文章讀懂Java類載入器

来源:http://www.cnblogs.com/aishangJava/archive/2017/09/03/7471409.html
-Advertisement-
Play Games

Java類載入器算是一個老生常談的問題,大多Java工程師也都對其中的知識點倒背如流,最近在看源碼的時候發現有一些細節的地方理解還是比較模糊,正好寫一篇文章梳理一下。 關於Java類載入器的知識,網上一搜一大片,我自己也看過很多文檔,博客。資料雖然很多,但還是希望通過本文儘量寫出一些自己的理解,自己 ...


Java類載入器算是一個老生常談的問題,大多Java工程師也都對其中的知識點倒背如流,最近在看源碼的時候發現有一些細節的地方理解還是比較模糊,正好寫一篇文章梳理一下。

關於Java類載入器的知識,網上一搜一大片,我自己也看過很多文檔,博客。資料雖然很多,但還是希望通過本文儘量寫出一些自己的理解,自己的東西。如果只是重覆別人寫的內容那就失去寫作的意義了。

類載入器結構

類載入器結構

名稱解釋:

  1. 根類載入器,也叫引導類載入器、啟動類載入器。由於它不屬於Java類庫,這裡就不說它對應的類名了,很多人喜歡稱BootstrapClassLoader。本文都稱之為根類載入器。
    載入路徑:<JAVA_HOME>\lib
  2. 擴展類載入器,對應Java類名為ExtClassLoader,該類是sun.misc.Launcher的一個內部類。
    載入路徑:<JAVA_HOME>\lib\ext
  3. 應用類載入器,對應Java類名為AppClassLoader,該類是sun.misc.Launcher的一個內部類。
    載入路徑:用戶目錄
//可以通過這種方式列印載入路徑
System.out.println("boot:"+System.getProperty("sun.boot.class.path"));
System.out.println("ext:"+System.getProperty("java.ext.dirs"));
System.out.println("app:"+System.getProperty("java.class.path"));

重點說明:

  1. 根類載入器對於普通Java工程師來講可以理解成一個概念上的東西,因為我們無法通過Java代碼獲取到根類載入器,它屬於JVM層面。
  2. 除了根類載入器之外,其他兩個擴展類載入器和應用類載入器都是通過類sun.misc.Launcher進行初始化,而Launcher類則由根類載入器進行載入。

看下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 {
            //初始化應用類載入器,註意這裡的入參就是擴展類載入器
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        //設置上下文類載入器,這個後面會詳細說
        Thread.currentThread().setContextClassLoader(this.loader);

       //刪除了一些安全方面的代碼
       //...
}

雙親委派模型

雙親委派模型是指當我們調用類載入器的loadClass方法進行類載入時,該類載入器會首先請求它的父類載入器進行載入,依次遞歸。如果所有父類載入器都載入失敗,則當前類載入器自己進行載入操作。
邏輯很簡單,通過ClassLoader類的源碼來分析一下。

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) {
                        //如果當前類沒有被載入且父類載入器不為null,則請求父類載入器進行載入操作
                        c = parent.loadClass(name, false);
                    } else {
                       //如果當前類沒有被載入且父類載入器為null,則請求根類載入器進行載入操作
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

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

雙親委派模型的實現邏輯總體看還是非常簡單明瞭的。
這裡有幾個細節需要說明:

  1. ClassLoader類是一個抽象類,但卻沒有包含任何抽象方法。
  2. 如果要實現自己的類載入器且不破壞雙親委派模型,只需要繼承ClassLoader類並重寫findClass方法。
  3. 如果要實現自己的類載入器且破壞雙親委派模型,則需要繼承ClassLoader類並重寫loadClass,findClass方法。

令人疑惑的系統類載入器

當你把上面的知識都搞清楚以後,會發現ClassLoader類中有個方法是getSystemClassLoader,系統類載入器,這又是什麼?
系統類載入器是個容易讓人混淆的概念,我一度以為它就是應用類載入器的別名,就跟啟動類載入器和根類載入器道理一樣。事實上,預設情況下我們通過ClassLoader.getSystemClassLoader()獲取到的系統類載入器也確實是應用類載入器
很多資料在說類載入器結構的時候會直接把應用類載入器說成是系統類載入器,其實我們通過類名就可以判斷兩個不是一回事。
系統類載入器可以通過System.setProperty("java.system.class.loader", xxx類名)進行自定義設置。
系統類載入器不是一個全新的載入器,它只是一個概念,本質上還是上述說的四大類載入器(把用戶自定義類載入器算進去),至於提出這個概念的原因以及使用場景,還需要繼續考究。

被人忽略的上下文類載入器

上面討論了各個類載入器的載入路徑。鑒於雙親委派模型的設計,子類載入器都保留了父類載入器的引用,也就是說當由子類載入器載入的類需要訪問由父類載入器載入的類時,毫無疑問是可以訪問到的。但考慮一種場景,會不會有父類載入器載入的類需要訪問子類載入器載入的類這種情況?如果有,怎麼解決(父類載入器並沒有子類載入器的引用)?
這就是我們要討論的常常被人們忽略的上下文類載入器。
經典案例:
JDBC是Java制定的一套訪問資料庫的標準介面,它包含在Java基礎類庫中,也就是說它是由根類載入器載入的。與此同時,各個資料庫廠商會各自實現這套介面來讓Java工程師可以訪問自己的資料庫,而這部分實現類庫是需要Java工程師在工程中作為一個第三方依賴引入使用的,也就是說這部分實現類庫是由應用類載入器進行載入的。
先上一段Java獲取Mysql連接的代碼:

//載入驅動程式
Class.forName("com.mysql.jdbc.Driver");
//連接資料庫
Connection conn = DriverManager.getConnection(url, user, password);

這裡DriverManager類就屬於Java基礎類庫,由根類載入器載入。我們可以通過它獲取到資料庫的連接,顯然是它通過com.mysql.jdbc.Driver驅動成功連接到了資料庫,上面也說了資料庫驅動(作為第三方類庫引入)是由應用類載入器載入的。這個場景就是典型的由父類載入器載入的類需要訪問由子類載入器載入的類。
Java是怎麼實現這種逆向訪問的呢?直接看DriverManager類的源碼:

//建立資料庫連接各個不同參數的方法最終都會走到這裡
private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        //獲取調用者的類載入器
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            //如果為null,則使用上下文類載入器
            //這裡是重點,什麼時候類載入器才會為null? 當然就是由根類載入器載入的類了
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        //...省略

        for(DriverInfo aDriver : registeredDrivers) {
            //使用上下文類載入器去載入驅動
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    //如果載入成功,則進行連接
                    Connection con = aDriver.driver.connect(url, info);
                    //...
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }
            } 
            //...
        }
    }

重點說明:
為什麼上下文類載入器就可以載入到資料庫驅動呢?回到上面一開始Launcher初始化類載入器的源碼,我們發現原來所謂的上下文類載入器本質上就是應用類載入器,有沒有豁然開朗的感覺?上下文類載入器只是為瞭解決類的逆向訪問提出來的一個概念,並不是一個全新的類載入器,它本質上就是應用類載入器


基本上我理解的Java類載入器就這麼多知識,如果有沒提到的或者是錯誤的地方,歡迎交流。

 

Java學習交流QQ群:523047986  禁止閑聊,非喜勿進!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 開篇先來說一下我和來畫的故事,以及寫這篇文章的初衷。 今年年初時,我還在北京,在 Face++,做著人臉識別技術的 Windows 和 Android 端,做著人工智慧終將實現世間所有美好的夢。這時的我已經離開 UWP,甚至 C# 很久了,寫著 C++ 和 Java,當時真的沒想過會再次回到 UWP ...
  • 自己畫一個轉圈圈的控制項 ...
  • 首先說一下foreach有的也叫增強for迴圈,foreach其實是for迴圈的一個特殊簡化版。 再說一下foreach的書寫格式: for(元素類型 元素名稱 : 遍曆數組(集合)(或者能進行迭代的)){ 語句 } foreach雖然是for迴圈的簡化版本,但是並不是說foreach就比for更好 ...
  • 特別聲明本隨筆copy於egon(林海峰)。 一 IO模型介紹 為了更好地瞭解IO模型,我們需要事先回顧下:同步、非同步、阻塞、非阻塞 同步(synchronous) IO和非同步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分別是什麼,到底有 ...
  • 建立資料庫訪問類的封裝 <?php class DBDA { public $host = "localhost"; //伺服器地址 public $uid = "root"; //資料庫的用戶名 public $pwd = ""; //資料庫的密碼 public $dbname = "";//數據 ...
  • Spring與SpringMVC整合! 問:實際上SpringMVC就運行在Spring環境之下,還有必要整合麽?SpringMVC和Spring都有IOC容器,是不是都需要保留呢? 答案是:通常情況下,類似於數據源,事務,整合其他框架都是放在spring的配置文件中(而不是放在SpringMVC的 ...
  • 冒泡排序是一種非常常見的排序演算法。如同水中的一排泡泡,先冒出最大的一個泡泡。再冒出剩餘泡泡中的最大泡泡,依次類推,它的排序規則如下: 1. 從第一個元素開始,比較相鄰的兩個元素,如果後面的小於前面的,交換兩個的位置,一直比較到最後一個 2. 迴圈1中的操作,但已經確定的最大的元素不再參與比較 3. ...
  • 多線程 前言 我看了不止一個人說多線程是雞肋,但是就依照我個人覺得多線程在一些小型的爬蟲中還是可以顯著的提高速度的,相比多進程來說應該還是挺簡單的 使用多線程 繼承threading.Thread 繼承threading.Thread模塊是一個很好的一個選擇,就像java中也是可以繼承類和實現介面一 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...