一個java文件的JVM之旅

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/11/07/17814733.html
-Advertisement-
Play Games

準備 我是小C同學編寫得一個java文件,如何實現我的功能呢?需要去JVM(Java Virtual Machine)這個地方旅行。 變身 我高高興興的來到JVM,想要開始JVM之旅,它確說:“現在的我還不能進去,需要做一次轉換,生成class文件才行”。為什麼這樣呢? JVM不能直接載入java文 ...


準備

我是小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 轉載請註明來源


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

-Advertisement-
Play Games
更多相關文章
  • 機器學習是通過研究數據和統計信息使電腦學習的過程。機器學習是邁向人工智慧(AI)的一步。機器學習是一個分析數據並學會預測結果的程式。 數據集 在電腦的思維中,數據集是任何數據的集合。它可以是從數組到完整資料庫的任何東西。 數組的示例: [99,86,87,88,111,86,103,87,94, ...
  • @目錄山茶花100ptsT1區間逆序對60pts100pts 區間操作固定套路,轉化為首碼操作dream20pts 神奇分塊杭州:轉化題意,正難則反正難則反(或者對於這種有刪邊操作的題), 我們看成反向加邊看題:構造坐飛機:斜率優化DP抓頹 : 啟髮式合併 + stl大雜燴討厭的線段樹Foo Fig ...
  • Spring5框架概述 Spring是輕量級的開源的JavaEE框架。 Spring可以解決企業應用開發的複雜性。 Spring有兩個核心部分:IOC和AOP IOC:控制反轉,把創建對象過程交給Spring進行管理 AOP:面向切麵,不修改源代碼進行功能增強 Spring特點 方便解耦,簡化開發( ...
  • 1. 左值、右值、左值引用以及右值引用 左值:一般指的是在記憶體中有對應的存儲單元的值,最常見的就是程式中創建的變數 右值:和左值相反,一般指的是沒有對應存儲單元的值(寄存器中的立即數,中間結果等),例如一個常量,或者表達式計算的臨時變數 int x = 10 int y = 20 int z = x ...
  • eclipse下載 官網下載:https://www.eclipse.org/downloads/packages/ 打開後,找到Eclipse IDE for Java Developers點擊進入 進入後點擊右側電腦適配的版本,進入到下載界面點擊“>> Select Another Mirror ...
  • 1 任務調度整體流程 2 組件 調度器 :工廠類創建Scheduler,根據觸發器定義的時間規則調度任務 任務:Job表示被調度的任務 觸發器:Trigger 定義調度時間的元素,按啥時間規則執行任務。一個Job可被多個Trigger關聯,但是一個Trigger 只能關聯一個Job import o ...
  • 進行支付寶開發的第一步就是:配置密鑰。 但是有很多小伙伴都不知道怎麼配置,這篇文章將手把手幫你從頭開始捋清如何配置支付寶密鑰~ ...
  • 在"zookeeper源碼(03)集群啟動流程"中介紹了leader選舉的入口,本文將詳細分析leader選舉組件和流程。 leader選舉流程(重要) quorumPeer的start階段使用startLeaderElection()方法啟動選舉 LOOKING狀態,投自己一票 createEle ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...