一個java檔案的JVM之旅

2023-11-07 15:01:19

準備

我是小C同學編寫得一個java檔案,如何實現我的功能呢?需要去JVM(Java Virtual Machine)這個地方旅行。

變身

我高高興興的來到JVM,想要開始JVM之旅,它確說:「現在的我還不能進去,需要做一次轉換,生成class檔案才行」。為什麼這樣呢?

JVM不能直接載入java檔案的原因:

  • Java原始碼中包含了許多高階語言特性和語法,比如類、繼承、多型、例外處理等等。這些高階特性在JVM中沒有直接對應的形式,只有通過編譯器的處理才能轉化為JVM可以理解的位元組碼指令。
  • Java原始碼需要經過編譯器的編譯過程,才能生成相應的位元組碼檔案,然後再由JVM載入、解釋執行。在編譯過程中,編譯器對原始碼進行語法分析、型別檢查、優化等操作,最終生成與目標平臺相容的Java位元組碼檔案。
  • JVM只能夠載入和執行符合Java虛擬機器器規範的.class位元組碼檔案,而不能夠直接載入和執行Java原始碼檔案。

編譯

知道原因後,我又問JVM,我怎麼才能變成class檔案呢,JVM告訴我可以通過javac命令。

javac

javac 是 Java 編譯器命令,用於將 Java 原始碼檔案編譯成位元組碼檔案(.class 檔案)。

命令格式

javac [options] [source files]

  • options:為編譯選項,可以控制編譯器的行為,例如指定類路徑、生成偵錯資訊、壓縮檔案等。
  • source files:為需要編譯的 Java 原始碼檔案,可以指定多個檔案,用空格隔開。如果不指定原始碼檔案,則 `javac` 命令會在當前目錄查詢所有擴充套件名為 .java 的檔案進行編譯。

需要注意的是,`javac` 命令需要在正確設定 JDK 環境後才能使用。JDK(Java Development Kit)是 Java 開發工具包的縮寫,是 Java 應用程式開發的核心元件之一。

具體實現

編譯器在編譯原始檔時,需要對原始檔進行語法分析、語意分析和型別檢查等操作。

  • 語法分析:javac命令首先將原始檔讀入記憶體,然後進行詞法分析和語法分析。詞法分析器負責將原始檔中的字元序列轉換成一個個單詞(Token),然後語法分析器將單詞組合成可以被解釋執行的語法結構,形成抽象語法樹(AST)。
  • 語意分析:javac命令在生成AST之後,進行語意分析。語意分析器主要是為了檢查程式中是否存在語意錯誤,例如變數未定義、型別不匹配等,如果發現語意錯誤,編譯器會輸出錯誤資訊,並中止編譯過程,不會生成位元組碼檔案。
  • 型別檢查:javac命令在語意分析的基礎上,進行型別檢查。型別檢查器主要是檢查程式的型別是否匹配和相容,如果型別不匹配或不相容,編譯器會在編譯期間報告錯誤。
  • 程式碼生成:javac命令在生成抽象語法樹後,對其進行優化和轉化,最終生成位元組碼檔案。編譯器會根據目的碼的平臺和版本,生成適當的位元組碼檔案。

執行

知道怎麼變身後,我立即通過javac命令,讓自己變成可以被JVM執行的class檔案。

載入

變成class檔案後,我怎麼能進入JVM內部呢,是走著去還是坐車去呢?JVM告訴我要通過類載入器進入。

類載入器

Java類載入器是Java虛擬機器器(JVM)中的一個重要元件,它負責將類檔案(.class檔案)載入到JVM中。

分類

Java 中的類載入器是按照其載入類的特點進行分類的,主要有以下幾種型別:

  • 啟動類載入器(Bootstrap ClassLoader):負責載入 JRE/lib/rt.jar 中的核心 Java 類庫,是最頂層的類載入器,不是 Java 類(因為在 JVM 實現時就已經存在)。
  • 擴充套件類載入器(Extension ClassLoader):負責載入 JRE/lib/ext 目錄下的擴充套件類庫,也是由 C++ 實現的類載入器。
  • 應用程式類載入器(APP ClassLoader):負責載入應用程式的類,包括在 CLASSPATH 中指定的類庫或者目錄中的 Java 類。
  • 自定義類載入器(Custom ClassLoader):繼承自 ClassLoader 類,實現自己的類載入器,主要用於載入一些自定義的類或者修改某些類的位元組碼。

檢視使用的類載入器

程式碼:

public class ClassLoaderTest {
    public static void main(String[] args) {
        //啟動類載入器
        System.out.println(String.class.getClassLoader());
        //擴充套件類載入器
        System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());
        //應用程式類載入器
        System.out.println(ClassLoaderTest.class.getClassLoader());
        //擴充套件類載入器的父載入器
        System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getParent());
        //應用程式類載入器的父載入器
        System.out.println(ClassLoaderTest.class.getClassLoader().getParent());
    }
}


執行結果:

自定義類載入器

自定義類載入器主要包括兩種型別:

  • 獨立的自定義類載入器,通過過載 ClassLoader 類中的 findClass 方法來實現載入類檔案的功能;
  • 基於 URLClassLoader 類實現的自定義類載入器,使用 URL 的形式來指定類檔案的位置。

過載ClassLoader

程式碼:

public class CustomClassLoader extends ClassLoader {
    private String basePath;

    public CustomClassLoader(String basePath) {
        this.basePath = basePath;
    }

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = getClassData(name);
        if (data == null) {
            throw new ClassNotFoundException();
        } else {
            // 使用 defineClass 方法將 byte 陣列轉換為 Class 物件
            return defineClass(name, data, 0, data.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = basePath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
        try (InputStream inputStream = new FileInputStream(path);
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, length);
            }
            return outputStream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}


說明:

上述程式碼繼承了ClassLoader類,並重寫了其中的findClass()方法,實現從指定目錄中載入類檔案的功能。

findClass()方法中,首先通過getClassData()方法讀取並返回類檔案的位元組陣列,如果獲取的位元組陣列為空,則丟擲ClassNotFoundException異常;否則,使用defineClass()方法將位元組陣列轉換為 Class 物件,並返回該物件。

getClassData()方法中,根據傳入的類名生成類檔案路徑,並使用FileInputStream將類檔案讀入位元組陣列中。

使用:

public class CustomClassLoaderTest {
    public static void main(String[] args) throws Exception {
        // 建立自定義類載入器,指定類檔案所在的目錄
        CustomClassLoader classLoader = new CustomClassLoader("F:\\classes");

        // 使用自定義類載入器載入 Hello 類
        Class<?> clazz = classLoader.loadClass("com.example.something.Hello");
        Object obj = clazz.getDeclaredConstructor().newInstance();
        System.out.println(obj);
    }
}


基於 URLClassLoader

程式碼:

public class CustomURLClassLoader extends URLClassLoader {
    public CustomURLClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 呼叫父類別 loadClass 方法進行委託載入
            Class<?> clazz = super.findClass(name);
            return clazz;
        } catch (ClassNotFoundException e) {
            // 如果父類別無法載入,則嘗試在 URL 中載入
            byte[] data = getClassData(name);
            if (data == null) {
                throw new ClassNotFoundException();
            } else {
                // 使用 defineClass 方法將 byte 陣列轉換為 Class 物件
                return defineClass(name, data, 0, data.length);
            }
        }
    }

    private byte[] getClassData(String className) {
        String path = className.replace('.', '/') + ".class";
        URL[] urls = getURLs();
        for (URL url : urls) {
            try {
                URL classUrl = new URL(url, path);
                // 使用 URLConnection 檢查類檔案是否存在
                try (InputStream is = classUrl.openStream();
                     ByteArrayOutputStream os = new ByteArrayOutputStream()) {
                    byte[] buffer = new byte[1024];
                    int length;
                    while ((length = is.read(buffer)) != -1) {
                        os.write(buffer, 0, length);
                    }
                    return os.toByteArray();
                }
            } catch (IOException e) {
                // ignore and try next URL
            }
        }
        return null;
    }
}


說明:

上述程式碼繼承了 `URLClassLoader` 類,並重寫了其中的 `findClass()` 方法,實現先嚐試使用父類別載入器進行載入,如果無法載入,則嘗試使用 URL 載入類檔案的功能。在 `getClassData()` 方法中,會遍歷 `URLClassLoader` 中定義的 URL,檢查類檔案是否存在,並返回類檔案的位元組陣列,如果無法找到類檔案,則返回 `null`。

使用:

public class CustomURLClassLoaderTest {
    public static void main(String[] args) throws Exception {
        // 建立 URL 陣列,指定類檔案所在的 URL
        URL[] urls = { new URL("file:F:\\classes") };

        // 建立父類別載入器,使用系統類載入器
        ClassLoader parent = ClassLoader.getSystemClassLoader();

        // 建立自定義 URL 類載入器
        CustomURLClassLoader classLoader = new CustomURLClassLoader(urls, parent);

        // 使用自定義 URL 類載入器載入 Hello 類
        Class<?> clazz = classLoader.loadClass("com.example.something.Hello");
        Object obj = clazz.getDeclaredConstructor().newInstance();
        System.out.println(obj);
    }
}


雙親委派

載入器那麼多,我具體是哪個類進行載入得呢?雙親委派機制告訴我答案.

定義

雙親委派是一種Java類載入器的工作機制,它將類載入請求委派給父類別載入器,直到頂級系統類載入器。基本思想是,除非有特殊需求,否則所有類的載入任務都應該由父類別載入器完成,從而保證Java核心庫的型別安全和穩定性,並防止惡意程式碼的自行佈置。如果一個類沒有在父類別載入器中被發現,子類載入器才會嘗試載入該類。這種類載入器之間的父子關係被稱為「雙親委派模型」.

如圖:

意義

為什麼通過雙親委派進行載入呢?

  • 避免重複載入
  • 提高安全性
  • 維護Java平臺的一致性
  • 程式碼優化

Linking

載入過後,我是否就可以被使用了呢?答案是否定的,我還要經歷Lingking 階段,包括Verification、Preparation 和 Resolution。

Verification(驗證)

在驗證階段,Java虛擬機器器會進行語法與語意的檢查,以保證class檔案的完整性和正確性,同時保證被載入的class與虛擬機器器的版本相容。主要的檢查內容包括檔案格式、位元組碼語意、符號參照等。

Preparation(準備)

在準備階段,Java虛擬機器器會為類變數分配記憶體,並且賦予初始值。如果類變數包含有靜態變數,那麼這時也會初始化靜態變數。因此,在這個階段,類變數所使用的空間已經被分配,將其設定為預設初始值即可。

Resolution(解析)

在解析階段,將類或介面中的符號參照轉化為直接參照的過程。在 Java 虛擬機器器載入類時,符號參照是一種指向常數池中某個符號的參照,而直接參照則是指向記憶體中某個位置的直接指標。解析階段可以理解為是在解決類之間的依賴關係,使各個類之間可以像使用自身成員一樣使用別的類中的成員。

初始化

在驗證、準備和解析後,我還要經過初始化,才能被使用。

定義

初始化是指在類載入過程的最後一步,JVM要對類進行一些初始化的操作,確保類可以安全地使用。在這個階段,往往包括靜態變數顯式賦值和靜態程式碼塊執行。

內容

靜態變數顯式賦值

當類載入器完成類的載入、驗證、準備後,在初始化階段,JVM對類的靜態變數進行顯式賦值。如果類定義了多個靜態變數,JVM會按照程式碼中宣告的順序進行初始化,並且若發現此過程需要存取到其他未初始化的類,JVM會先完成這些類的初始化。

靜態程式碼塊的執行

除了靜態變數的顯式賦值,類的靜態程式碼塊也會在初始化階段執行。當JVM執行類載入的Initializing階段時,會執行類中所有靜態程式碼塊的內容,如果類中沒有定義靜態程式碼塊,則不執行。這個過程一般用於在使用之前對類進行初始化。

介面初始化

當一個類在初始化時,如果發現其父類別還未進行初始化,JVM會先對其父類別進行初始化。如果該類實現了介面,也會對這個介面進行初始化操作,介面的初始化過程和類一樣,都會進行靜態變數顯式賦值及靜態程式碼塊執行,同時還會檢查介面中的所有靜態方法。

功能實現

初始化之後,我才真正的進入JVM中,其它小夥伴需要我的時候,只需要建立我的範例,就可以使用我的功能了,得到我幫助得小夥伴都很感謝我。

GC

在JVM中我過得很開心,也留下了很多足跡。在我走後,如何讓我得足跡不對其他小夥伴有影響呢?GC可以幫我解決這個問題。

定義

GC(Garbage Collection)是JVM提供的垃圾回收機制。在Java中,物件是動態分配的,記憶體是由JVM自動管理,而不是由程式設計師手動分配和釋放。當一個物件不再被程式參照時,就應該由垃圾回收器回收其佔用的記憶體,這樣可以防止記憶體漏失和提高記憶體的。

小結

通過我的旅行,你知道JVM是怎麼載入一個類的了麼?我們通過載入、Linking、初始化和使用等各個階段,將Java類完整地載入記憶體並執行其中定義的方法和變數。這個過程中,每個階段都扮演著不同的角色,併為類的正常執行提供了必要的支援。

作者:京東物流 陳昌浩

來源:京東雲開發者社群 自猿其說Tech 轉載請註明來源