阿裡面試題,深入理解Java類載入機制

来源:https://www.cnblogs.com/yuxiang1/archive/2019/04/16/10720254.html
-Advertisement-
Play Games

類的生命周期 包括以下 7 個階段: 載入(Loading) 驗證(Verification) 準備(Preparation) 解析(Resolution) 初始化(Initialization) 使用(Using) 卸載(Unloading) 其中解析過程在某些情況下可以在初始化階段之後再開始,這 ...


類的生命周期

包括以下 7 個階段:

  • 載入(Loading)
  • 驗證(Verification)
  • 準備(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸載(Unloading)

其中解析過程在某些情況下可以在初始化階段之後再開始,這是為了支持 Java 的動態綁定。

類初始化時機

虛擬機規範中並沒有強制約束何時進行載入,但是規範嚴格規定了有且只有下列五種情況必須對類進行初始化(載入、驗證、準備都會隨著發生):

  • 遇到 new、getstatic、putstatic、invokestatic 這四條位元組碼指令時,如果類沒有進行過初始化,則必須先觸發其初始化。最常見的生成這 4 條指令的場景是:使用 new 關鍵字實例化對象的時候;讀取或設置一個類的靜態欄位(被 final 修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候;以及調用一個類的靜態方法的時候。

  • 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行初始化,則需要先觸發其初始化。

  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

  • 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類;

  • 當使用 JDK 1.7 的動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最後的解析結果為 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化;

以上 5 種場景中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用。被動引用的常見例子包括:

  • 通過子類引用父類的靜態欄位,不會導致子類初始化。
System.out.println(SubClass.value); // value 欄位在 SuperClass 中定義
  • 通過數組定義來引用類,不會觸發此類的初始化。該過程會對數組類進行初始化,數組類是一個由虛擬機自動生成的、直接繼承自 Object 的子類,其中包含了數組的屬性和方法。
SuperClass[] sca = new SuperClass[10];
  • 常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
System.out.println(ConstClass.HELLOWORLD);

類載入過程

包含了載入、驗證、準備、解析和初始化這 5 個階段。

1. 載入

載入是類載入的一個階段,註意不要混淆。

載入過程完成以下三件事:

  • 通過一個類的全限定名來獲取定義此類的二進位位元組流。
  • 將這個位元組流所代表的靜態存儲結構轉化為方法區的運行時存儲結構。
  • 在記憶體中生成一個代表這個類的 Class 對象,作為方法區這個類的各種數據的訪問入口。

其中二進位位元組流可以從以下方式中獲取:

  • 從 ZIP 包讀取,這很常見,最終成為日後 JAR、EAR、WAR 格式的基礎。
  • 從網路中獲取,這種場景最典型的應用是 Applet。
  • 運行時計算生成,這種場景使用得最多得就是動態代理技術,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理類的二進位位元組流。
  • 由其他文件生成,典型場景是 JSP 應用,即由 JSP 文件生成對應的 Class 類。
  • 從資料庫讀取,這種場景相對少見,例如有些中間件伺服器(如 SAP Netweaver)可以選擇把程式安裝到資料庫中來完成程式代碼在集群間的分發。 ...

2. 驗證

確保 Class 文件的位元組流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

  • 文件格式驗證:驗證位元組流是否符合 Class 文件格式的規範,並且能被當前版本的虛擬機處理。
  • 元數據驗證:對位元組碼描述的信息進行語義分析,以保證其描述的信息符合 Java 語言規範的要求。
  • 位元組碼驗證:通過數據流和控制流分析,確保程式語義是合法、符合邏輯的。
  • 符號引用驗證:發生在虛擬機將符號引用轉換為直接引用的時候,對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗。

3. 準備

類變數是被 static 修飾的變數,準備階段為類變數分配記憶體並設置初始值,使用的是方法區的記憶體。

實例變數不會在這階段分配記憶體,它將會在對象實例化時隨著對象一起分配在 Java 堆中。(實例化不是類載入的一個過程,類載入發生在所有實例化操作之前,並且類載入只進行一次,實例化可以進行多次)

初始值一般為 0 值,例如下麵的類變數 value 被初始化為 0 而不是 123。

public static int value = 123;

如果類變數是常量,那麼會按照表達式來進行初始化,而不是賦值為 0。

public static final int value = 123;

4. 解析

將常量池的符號引用替換為直接引用的過程。

5. 初始化

初始化階段才真正開始執行類中的定義的 Java 程式代碼。初始化階段即虛擬機執行類構造器 <clinit>() 方法的過程。

在準備階段,類變數已經賦過一次系統要求的初始值,而在初始化階段,根據程式員通過程式制定的主觀計划去初始化類變數和其它資源。

<clinit>() 方法具有以下特點:

  • 是由編譯器自動收集類中所有類變數的賦值動作和靜態語句塊(static{} 塊)中的語句合併產生的,編譯器收集的順序由語句在源文件中出現的順序決定。特別註意的是,靜態語句塊只能訪問到定義在它之前的類變數,定義在它之後的類變數只能賦值,不能訪問。例如以下代碼:
public class Test {
    static {
        i = 0;                // 給變數賦值可以正常編譯通過
        System.out.print(i);  // 這句編譯器會提示“非法向前引用”
    }
    static int i = 1;
}
  • 與類的構造函數(或者說實例構造器 <init>())不同,不需要顯式的調用父類的構造器。虛擬機會自動保證在子類的 <clinit>() 方法運行之前,父類的 <clinit>() 方法已經執行結束。因此虛擬機中第一個執行 <clinit>() 方法的類肯定為 java.lang.Object。

  • 由於父類的 <clinit>() 方法先執行,也就意味著父類中定義的靜態語句塊要優於子類的變數賦值操作。例如以下代碼:

static class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
}

static class Sub extends Parent {
    public static int B = A;
}

public static void main(String[] args) {
     System.out.println(Sub.B);  // 輸出結果是父類中的靜態變數 A 的值,也就是 2。
}

 

  • <clinit>() 方法對於類或介面不是必須的,如果一個類中不包含靜態語句塊,也沒有對類變數的賦值操作,編譯器可以不為該類生成 <clinit>() 方法。

  • 介面中不可以使用靜態語句塊,但仍然有類變數初始化的賦值操作,因此介面與類一樣都會生成 <clinit>() 方法。但介面與類不同的是,執行介面的 <clinit>() 方法不需要先執行父介面的 <clinit>() 方法。只有當父介面中定義的變數使用時,父介面才會初始化。另外,介面的實現類在初始化時也一樣不會執行介面的 <clinit>() 方法。

  • 虛擬機會保證一個類的 <clinit>() 方法在多線程環境下被正確的加鎖和同步,如果多個線程同時初始化一個類,只會有一個線程執行這個類的 <clinit>() 方法,其它線程都會阻塞等待,直到活動線程執行 <clinit>() 方法完畢。如果在一個類的 <clinit>() 方法中有耗時的操作,就可能造成多個線程阻塞,在實際過程中此種阻塞很隱蔽。

類載入器

實現類的載入動作。在 Java 虛擬機外部實現,以便讓應用程式自己決定如何去獲取所需要的類。

類與類載入器

兩個類相等:類本身相等,並且使用同一個類載入器進行載入。這是因為每一個類載入器都擁有一個獨立的類名稱空間。

這裡的相等,包括類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果為 true,也包括使用 instanceof 關鍵字做對象所屬關係判定結果為 true。

類載入器分類

從 Java 虛擬機的角度來講,只存在以下兩種不同的類載入器:

  • 啟動類載入器(Bootstrap ClassLoader),這個類載入器用 C++ 實現,是虛擬機自身的一部分;

  • 所有其他類的載入器,這些類由 Java 實現,獨立於虛擬機外部,並且全都繼承自抽象類 java.lang.ClassLoader。

從 Java 開發人員的角度看,類載入器可以劃分得更細緻一些:

  • 啟動類載入器(Bootstrap ClassLoader)此類載入器負責將存放在 <JAVA_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會被載入)類庫載入到虛擬機記憶體中。啟動類載入器無法被 Java 程式直接引用,用戶在編寫自定義類載入器時,如果需要把載入請求委派給啟動類載入器,直接使用 null 代替即可。

  • 擴展類載入器(Extension ClassLoader)這個類載入器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系統變數所指定路徑中的所有類庫載入到記憶體中,開發者可以直接使用擴展類載入器。

  • 應用程式類載入器(Application ClassLoader)這個類載入器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。由於這個類載入器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般稱為系統類載入器。它負責載入用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

雙親委派模型

應用程式都是由三種類載入器相互配合進行載入的,如果有必要,還可以加入自己定義的類載入器。

下圖展示的類載入器之間的層次關係,稱為類載入器的雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啟動類載入器外,其餘的類載入器都應有自己的父類載入器。這裡類載入器之間的父子關係一般通過組合(Composition)關係來實現,而不是通過繼承(Inheritance)的關係實現。

(一)工作過程

一個類載入器首先將類載入請求傳送到父類載入器,只有當父類載入器無法完成類載入請求時才嘗試載入。

(二)好處

使得 Java 類隨著它的類載入器一起具有一種帶有優先順序的層次關係,從而是的基礎類得到統一。

例如 java.lang.Object 存放在 rt.jar 中,如果編寫另外一個 java.lang.Object 的類並放到 ClassPath 中,程式可以編譯通過。因為雙親委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 優先順序更高,因為 rt.jar 中的 Object 使用的是啟動類載入器,而 ClassPath 中的 Object 使用的是應用程式類載入器。正因為 rt.jar 中的 Object 優先順序更高,因為程式中所有的 Object 都是這個 Object。

(三)實現

以下是抽象類 java.lang.ClassLoader 的代碼片段,其中的 loadClass() 方法運行過程如下:先檢查類是否已經載入過,如果沒有則讓父類載入器去載入。當父類載入器載入失敗時拋出 ClassNotFoundException,此時嘗試自己去載入。

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;

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

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    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.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}

 

自定義類載入器實現

FileSystemClassLoader 是自定義類載入器,繼承自 java.lang.ClassLoader,用於載入文件系統上的類。它首先根據類的全名在文件系統上查找類的位元組代碼文件(.class 文件),然後讀取該文件內容,最後通過 defineClass() 方法來把這些位元組代碼轉換成 java.lang.Class 類的實例。

java.lang.ClassLoader 類的方法 loadClass() 實現了雙親委派模型的邏輯,因此自定義類載入器一般不去重寫它,而是通過重寫 findClass() 方法。

public class FileSystemClassLoader extends ClassLoader {

    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
}

 

免費Java資料需要自己領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分散式等教程,一共30G。 
傳送門: https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q


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

-Advertisement-
Play Games
更多相關文章
  • 1 數組也是一種類型 Java中要求所有的數組元素具有相同的數據類型。因此在一個數組中,數組元素的類型是唯一的,不能存儲多種類型的數據。 一旦數組的初始化完成,數組在記憶體中所占的空間將被固定下來,因此數組的長度不可以被改變。即使某個數組元素的數據被清空,他占的空間依然被保留,依然屬於該數組,數組的長 ...
  • 一、概述 當我們打開一個SqlSession的時候,我們就完成了操作資料庫的第一步,那MyBatis是如何執行Sql的呢?其實MyBatis的增刪改查都是通過Executor執行的,Executor和SqlSession綁定在一起,由Configuration類的newExecutor方法創建。 二 ...
  • 在很多時候,我們代碼中會有很多分支,而且分支下麵的代碼又有一些複雜的邏輯,相信很多人都喜歡用 if-else/switch-case 去實現。做的不好的會直接把實現的代碼放在 if-else/switch-case 的分支之下: 這樣的代碼不僅冗長,讀起來也非常困難。做的好一點的會把這些邏輯封裝成函 ...
  • 本文首發於公眾號:javaadu 簡單介紹 構建高性能的Java應用過程中,必然會遇到各種各樣的問題,像CPU飆高、記憶體泄漏、應用奔潰,以及其他疑難雜症,這時可以使用Serviceability Agent(SA)。SA是JDK提供的一個強大的調試工具集,適用於語言層和虛擬機層,支持調試運行著的Ja ...
  • 前幾天,有個同事在使用JPA的自定義SQL方法時,程式一直報異常,搗鼓了半天也沒能解決,咨詢我的時候,我看了一眼他的程式,差不多是這個樣子的: 我告訴他,你的deleteUserById方法缺少了@Modifying註解和@Transactional註解,他半信半疑地試了一下,然後果然就解決了。其實 ...
  • Python3 基本數據類型 Python 中的變數不需要聲明。每個變數在使用前都必須賦值,變數賦值以後該變數才會被創建。 在 Python 中,變數就是變數,它沒有類型,我們所說的"類型"是變數所指的記憶體中對象的類型。 等號(=)用來給變數賦值。 等號(=)運算符左邊是一個變數名,等號(=)運算符 ...
  • 1.數組的定義: 數組(Array)是相同數據類型的數據的有序集合。 2.數組的3個特點: 2.1數組長度是確定。數組一旦申請完空間,長度不能發生變化,用length屬性訪問。 2.2數組的元素都是同一數據類型。 2.3數組是有序的 。每個元素通過下標/索引標記,索引從0開始。 3.數組的3種聲明方 ...
  • 異常信息:The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone. You must configure either the server or JDBC dri ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...